Merge remote-tracking branch 'upstream/master' into locmain

# Conflicts:
#	osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
#	osu.Game/osu.Game.csproj
This commit is contained in:
LA
2026-02-17 22:22:22 +08:00
14 changed files with 397 additions and 64 deletions

View File

@@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModEasy : OsuModTestScene
{
protected override bool AllowFail => true;
[Test]
public void TestMultipleApplication()
{
bool reapplied = false;
CreateModTest(new ModTestData
{
Mods = [new OsuModEasy { Retries = { Value = 1 } }],
Autoplay = false,
CreateBeatmap = () =>
{
// do stuff to speed up fails
var b = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
Difficulty = { DrainRate = 10 }
};
foreach (var ho in b.HitObjects)
ho.StartTime /= 4;
return b;
},
PassCondition = () =>
{
if (((ModEasyTestPlayer)Player).FailuresSuppressed > 0 && !reapplied)
{
try
{
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(new BeatmapDifficulty());
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(Player);
}
catch
{
// don't care if this fails. in fact a failure here is probably better than the alternative.
}
finally
{
reapplied = true;
}
}
return Player.GameplayState.HasFailed && ((ModEasyTestPlayer)Player).FailuresSuppressed <= 1;
}
});
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModEasyTestPlayer(CurrentTestData, AllowFail);
private partial class ModEasyTestPlayer : ModTestPlayer
{
public int FailuresSuppressed { get; private set; }
public ModEasyTestPlayer(ModTestData data, bool allowFail)
: base(data, allowFail)
{
}
protected override bool CheckModsAllowFailure()
{
bool failureAllowed = GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
if (!failureAllowed)
FailuresSuppressed++;
return failureAllowed;
}
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils; using osu.Framework.Utils;
@@ -37,13 +38,16 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; } private IDistanceSnapProvider? snapProvider { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>(); private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap) private void load()
{ {
selectedItems.BindTo(editorBeatmap.SelectedHitObjects); selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
} }
@@ -53,15 +57,22 @@ namespace osu.Game.Rulesets.Osu.Edit
base.LoadComplete(); base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState(); selectedItems.CollectionChanged += (_, __) => updateState();
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
updateState(); updateState();
} }
private void hitObjectUpdated(HitObject hitObject)
{
if (selectedMovableObjects.Contains(hitObject))
updateState();
}
private void updateState() private void updateState()
{ {
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanScaleX.Value = quad.Width > 0; CanScaleX.Value = Precision.DefinitelyBigger(quad.Width, 0);
CanScaleY.Value = quad.Height > 0; CanScaleY.Value = Precision.DefinitelyBigger(quad.Height, 0);
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider; IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
@@ -339,5 +350,13 @@ namespace osu.Game.Rulesets.Osu.Edit
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorBeatmap.IsNotNull())
editorBeatmap.HitObjectUpdated -= hitObjectUpdated;
}
} }
} }

View File

@@ -132,6 +132,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true })); AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{ {
new HitCircle new HitCircle
@@ -185,6 +186,7 @@ namespace osu.Game.Tests.Visual.Editing
var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single(); var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single();
return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault; return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault;
}); });
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
@@ -223,6 +225,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add effect points", () => AddStep("add effect points", () =>
{ {
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 }); EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
@@ -253,6 +256,8 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
}); });
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has effect points", () => AddAssert("created difficulty has effect points", () =>
{ {
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[] return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
@@ -284,6 +289,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add effect points", () => AddStep("add effect points", () =>
{ {
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 }); EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
@@ -311,6 +317,8 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
}); });
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has effect points", () => AddAssert("created difficulty has effect points", () =>
{ {
// since this difficulty is on another ruleset, scroll speed specifications are completely reset, // since this difficulty is on another ruleset, scroll speed specifications are completely reset,
@@ -344,6 +352,7 @@ namespace osu.Game.Tests.Visual.Editing
StartTime = 1000 StartTime = 1000
} }
})); }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4); AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4);
AddStep("set combo colours", () => AddStep("set combo colours", () =>
{ {
@@ -394,6 +403,7 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
}); });
AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2);
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);

View File

@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@@ -266,6 +267,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddAssert("still has selection", () => Beatmap.IsDefault, () => Is.False); AddAssert("still has selection", () => Beatmap.IsDefault, () => Is.False);
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0));
} }
[Test] [Test]
@@ -365,13 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Difficulty); SortBy(SortMode.Difficulty);
checkMatchedBeatmaps(6); checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value)); scopeBeatmap(false);
AddStep("click spread indicator", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value));
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
checkMatchedBeatmaps(3); checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape)); AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -389,9 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Artist); SortBy(SortMode.Artist);
checkMatchedBeatmaps(6); checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value)); scopeBeatmap(true);
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
checkMatchedBeatmaps(3); checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape)); AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -413,9 +408,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForFiltering(); WaitForFiltering();
checkMatchedBeatmaps(3); checkMatchedBeatmaps(3);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value)); scopeBeatmap(true);
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
checkMatchedBeatmaps(3); checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape)); AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -424,6 +417,179 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("text filter not emptied", () => filterTextBox.Current.Value, () => Is.Not.Empty); AddAssert("text filter not emptied", () => filterTextBox.Current.Value, () => Is.Not.Empty);
} }
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeRevertsToOriginalSelection(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select normal difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Normal")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
checkMatchedBeatmaps(6);
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeWhenSelectedBeatmapHiddenByFilters(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, findBeatmap("Hard").StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingRuleset(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0, 2);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(2);
scopeBeatmap(grouped);
checkMatchedBeatmaps(2);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
WaitForFiltering();
AddAssert("hard catch difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByShowingConverts(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1).StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("show converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingFilterText(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("set filter text", () => filterTextBox.Current.Value = findBeatmap("Normal").DifficultyName);
WaitForFiltering();
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
}
private void scopeBeatmap(bool grouped)
{
if (grouped)
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
else
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
WaitForFiltering();
}
private BeatmapInfo findBeatmap(string difficultySubstring) => Beatmap.Value.BeatmapSetInfo.Beatmaps.First(b => b.DifficultyName.Contains(difficultySubstring));
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault(); private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));

View File

@@ -154,7 +154,11 @@ namespace osu.Game.Beatmaps
{ {
DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty") DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty")
}; };
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; var newBeatmap = new Beatmap
{
BeatmapInfo = newBeatmapInfo,
Bookmarks = referenceWorkingBeatmap.Beatmap.Bookmarks.ToArray()
};
foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints) foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints)
newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());

View File

@@ -3,17 +3,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToPlayer, IApplicableToHealthProcessor
{ {
[SettingSource("Extra Lives", "Number of extra lives")] [SettingSource("Extra Lives", "Number of extra lives")]
public Bindable<int> Retries { get; } = new BindableInt(2) public Bindable<int> Retries { get; } = new BindableInt(2)
@@ -33,18 +34,26 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries; private int? retries;
private readonly BindableNumber<double> health = new BindableDouble(); private readonly BindableNumber<double> health = new BindableDouble();
public override void ApplyToDifficulty(BeatmapDifficulty difficulty) public void ApplyToPlayer(Player player)
{ {
base.ApplyToDifficulty(difficulty); // this throw works for two reasons:
// - every time `Player` loads, it deep-clones mods into itself, and the deep clone copies *only* `[SettingsSource]` properties
// - `Player` is the only consumer of `IApplicableToPlayer` and it calls `ApplyToPlayer()` exactly once per mod instance
// if either of the above assumptions no longer holds true for any reason, this will need to be reconsidered
if (retries != null)
throw new InvalidOperationException(@"Cannot apply this mod instance to a player twice.");
retries = Retries.Value; retries = Retries.Value;
} }
public bool PerformFail() public bool PerformFail()
{ {
Debug.Assert(retries != null);
if (retries == 0) return true; if (retries == 0) return true;
health.Value = health.MaxValue; health.Value = health.MaxValue;

View File

@@ -46,6 +46,7 @@ namespace osu.Game.Screens.Edit.Components
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>(); private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
private readonly Dictionary<(string name, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>(); private readonly Dictionary<(string name, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>();
private readonly Bindable<DirectoryInfo?> lastSelectedFileDirectory = new Bindable<DirectoryInfo?>();
private FormControlBackground background = null!; private FormControlBackground background = null!;
private FormFieldCaption caption = null!; private FormFieldCaption caption = null!;
@@ -125,6 +126,7 @@ namespace osu.Game.Screens.Edit.Components
Margin = new MarginPadding(5), Margin = new MarginPadding(5),
SampleAddRequested = SampleAddRequested, SampleAddRequested = SampleAddRequested,
SampleRemoveRequested = SampleRemoveRequested, SampleRemoveRequested = SampleRemoveRequested,
LastSelectedFileDirectory = { BindTarget = lastSelectedFileDirectory },
}; };
protected override void LoadComplete() protected override void LoadComplete()
@@ -197,6 +199,7 @@ namespace osu.Game.Screens.Edit.Components
public Action<string>? SampleRemoveRequested { get; init; } public Action<string>? SampleRemoveRequested { get; init; }
private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>(); private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>();
public Bindable<DirectoryInfo?> LastSelectedFileDirectory { get; } = new Bindable<DirectoryInfo?>();
private TrianglesV2? triangles { get; set; } private TrianglesV2? triangles { get; set; }
@@ -313,6 +316,7 @@ namespace osu.Game.Screens.Edit.Components
this.HidePopover(); this.HidePopover();
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString(); ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
LastSelectedFileDirectory.Value = selectedFile.Value.Directory;
} }
private void deleteSample() private void deleteSample()
@@ -324,7 +328,9 @@ namespace osu.Game.Screens.Edit.Components
ActualFilename.Value = null; ActualFilename.Value = null;
} }
public Popover? GetPopover() => ActualFilename.Value == null ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, null) : null; public Popover? GetPopover() => ActualFilename.Value == null
? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, LastSelectedFileDirectory.Value?.FullName)
: null;
public MenuItem[]? ContextMenuItems => public MenuItem[]? ContextMenuItems =>
ActualFilename.Value != null ActualFilename.Value != null

View File

@@ -22,6 +22,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Placeholders; using osu.Game.Online.Placeholders;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User; using osu.Game.Screens.Ranking.Statistics.User;
using osuTK; using osuTK;
@@ -287,6 +288,8 @@ namespace osu.Game.Screens.Ranking.Statistics
preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!"; preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!";
else if (localUserScore.Rank < ScoreRank.C) else if (localUserScore.Rank < ScoreRank.C)
preventTaggingReason = "Set a better score to contribute to beatmap tags!"; preventTaggingReason = "Set a better score to contribute to beatmap tags!";
else if (localUserScore.Mods.Any(m => (m.Type == ModType.Conversion) && !(m is ModClassic)))
preventTaggingReason = "Play this beatmap without conversion mods to contribute to beatmap tags!";
if (preventTaggingReason == null) if (preventTaggingReason == null)
{ {

View File

@@ -22,13 +22,8 @@ namespace osu.Game.Screens.SelectV2
{ {
public partial class ScopedBeatmapSetDisplay : OsuClickableContainer, IKeyBindingHandler<GlobalAction> public partial class ScopedBeatmapSetDisplay : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{ {
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
private Box flashLayer = null!; private Box flashLayer = null!;
private Container content = null!; private Container content = null!;
private OsuTextFlowContainer text = null!; private OsuTextFlowContainer text = null!;
@@ -44,7 +39,7 @@ namespace osu.Game.Screens.SelectV2
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(ISongSelect? songSelect, OverlayColourProvider colourProvider)
{ {
Content.AutoSizeEasing = Easing.OutQuint; Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration; Content.AutoSizeDuration = transition_duration;
@@ -97,25 +92,25 @@ namespace osu.Game.Screens.SelectV2
Alpha = 0, Alpha = 0,
}, },
}); });
Action = () => scopedBeatmapSet.Value = null; Action = () => songSelect?.UnscopeBeatmapSet();
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
scopedBeatmapSet.BindValueChanged(_ => updateState(), true); ScopedBeatmapSet.BindValueChanged(_ => updateState(), true);
} }
private void updateState() private void updateState()
{ {
if (scopedBeatmapSet.Value != null) if (ScopedBeatmapSet.Value != null)
{ {
content.BypassAutoSizeAxes = Axes.None; content.BypassAutoSizeAxes = Axes.None;
text.Clear(); text.Clear();
text.AddText(SongSelectStrings.TemporarilyShowingAllBeatmapsIn); text.AddText(SongSelectStrings.TemporarilyShowingAllBeatmapsIn);
text.AddText(@" "); text.AddText(@" ");
text.AddText(scopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold)); text.AddText(ScopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
} }
else else
{ {
@@ -126,7 +121,7 @@ namespace osu.Game.Screens.SelectV2
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (scopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat) if (ScopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
{ {
TriggerClick(); TriggerClick();
return true; return true;

View File

@@ -41,7 +41,7 @@ namespace osu.Game.Screens.SelectV2
private const float corner_radius = 10; private const float corner_radius = 10;
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>(); public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private SongSelectSearchTextBox searchTextBox = null!; private SongSelectSearchTextBox searchTextBox = null!;
private ShearedToggleButton showConvertedBeatmapsButton = null!; private ShearedToggleButton showConvertedBeatmapsButton = null!;
@@ -57,6 +57,9 @@ namespace osu.Game.Screens.SelectV2
[Resolved] [Resolved]
private Ez2ConfigManager ezConfig { get; set; } = null!; private Ez2ConfigManager ezConfig { get; set; } = null!;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -124,7 +127,7 @@ namespace osu.Game.Screens.SelectV2
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
HoldFocus = true, HoldFocus = true,
ScopedBeatmapSet = ScopedBeatmapSet, ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
}, },
}, },
new GridContainer new GridContainer
@@ -246,7 +249,7 @@ namespace osu.Game.Screens.SelectV2
}, },
new ScopedBeatmapSetDisplay new ScopedBeatmapSetDisplay
{ {
ScopedBeatmapSet = ScopedBeatmapSet, ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
} }
}, },
} }
@@ -413,7 +416,7 @@ namespace osu.Game.Screens.SelectV2
{ {
if (clearScopedSet && ScopedBeatmapSet.Value != null) if (clearScopedSet && ScopedBeatmapSet.Value != null)
{ {
ScopedBeatmapSet.Value = null; songSelect?.UnscopeBeatmapSet();
// because `ScopedBeatmapSet` has a value change callback bound to it that calls `updateCriteria()` again, // because `ScopedBeatmapSet` has a value change callback bound to it that calls `updateCriteria()` again,
// we can just do nothing other than clear it to avoid extra work and duplicated `CriteriaChanged` invocations // we can just do nothing other than clear it to avoid extra work and duplicated `CriteriaChanged` invocations
return; return;
@@ -446,34 +449,22 @@ namespace osu.Game.Screens.SelectV2
internal partial class SongSelectSearchTextBox : ShearedFilterTextBox internal partial class SongSelectSearchTextBox : ShearedFilterTextBox
{ {
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox
{ {
ScopedBeatmapSet = ScopedBeatmapSet, ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
}; };
private partial class InnerTextBox : InnerFilterTextBox private partial class InnerTextBox : InnerFilterTextBox
{ {
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
public override bool HandleLeftRightArrows => false; public override bool HandleLeftRightArrows => false;
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Action == GlobalAction.Back && scopedBeatmapSet.Value != null) if (e.Action == GlobalAction.Back && ScopedBeatmapSet.Value != null)
return false; return false;
return base.OnPressed(e); return base.OnPressed(e);

View File

@@ -48,8 +48,21 @@ namespace osu.Game.Screens.SelectV2
IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap); IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap);
/// <summary> /// <summary>
/// Set this to a non-<see langword="null"/> value in order to temporarily bypass filter and show all difficulties of the given beatmap set. /// Temporarily bypasses filters and shows all difficulties of the given beatmapset.
/// </summary> /// </summary>
Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } /// <param name="beatmapSet">The beatmapset.</param>
void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet);
/// <summary>
/// Removes the beatmapset scope and reverts the previously selected filters.
/// </summary>
void UnscopeBeatmapSet();
/// <summary>
/// Contains the currently scoped beatmapset. Used by external consumers for displaying its state.
/// Cannot be used to change the value, any changes must be done through <see cref="ScopeToBeatmapSet"/>
/// or <see cref="UnscopeBeatmapSet"/>.
/// </summary>
IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
} }
} }

View File

@@ -34,11 +34,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White; protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>(); private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>(); private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200; private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!; private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -59,7 +62,7 @@ namespace osu.Game.Screens.SelectV2
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager) private void load(OsuConfigManager configManager)
{ {
Add(new FillFlowContainer Add(new FillFlowContainer
{ {
@@ -201,7 +204,7 @@ namespace osu.Game.Screens.SelectV2
} }
} }
Action = () => scopedBeatmapSet.Value = BeatmapSet.Value; Action = () => songSelect?.ScopeToBeatmapSet(BeatmapSet.Value);
updateEnabled(); updateEnabled();
} }

View File

@@ -29,11 +29,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White; protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>(); private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>(); private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200; private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!; private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -56,12 +59,12 @@ namespace osu.Game.Screens.SelectV2
Action = () => Action = () =>
{ {
if (Beatmap.Value != null) if (Beatmap.Value != null)
scopedBeatmapSet.Value = Beatmap.Value.BeatmapSet!; songSelect?.ScopeToBeatmapSet(Beatmap.Value.BeatmapSet!);
}; };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager) private void load(OsuConfigManager configManager)
{ {
Add(new FillFlowContainer Add(new FillFlowContainer
{ {

View File

@@ -288,6 +288,7 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
}, },
} }
}, },
@@ -1266,7 +1267,29 @@ namespace osu.Game.Screens.SelectV2
beatmaps.Restore(b); beatmaps.Restore(b);
} }
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet => filterControl.ScopedBeatmapSet; private GroupedBeatmap? beforeScopedSelection;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet => scopedBeatmapSet;
public void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet)
{
beforeScopedSelection = carousel.CurrentGroupedBeatmap;
scopedBeatmapSet.Value = beatmapSet;
}
public void UnscopeBeatmapSet()
{
if (scopedBeatmapSet.Value == null)
return;
if (beforeScopedSelection != null)
queueBeatmapSelection(beforeScopedSelection);
scopedBeatmapSet.Value = null;
beforeScopedSelection = null;
}
#endregion #endregion