diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 1132396608..65ac05261a 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2022.1.0-eap10",
+ "version": "2022.1.1",
"commands": [
"jb"
]
@@ -27,4 +27,4 @@
]
}
}
-}
+}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 514acef525..729f2f266d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
- key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
+ key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
- name: Dotnet code style
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index cb922c5a58..bc285dbe11 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 5ecd9cc675..718ada1905 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 33ad0ac4f7..6b9c3f4d63 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 5ecd9cc675..718ada1905 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 97d9dbc380..98dc28d915 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,10 +52,10 @@
-
+
-
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index eb9045d9ce..405f0a8006 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -4,8 +4,6 @@
using System;
using System.IO;
using System.Runtime.Versioning;
-using System.Threading;
-using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
using osu.Framework;
using osu.Framework.Development;
@@ -63,8 +61,6 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
{
- host.ExceptionThrown += handleException;
-
if (!host.IsPrimaryInstance)
{
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
@@ -131,23 +127,5 @@ namespace osu.Desktop
// tools.SetProcessAppUserModelId();
});
}
-
- private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
-
- ///
- /// Allow a maximum of one unhandled exception, per second of execution.
- ///
- ///
- private static bool handleException(Exception arg)
- {
- bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
-
- Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
-
- // restore the stock of allowable exceptions after a short delay.
- Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
-
- return continueExecution;
- }
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index a4f309c6ac..a4f9e2671b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 434c0e0367..36ffd3b5b6 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index fc6d900567..b957ade952 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index ddad2adfea..d3b4b378c0 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 4ce29ab5c7..2c0d3fd937 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index a6b8eb8651..ce468d399b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index f9c13a8169..b7bfe14402 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -136,6 +136,37 @@ namespace osu.Game.Tests.Database
});
}
+ [Test]
+ public void TestAddFileToAsyncImportedBeatmap()
+ {
+ RunTestWithRealm((realm, storage) =>
+ {
+ BeatmapSetInfo? detachedSet = null;
+
+ using (var importer = new BeatmapModelManager(realm, storage))
+ using (new RealmRulesetStore(realm, storage))
+ {
+ Task.Run(async () =>
+ {
+ Live? beatmapSet;
+
+ using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+ // ReSharper disable once AccessToDisposedClosure
+ beatmapSet = await importer.Import(reader);
+
+ Assert.NotNull(beatmapSet);
+ Debug.Assert(beatmapSet != null);
+
+ // Intentionally detach on async thread as to not trigger a refresh on the main thread.
+ beatmapSet.PerformRead(s => detachedSet = s.Detach());
+ }).WaitSafely();
+
+ Debug.Assert(detachedSet != null);
+ importer.AddFile(detachedSet, new MemoryStream(), "test");
+ }
+ });
+ }
+
[Test]
public void TestImportBeatmapThenCleanup()
{
diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs
new file mode 100644
index 0000000000..b9ea1f2567
--- /dev/null
+++ b/osu.Game.Tests/Mods/ModSettingsTest.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Tests.Mods
+{
+ public class ModSettingsTest
+ {
+ [Test]
+ public void TestModSettingsUnboundWhenCopied()
+ {
+ var original = new OsuModDoubleTime();
+ var copy = (OsuModDoubleTime)original.DeepClone();
+
+ original.SpeedChange.Value = 2;
+
+ Assert.That(original.SpeedChange.Value, Is.EqualTo(2.0));
+ Assert.That(copy.SpeedChange.Value, Is.EqualTo(1.5));
+ }
+
+ [Test]
+ public void TestMultiModSettingsUnboundWhenCopied()
+ {
+ var original = new MultiMod(new OsuModDoubleTime());
+ var copy = (MultiMod)original.DeepClone();
+
+ ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2;
+
+ Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
+ Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
new file mode 100644
index 0000000000..3992d9abe6
--- /dev/null
+++ b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Tests.Mods
+{
+ public class TestCustomisableModRuleset : Ruleset
+ {
+ public static RulesetInfo CreateTestRulesetInfo() => new TestCustomisableModRuleset().RulesetInfo;
+
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ if (type == ModType.Conversion)
+ {
+ return new Mod[]
+ {
+ new TestModCustomisable1(),
+ new TestModCustomisable2()
+ };
+ }
+
+ return Array.Empty();
+ }
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
+
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
+
+ public override string Description { get; } = "test";
+ public override string ShortName { get; } = "tst";
+
+ public class TestModCustomisable1 : TestModCustomisable
+ {
+ public override string Name => "Customisable Mod 1";
+
+ public override string Acronym => "CM1";
+ }
+
+ public class TestModCustomisable2 : TestModCustomisable
+ {
+ public override string Name => "Customisable Mod 2";
+
+ public override string Acronym => "CM2";
+
+ public override bool RequiresConfiguration => true;
+ }
+
+ public abstract class TestModCustomisable : Mod, IApplicableMod
+ {
+ public override double ScoreMultiplier => 1.0;
+
+ public override string Description => "This is a customisable test mod.";
+
+ public override ModType Type => ModType.Conversion;
+
+ [SettingSource("Sample float", "Change something for a mod")]
+ public BindableFloat SliderBindable { get; } = new BindableFloat
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Default = 5,
+ Value = 7
+ };
+
+ [SettingSource("Sample bool", "Clicking this changes a setting")]
+ public BindableBool TickBindable { get; } = new BindableBool();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
index 4012a672ed..7c05abc2cd 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
@@ -66,6 +67,13 @@ namespace osu.Game.Tests.Visual.Editing
});
}
+ [Test]
+ public void TestPopoverHasFocus()
+ {
+ clickDifficultyPiece(0);
+ velocityPopoverHasFocus();
+ }
+
[Test]
public void TestSingleSelection()
{
@@ -133,6 +141,15 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left);
});
+ private void velocityPopoverHasFocus() => AddUntilStep("velocity popover textbox focused", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+ var textbox = slider?.ChildrenOfType().Single();
+
+ return textbox?.HasFocus == true;
+ });
+
private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
{
var popover = this.ChildrenOfType().SingleOrDefault();
@@ -151,6 +168,7 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover()
{
+ AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
index dca30a6fc0..4501eea88e 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
@@ -57,6 +57,13 @@ namespace osu.Game.Tests.Visual.Editing
});
}
+ [Test]
+ public void TestPopoverHasFocus()
+ {
+ clickSamplePiece(0);
+ samplePopoverHasFocus();
+ }
+
[Test]
public void TestSingleSelection()
{
@@ -173,14 +180,23 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasSingleBank("normal");
}
- private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
+ private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{
- var difficultyPiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
+ var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
- InputManager.MoveMouseTo(difficultyPiece);
+ InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left);
});
+ private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+ var textbox = slider?.ChildrenOfType().Single();
+
+ return textbox?.HasFocus == true;
+ });
+
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
{
var popover = this.ChildrenOfType().SingleOrDefault();
@@ -215,8 +231,9 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover()
{
+ AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
- AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
+ AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
}
private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index 8df32c500e..81763564fa 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -24,7 +24,7 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
-using osu.Game.Tests.Visual.UserInterface;
+using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
new Drawable[]
{
- recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = recorder = new TestReplayRecorder(new Score
{
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
},
new Drawable[]
{
- playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
deleted file mode 100644
index f8fab784cc..0000000000
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Screens.Play.HUD;
-using osu.Game.Screens.Play.PlayerSettings;
-
-namespace osu.Game.Tests.Visual.Gameplay
-{
- [TestFixture]
- public class TestSceneReplaySettingsOverlay : OsuTestScene
- {
- public TestSceneReplaySettingsOverlay()
- {
- ExampleContainer container;
-
- Add(new PlayerSettingsOverlay
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- State = { Value = Visibility.Visible }
- });
-
- Add(container = new ExampleContainer());
-
- AddStep(@"Add button", () => container.Add(new TriangleButton
- {
- RelativeSizeAxes = Axes.X,
- Text = @"Button",
- }));
-
- AddStep(@"Add checkbox", () => container.Add(new PlayerCheckbox
- {
- LabelText = "Checkbox",
- }));
-
- AddStep(@"Add textbox", () => container.Add(new FocusedTextBox
- {
- RelativeSizeAxes = Axes.X,
- Height = 30,
- PlaceholderText = "Textbox",
- HoldFocus = false,
- }));
- }
-
- private class ExampleContainer : PlayerSettingsGroup
- {
- public ExampleContainer()
- : base("example")
- {
- }
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 4ec46036f6..f8748922cf 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -27,8 +27,8 @@ using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Tests.Mods;
using osu.Game.Tests.Visual.Spectator;
-using osu.Game.Tests.Visual.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
new Drawable[]
{
- recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = recorder = new TestReplayRecorder
{
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
},
new Drawable[]
{
- playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
index 26a0301d8a..f40c31b07f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
@@ -1,21 +1,105 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneFreeModSelectOverlay : MultiplayerTestScene
{
- [SetUp]
- public new void Setup() => Schedule(() =>
+ private FreeModSelectOverlay freeModSelectOverlay;
+ private readonly Bindable>> availableMods = new Bindable>>();
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGameBase osuGameBase)
{
- Child = new FreeModSelectOverlay
+ availableMods.BindTo(osuGameBase.AvailableMods);
+ }
+
+ [Test]
+ public void TestFreeModSelect()
+ {
+ createFreeModSelect();
+
+ AddUntilStep("all visible mods are playable",
+ () => this.ChildrenOfType()
+ .Where(panel => panel.IsPresent)
+ .All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
+
+ AddToggleStep("toggle visibility", visible =>
+ {
+ if (freeModSelectOverlay != null)
+ freeModSelectOverlay.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
+ });
+ }
+
+ [Test]
+ public void TestCustomisationNotAvailable()
+ {
+ createFreeModSelect();
+
+ AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ AddWaitStep("wait some", 3);
+ AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0);
+ }
+
+ [Test]
+ public void TestSelectDeselectAll()
+ {
+ createFreeModSelect();
+
+ AddStep("click select all button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1));
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("all mods selected", assertAllAvailableModsSelected);
+
+ AddStep("click deselect all button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Last());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
+ }
+
+ private void createFreeModSelect()
+ {
+ AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
{
State = { Value = Visibility.Visible }
- };
- });
+ });
+ AddUntilStep("all column content loaded",
+ () => freeModSelectOverlay.ChildrenOfType().Any()
+ && freeModSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
+ }
+
+ private bool assertAllAvailableModsSelected()
+ {
+ var allAvailableMods = availableMods.Value
+ .SelectMany(pair => pair.Value)
+ .Where(mod => mod.UserPlayable && mod.HasImplementation)
+ .ToList();
+
+ foreach (var availableMod in allAvailableMods)
+ {
+ if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
+ return false;
+ }
+
+ return true;
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs
deleted file mode 100644
index 4eb14542ba..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Testing;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Screens.OnlinePlay;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneFreeModSelectScreen : MultiplayerTestScene
- {
- private FreeModSelectScreen freeModSelectScreen;
- private readonly Bindable>> availableMods = new Bindable>>();
-
- [BackgroundDependencyLoader]
- private void load(OsuGameBase osuGameBase)
- {
- availableMods.BindTo(osuGameBase.AvailableMods);
- }
-
- [Test]
- public void TestFreeModSelect()
- {
- createFreeModSelect();
-
- AddUntilStep("all visible mods are playable",
- () => this.ChildrenOfType()
- .Where(panel => panel.IsPresent)
- .All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
-
- AddToggleStep("toggle visibility", visible =>
- {
- if (freeModSelectScreen != null)
- freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
- });
- }
-
- [Test]
- public void TestCustomisationNotAvailable()
- {
- createFreeModSelect();
-
- AddStep("select difficulty adjust", () => freeModSelectScreen.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
- AddWaitStep("wait some", 3);
- AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0);
- }
-
- [Test]
- public void TestSelectDeselectAll()
- {
- createFreeModSelect();
-
- AddStep("click select all button", () =>
- {
- InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1));
- InputManager.Click(MouseButton.Left);
- });
- AddUntilStep("all mods selected", assertAllAvailableModsSelected);
-
- AddStep("click deselect all button", () =>
- {
- InputManager.MoveMouseTo(this.ChildrenOfType().Last());
- InputManager.Click(MouseButton.Left);
- });
- AddUntilStep("all mods deselected", () => !freeModSelectScreen.SelectedMods.Value.Any());
- }
-
- private void createFreeModSelect()
- {
- AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
- {
- State = { Value = Visibility.Visible }
- });
- AddUntilStep("all column content loaded",
- () => freeModSelectScreen.ChildrenOfType().Any()
- && freeModSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
- }
-
- private bool assertAllAvailableModsSelected()
- {
- var allAvailableMods = availableMods.Value
- .SelectMany(pair => pair.Value)
- .Where(mod => mod.UserPlayable && mod.HasImplementation)
- .ToList();
-
- foreach (var availableMod in allAvailableMods)
- {
- if (freeModSelectScreen.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 97cb08e3c1..8e45d99eae 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -627,7 +627,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
- AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+ AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 061fe5715b..eacd80925d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertHasFreeModButton(Type type, bool hasButton = true)
{
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
- () => this.ChildrenOfType()
+ () => this.ChildrenOfType()
.Single()
.ChildrenOfType()
.Where(panel => !panel.Filtered.Value)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 6173580f0b..ca79fa9cb8 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contents loaded",
() => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded));
AddUntilStep("mod select contains only double time mod",
- () => this.ChildrenOfType()
+ () => this.ChildrenOfType()
.SingleOrDefault()?
.ChildrenOfType()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index 0f8337deb6..e4871f611e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
-using osu.Game.Utils;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -33,7 +32,6 @@ namespace osu.Game.Tests.Visual.Navigation
private IReadOnlyList requiredGameDependencies => new[]
{
typeof(OsuGame),
- typeof(SentryLogger),
typeof(OsuLogo),
typeof(IdleTracker),
typeof(OnScreenDisplay),
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 9674ef7ae1..a3e0caedb9 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Navigation
public class TestPlaySongSelect : PlaySongSelect
{
- public ModSelectScreen ModSelectOverlay => ModSelect;
+ public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index a1d51683e4..2a5fc050d3 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking
score.Accuracy = accuracy;
score.Rank = rank;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
+ loadResultsScreen(() => screen = createResultsScreen(score));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
@@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
UnrankedSoloResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createUnrankedSoloResultsScreen()));
+ loadResultsScreen(() => screen = createUnrankedSoloResultsScreen());
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
@@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
@@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
@@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
ScorePanel expandedPanel = null;
@@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.Ranking
var tcs = new TaskCompletionSource();
- AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task)));
+ loadResultsScreen(() => screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
@@ -255,7 +255,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value);
@@ -276,7 +276,7 @@ namespace osu.Game.Tests.Visual.Ranking
var ruleset = new RulesetWithNoPerformanceCalculator();
var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo);
- AddStep("load results", () => Child = new TestResultsContainer(createResultsScreen(score)));
+ loadResultsScreen(() => createResultsScreen(score));
AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddAssert("PP displayed as 0", () =>
@@ -287,6 +287,22 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
+ private void loadResultsScreen(Func createResults)
+ {
+ ResultsScreen results = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(results = createResults()));
+
+ // expanded panel should be centered the moment results screen is loaded
+ // but can potentially be scrolled away on certain specific load scenarios.
+ // see: https://github.com/ppy/osu/issues/18226
+ AddUntilStep("expanded panel in centre of screen", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, results.ScreenSpaceDrawQuad.Centre.X, 1);
+ });
+ }
+
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo());
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 083e24be7b..aad7f6b301 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -1008,7 +1008,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
public new BeatmapCarousel Carousel => base.Carousel;
- public new ModSelectScreen ModSelect => base.ModSelect;
+ public new ModSelectOverlay ModSelect => base.ModSelect;
public new void PresentScore(ScoreInfo score) => base.PresentScore(score);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
index 39298f56ba..48b5690243 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
@@ -4,9 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -25,9 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private FirstRunSetupOverlay overlay;
- private readonly Mock performer = new Mock();
+ private readonly Mock performer = new Mock();
- private readonly Mock notificationOverlay = new Mock();
+ private readonly Mock notificationOverlay = new Mock();
private Notification lastNotification;
@@ -37,8 +39,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void load()
{
Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
- Dependencies.CacheAs(performer.Object);
- Dependencies.CacheAs(notificationOverlay.Object);
+ Dependencies.CacheAs(performer.Object);
+ Dependencies.CacheAs(notificationOverlay.Object);
}
[SetUpSteps]
@@ -72,7 +74,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
- [Ignore("Enable when first run setup is being displayed on first run.")]
public void TestDoesntOpenOnSecondRun()
{
AddStep("set first run", () => LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, true));
@@ -196,5 +197,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps);
}
+
+ // interface mocks break hot reload, mocking this stub implementation instead works around it.
+ // see: https://github.com/moq/moq4/issues/1252
+ [UsedImplicitly]
+ public class TestNotificationOverlay : INotificationOverlay
+ {
+ public virtual void Post(Notification notification)
+ {
+ }
+
+ public virtual void Hide()
+ {
+ }
+
+ public virtual IBindable UnreadCount => null;
+ }
+
+ // interface mocks break hot reload, mocking this stub implementation instead works around it.
+ // see: https://github.com/moq/moq4/issues/1252
+ [UsedImplicitly]
+ public class TestPerformerFromScreenRunner : IPerformFromScreenRunner
+ {
+ public virtual void PerformFromScreen(Action action, IEnumerable validScreens = null)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs
deleted file mode 100644
index fdc21d80ff..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Overlays.Mods;
-using osu.Game.Rulesets.Mods;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- public class TestSceneModButton : OsuTestScene
- {
- public TestSceneModButton()
- {
- Children = new Drawable[]
- {
- new ModButton(new MultiMod(new TestMod1(), new TestMod2(), new TestMod3(), new TestMod4()))
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- }
- };
- }
-
- private class TestMod1 : TestMod
- {
- public override string Name => "Test mod 1";
-
- public override string Acronym => "M1";
- }
-
- private class TestMod2 : TestMod
- {
- public override string Name => "Test mod 2";
-
- public override string Acronym => "M2";
-
- public override IconUsage? Icon => FontAwesome.Solid.Exclamation;
- }
-
- private class TestMod3 : TestMod
- {
- public override string Name => "Test mod 3";
-
- public override string Acronym => "M3";
-
- public override IconUsage? Icon => FontAwesome.Solid.ArrowRight;
- }
-
- private class TestMod4 : TestMod
- {
- public override string Name => "Test mod 4";
-
- public override string Acronym => "M4";
- }
-
- private abstract class TestMod : Mod, IApplicableMod
- {
- public override double ScoreMultiplier => 1.0;
-
- public override string Description => "This is a test mod.";
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index b429619044..fc543d9db7 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
@@ -10,45 +10,209 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mania;
-using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Screens.Play.HUD;
using osuTK;
-using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
- [Description("mod select and icon display")]
- public class TestSceneModSelectOverlay : OsuTestScene
+ [TestFixture]
+ public class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
{
- private RulesetStore rulesets;
- private ModDisplay modDisplay;
- private TestModSelectOverlay modSelect;
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; }
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- this.rulesets = rulesets;
- }
-
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- SelectedMods.Value = Array.Empty();
- createDisplay(() => new TestModSelectOverlay());
- });
+ private UserModSelectOverlay modSelectOverlay;
[SetUpSteps]
public void SetUpSteps()
{
- AddStep("show", () => modSelect.Show());
+ AddStep("clear contents", Clear);
+ AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
+ AddStep("reset mods", () => SelectedMods.SetDefault());
+ }
+
+ private void createScreen()
+ {
+ AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ SelectedMods = { BindTarget = SelectedMods }
+ });
+ waitForColumnLoad();
+ }
+
+ [Test]
+ public void TestStateChange()
+ {
+ createScreen();
+ AddStep("toggle state", () => modSelectOverlay.ToggleVisibility());
+ }
+
+ [Test]
+ public void TestPreexistingSelection()
+ {
+ AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
+ createScreen();
+ AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("mod multiplier correct", () =>
+ {
+ double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
+ return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
+ AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestExternalSelection()
+ {
+ createScreen();
+ AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
+ AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("mod multiplier correct", () =>
+ {
+ double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
+ return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
+ AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestRulesetChange()
+ {
+ createScreen();
+ changeRuleset(0);
+ changeRuleset(1);
+ changeRuleset(2);
+ changeRuleset(3);
+ }
+
+ [Test]
+ public void TestIncompatibilityToggling()
+ {
+ createScreen();
+ changeRuleset(0);
+
+ AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
+ AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
+ AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
+
+ AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
+ AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
+ AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
+ AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
+
+ AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
+ AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
+ && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
+ AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
+ AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
+
+ AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
+ AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
+ && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
+ AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
+ AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
+ AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
+ }
+
+ [Test]
+ public void TestDimmedState()
+ {
+ createScreen();
+ changeRuleset(0);
+
+ AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value));
+
+ ModColumn lastColumn = null;
+
+ AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value);
+ AddStep("request scroll to last column", () =>
+ {
+ var lastDimContainer = this.ChildrenOfType().Last();
+ lastColumn = lastDimContainer.Column;
+ lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
+ });
+ AddUntilStep("column undimmed", () => lastColumn.Active.Value);
+
+ AddStep("click panel", () =>
+ {
+ InputManager.MoveMouseTo(lastColumn.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("panel selected", () => lastColumn.ChildrenOfType().First().Active.Value);
+ }
+
+ [Test]
+ public void TestCustomisationToggleState()
+ {
+ createScreen();
+ assertCustomisationToggleState(disabled: true, active: false);
+
+ AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("dismiss mod customisation via toggle", () =>
+ {
+ InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("reset mods", () => SelectedMods.SetDefault());
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
+ assertCustomisationToggleState(disabled: true, active: false);
+
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
+ assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
+ }
+
+ [Test]
+ public void TestDismissCustomisationViaDimmedArea()
+ {
+ createScreen();
+ assertCustomisationToggleState(disabled: true, active: false);
+
+ AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+
+ AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("move mouse to dimmed area", () =>
+ {
+ InputManager.MoveMouseTo(new Vector2(
+ modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.X,
+ (modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.Y + modSelectOverlay.ScreenSpaceDrawQuad.BottomLeft.Y) / 2));
+ });
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First()));
+ AddAssert("first mod panel is hovered", () => modSelectOverlay.ChildrenOfType().First().IsHovered);
}
///
@@ -58,10 +222,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSettingsNotCrossPolluting()
{
Bindable> selectedMods2 = null;
+ ModSelectOverlay modSelectOverlay2 = null;
+ createScreen();
AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
- AddStep("set setting", () => modSelect.ChildrenOfType>().First().Current.Value = 8);
+ AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
@@ -69,7 +235,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create second overlay", () =>
{
- Add(modSelect = new TestModSelectOverlay().With(d =>
+ Add(modSelectOverlay2 = new UserModSelectOverlay().With(d =>
{
d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre;
@@ -77,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}));
});
- AddStep("show", () => modSelect.Show());
+ AddStep("show", () => modSelectOverlay2.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType().Single().CircleSize.Value == null);
@@ -88,83 +254,51 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
+ createScreen();
changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
- AddStep("deselect", () => modSelect.DeselectAllButton.TriggerClick());
+ AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
- AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).TriggerClick());
+ AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
}
[Test]
public void TestAnimationFlushOnClose()
{
+ createScreen();
changeRuleset(0);
AddStep("Select all fun mods", () =>
{
- modSelect.ModSectionsContainer
- .Single(c => c.ModType == ModType.DifficultyIncrease)
- .SelectAll();
+ modSelectOverlay.ChildrenOfType()
+ .Single(c => c.ModType == ModType.DifficultyIncrease)
+ .SelectAll();
});
- AddUntilStep("many mods selected", () => modDisplay.Current.Value.Count >= 5);
+ AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () =>
{
- modSelect.ModSectionsContainer
- .Single(c => c.ModType == ModType.DifficultyIncrease)
- .DeselectAll();
+ modSelectOverlay.ChildrenOfType()
+ .Single(c => c.ModType == ModType.DifficultyIncrease)
+ .DeselectAll();
- modSelect.Hide();
+ modSelectOverlay.Hide();
});
- AddAssert("all mods deselected", () => modDisplay.Current.Value.Count == 0);
- }
-
- [Test]
- public void TestOsuMods()
- {
- changeRuleset(0);
-
- var osu = new OsuRuleset();
-
- var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
- var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
-
- var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
-
- var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
-
- var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
- var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
-
- testSingleMod(noFailMod);
- testMultiMod(doubleTimeMod);
- testIncompatibleMods(easy, hardRock);
- testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
- }
-
- [Test]
- public void TestManiaMods()
- {
- changeRuleset(3);
-
- var mania = new ManiaRuleset();
-
- testModsWithSameBaseType(
- mania.CreateMod(),
- mania.CreateMod());
+ AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestRulesetChanges()
{
+ createScreen();
changeRuleset(0);
var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
@@ -173,42 +307,42 @@ namespace osu.Game.Tests.Visual.UserInterface
changeRuleset(0);
- AddAssert("ensure mods still selected", () => modDisplay.Current.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
+ AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
changeRuleset(3);
- AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
+ AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
changeRuleset(0);
- AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
+ AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestExternallySetCustomizedMod()
{
+ createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () =>
{
- var button = modSelect.GetModButton(SelectedMods.Value.Single());
- return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01;
+ var button = getPanelForMod(SelectedMods.Value.Single().GetType());
+ return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
});
}
[Test]
public void TestSettingsAreRetainedOnReload()
{
+ createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
-
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
- AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
-
+ createScreen();
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
@@ -218,236 +352,155 @@ namespace osu.Game.Tests.Visual.UserInterface
Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null;
+ createScreen();
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
- var button = modSelect.GetModButton(SelectedMods.Value.Single());
- overlayButtonMod = button.SelectedMod;
- return overlayButtonMod.GetType() == external.GetType();
+ var button = getPanelForMod(SelectedMods.Value.Single().GetType());
+ overlayButtonMod = button.Mod;
+ return button.Active.Value;
});
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
- AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod));
- AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external));
- }
-
- [Test]
- public void TestNonStacked()
- {
- changeRuleset(0);
-
- AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
-
- AddStep("show", () => modSelect.Show());
-
- AddAssert("ensure all buttons are spread out", () => modSelect.ChildrenOfType().All(m => m.Mods.Length <= 1));
+ AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
+ AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
}
[Test]
public void TestChangeIsValidChangesButtonVisibility()
{
+ createScreen();
changeRuleset(0);
- AddAssert("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
+ AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
- AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime));
- AddUntilStep("double time not visible", () => modSelect.ChildrenOfType().All(b => !b.Mods.Any(m => m is OsuModDoubleTime)));
- AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
+ AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
+ AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
+ AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
- AddStep("make double time valid again", () => modSelect.IsValidMod = m => true);
- AddUntilStep("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
- AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
+ AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = m => true);
+ AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
+ AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
}
[Test]
public void TestChangeIsValidPreservesSelection()
{
+ createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
- AddAssert("DT + HD selected", () => modSelect.ChildrenOfType().Count(b => b.Selected) == 2);
+ AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
- AddStep("make NF invalid", () => modSelect.IsValidMod = m => !(m is ModNoFail));
- AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType().Count(b => b.Selected) == 2);
+ AddStep("make NF invalid", () => modSelectOverlay.IsValidMod = m => !(m is ModNoFail));
+ AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
}
[Test]
public void TestUnimplementedModIsUnselectable()
{
var testRuleset = new TestUnimplementedModOsuRuleset();
- changeTestRuleset(testRuleset.RulesetInfo);
- var conversionMods = testRuleset.GetModsFor(ModType.Conversion);
+ createScreen();
- var unimplementedMod = conversionMods.FirstOrDefault(m => m is TestUnimplementedMod);
+ AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
+ waitForColumnLoad();
- testUnimplementedMod(unimplementedMod);
+ AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
}
- private void testSingleMod(Mod mod)
+ [Test]
+ public void TestDeselectAllViaButton()
{
- selectNext(mod);
- checkSelected(mod);
+ createScreen();
+ changeRuleset(0);
- selectPrevious(mod);
- checkNotSelected(mod);
+ AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
+ AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
- selectNext(mod);
- selectNext(mod);
- checkNotSelected(mod);
-
- selectPrevious(mod);
- selectPrevious(mod);
- checkNotSelected(mod);
- }
-
- private void testMultiMod(MultiMod multiMod)
- {
- foreach (var mod in multiMod.Mods)
+ AddStep("click deselect all button", () =>
{
- selectNext(mod);
- checkSelected(mod);
- }
-
- for (int index = multiMod.Mods.Length - 1; index >= 0; index--)
- selectPrevious(multiMod.Mods[index]);
-
- foreach (var mod in multiMod.Mods)
- checkNotSelected(mod);
- }
-
- private void testUnimplementedMod(Mod mod)
- {
- selectNext(mod);
- checkNotSelected(mod);
- }
-
- private void testIncompatibleMods(Mod modA, Mod modB)
- {
- selectNext(modA);
- checkSelected(modA);
- checkNotSelected(modB);
-
- selectNext(modB);
- checkSelected(modB);
- checkNotSelected(modA);
-
- selectPrevious(modB);
- checkNotSelected(modA);
- checkNotSelected(modB);
- }
-
- private void testDeselectAll(IEnumerable mods)
- {
- foreach (var mod in mods)
- selectNext(mod);
-
- AddAssert("check for any selection", () => modSelect.SelectedMods.Value.Any());
- AddStep("deselect all", () => modSelect.DeselectAllButton.Action.Invoke());
- AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any());
- }
-
- private void testModsWithSameBaseType(Mod modA, Mod modB)
- {
- selectNext(modA);
- checkSelected(modA);
- selectNext(modB);
- checkSelected(modB);
-
- // Backwards
- selectPrevious(modA);
- checkSelected(modA);
- }
-
- private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
-
- private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
-
- private void checkSelected(Mod mod)
- {
- AddAssert($"check {mod.Name} is selected", () =>
- {
- var button = modSelect.GetModButton(mod);
- return modSelect.SelectedMods.Value.SingleOrDefault(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
+ InputManager.MoveMouseTo(this.ChildrenOfType().Last());
+ InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
- private void changeRuleset(int? onlineId)
+ [Test]
+ public void TestCloseViaBackButton()
{
- AddStep($"change ruleset to {(onlineId?.ToString() ?? "none")}", () => { Ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == onlineId); });
- waitForLoad();
- }
+ createScreen();
+ changeRuleset(0);
- private void changeTestRuleset(RulesetInfo rulesetInfo)
- {
- AddStep($"change ruleset to {rulesetInfo.Name}", () => { Ruleset.Value = rulesetInfo; });
- waitForLoad();
- }
+ AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: true);
+ AddAssert("back button disabled", () => !this.ChildrenOfType().First().Enabled.Value);
- private void waitForLoad() =>
- AddUntilStep("wait for icons to load", () => modSelect.AllLoaded);
-
- private void checkNotSelected(Mod mod)
- {
- AddAssert($"check {mod.Name} is not selected", () =>
+ AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
+ AddStep("click back button", () =>
{
- var button = modSelect.GetModButton(mod);
- return modSelect.SelectedMods.Value.All(m => m.GetType() != mod.GetType()) && button.SelectedMod?.GetType() != mod.GetType();
+ InputManager.MoveMouseTo(this.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
});
+ AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
- private void createDisplay(Func createOverlayFunc)
+ [Test]
+ public void TestColumnHiding()
{
- Children = new Drawable[]
+ AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
{
- modSelect = createOverlayFunc().With(d =>
- {
- d.Origin = Anchor.BottomCentre;
- d.Anchor = Anchor.BottomCentre;
- d.SelectedMods.BindTarget = SelectedMods;
- }),
- modDisplay = new ModDisplay
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- Position = new Vector2(-5, 25),
- Current = { BindTarget = modSelect.SelectedMods }
- }
- };
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ SelectedMods = { BindTarget = SelectedMods },
+ IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
+ });
+ waitForColumnLoad();
+ changeRuleset(0);
+
+ AddAssert("two columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2);
+
+ AddStep("unset filter", () => modSelectOverlay.IsValidMod = _ => true);
+ AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
+
+ AddStep("filter out everything", () => modSelectOverlay.IsValidMod = _ => false);
+ AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent));
+
+ AddStep("hide", () => modSelectOverlay.Hide());
+ AddStep("set filter for 3 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
+ || mod.Type == ModType.Automation
+ || mod.Type == ModType.Conversion);
+
+ AddStep("show", () => modSelectOverlay.Show());
+ AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3);
}
- private class TestModSelectOverlay : UserModSelectOverlay
+ private void waitForColumnLoad() => AddUntilStep("all column content loaded",
+ () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
+
+ private void changeRuleset(int id)
{
- public new Bindable> SelectedMods => base.SelectedMods;
-
- public bool AllLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
-
- public new FillFlowContainer ModSectionsContainer =>
- base.ModSectionsContainer;
-
- public ModButton GetModButton(Mod mod)
- {
- var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type);
- return section.ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
- }
-
- public new TriangleButton DeselectAllButton => base.DeselectAllButton;
-
- public new Color4 LowMultiplierColour => base.LowMultiplierColour;
- public new Color4 HighMultiplierColour => base.HighMultiplierColour;
+ AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
+ waitForColumnLoad();
}
- private class TestNonStackedModSelectOverlay : TestModSelectOverlay
+ private void assertCustomisationToggleState(bool disabled, bool active)
{
- protected override bool Stacked => false;
+ ShearedToggleButton getToggle() => modSelectOverlay.ChildrenOfType().Single();
+
+ AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
+ AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
}
+ private ModPanel getPanelForMod(Type modType)
+ => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType);
+
private class TestUnimplementedMod : Mod
{
public override string Name => "Unimplemented mod";
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs
deleted file mode 100644
index fa7758df59..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs
+++ /dev/null
@@ -1,525 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Testing;
-using osu.Framework.Utils;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Mods;
-using osu.Game.Overlays.Settings;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
-using osuTK;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- [TestFixture]
- public class TestSceneModSelectScreen : OsuManualInputManagerTestScene
- {
- [Resolved]
- private RulesetStore rulesetStore { get; set; }
-
- private UserModSelectScreen modSelectScreen;
-
- [SetUpSteps]
- public void SetUpSteps()
- {
- AddStep("clear contents", Clear);
- AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
- AddStep("reset mods", () => SelectedMods.SetDefault());
- }
-
- private void createScreen()
- {
- AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
- {
- RelativeSizeAxes = Axes.Both,
- State = { Value = Visibility.Visible },
- SelectedMods = { BindTarget = SelectedMods }
- });
- waitForColumnLoad();
- }
-
- [Test]
- public void TestStateChange()
- {
- createScreen();
- AddStep("toggle state", () => modSelectScreen.ToggleVisibility());
- }
-
- [Test]
- public void TestPreexistingSelection()
- {
- AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
- createScreen();
- AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
- AddAssert("mod multiplier correct", () =>
- {
- double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
- return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value);
- });
- assertCustomisationToggleState(disabled: false, active: false);
- AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any());
- }
-
- [Test]
- public void TestExternalSelection()
- {
- createScreen();
- AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
- AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
- AddAssert("mod multiplier correct", () =>
- {
- double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
- return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value);
- });
- assertCustomisationToggleState(disabled: false, active: false);
- AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any());
- }
-
- [Test]
- public void TestRulesetChange()
- {
- createScreen();
- changeRuleset(0);
- changeRuleset(1);
- changeRuleset(2);
- changeRuleset(3);
- }
-
- [Test]
- public void TestIncompatibilityToggling()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
- AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
- AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
-
- AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
- AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
- AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
- AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
-
- AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
- AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
- && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
- AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
- AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
-
- AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
- AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
- && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
- AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
- AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
- AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
- }
-
- [Test]
- public void TestDimmedState()
- {
- createScreen();
- changeRuleset(0);
-
- AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value));
-
- ModColumn lastColumn = null;
-
- AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value);
- AddStep("request scroll to last column", () =>
- {
- var lastDimContainer = this.ChildrenOfType().Last();
- lastColumn = lastDimContainer.Column;
- lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
- });
- AddUntilStep("column undimmed", () => lastColumn.Active.Value);
-
- AddStep("click panel", () =>
- {
- InputManager.MoveMouseTo(lastColumn.ChildrenOfType().First());
- InputManager.Click(MouseButton.Left);
- });
- AddUntilStep("panel selected", () => lastColumn.ChildrenOfType().First().Active.Value);
- }
-
- [Test]
- public void TestCustomisationToggleState()
- {
- createScreen();
- assertCustomisationToggleState(disabled: true, active: false);
-
- AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
- assertCustomisationToggleState(disabled: false, active: false);
-
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
- assertCustomisationToggleState(disabled: false, active: true);
-
- AddStep("dismiss mod customisation via toggle", () =>
- {
- InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single());
- InputManager.Click(MouseButton.Left);
- });
- assertCustomisationToggleState(disabled: false, active: false);
-
- AddStep("reset mods", () => SelectedMods.SetDefault());
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
- assertCustomisationToggleState(disabled: false, active: true);
-
- AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
- assertCustomisationToggleState(disabled: false, active: false);
-
- AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
- assertCustomisationToggleState(disabled: false, active: false);
-
- AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
- assertCustomisationToggleState(disabled: true, active: false);
-
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
- assertCustomisationToggleState(disabled: false, active: true);
-
- AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
- assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
- }
-
- [Test]
- public void TestDismissCustomisationViaDimmedArea()
- {
- createScreen();
- assertCustomisationToggleState(disabled: true, active: false);
-
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
- assertCustomisationToggleState(disabled: false, active: true);
-
- AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
- AddStep("move mouse to dimmed area", () =>
- {
- InputManager.MoveMouseTo(new Vector2(
- modSelectScreen.ScreenSpaceDrawQuad.TopLeft.X,
- (modSelectScreen.ScreenSpaceDrawQuad.TopLeft.Y + modSelectScreen.ScreenSpaceDrawQuad.BottomLeft.Y) / 2));
- });
- AddStep("click", () => InputManager.Click(MouseButton.Left));
- assertCustomisationToggleState(disabled: false, active: false);
-
- AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().First()));
- AddAssert("first mod panel is hovered", () => modSelectScreen.ChildrenOfType().First().IsHovered);
- }
-
- ///
- /// Ensure that two mod overlays are not cross polluting via central settings instances.
- ///
- [Test]
- public void TestSettingsNotCrossPolluting()
- {
- Bindable> selectedMods2 = null;
- ModSelectScreen modSelectScreen2 = null;
-
- createScreen();
- AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
-
- AddStep("set setting", () => modSelectScreen.ChildrenOfType>().First().Current.Value = 8);
-
- AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
-
- AddStep("create second bindable", () => selectedMods2 = new Bindable>(new Mod[] { new OsuModDifficultyAdjust() }));
-
- AddStep("create second overlay", () =>
- {
- Add(modSelectScreen2 = new UserModSelectScreen().With(d =>
- {
- d.Origin = Anchor.TopCentre;
- d.Anchor = Anchor.TopCentre;
- d.SelectedMods.BindTarget = selectedMods2;
- }));
- });
-
- AddStep("show", () => modSelectScreen2.Show());
-
- AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
- AddAssert("ensure second is default", () => selectedMods2.Value.OfType().Single().CircleSize.Value == null);
- }
-
- [Test]
- public void TestSettingsResetOnDeselection()
- {
- var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
-
- createScreen();
- changeRuleset(0);
-
- AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
-
- AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
-
- AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
- AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
-
- AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
- AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
- }
-
- [Test]
- public void TestAnimationFlushOnClose()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("Select all fun mods", () =>
- {
- modSelectScreen.ChildrenOfType()
- .Single(c => c.ModType == ModType.DifficultyIncrease)
- .SelectAll();
- });
-
- AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
-
- AddStep("trigger deselect and close overlay", () =>
- {
- modSelectScreen.ChildrenOfType()
- .Single(c => c.ModType == ModType.DifficultyIncrease)
- .DeselectAll();
-
- modSelectScreen.Hide();
- });
-
- AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
- }
-
- [Test]
- public void TestRulesetChanges()
- {
- createScreen();
- changeRuleset(0);
-
- var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
-
- AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; });
-
- changeRuleset(0);
-
- AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
-
- changeRuleset(3);
-
- AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
-
- changeRuleset(0);
-
- AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
- }
-
- [Test]
- public void TestExternallySetCustomizedMod()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
-
- AddAssert("ensure button is selected and customized accordingly", () =>
- {
- var button = getPanelForMod(SelectedMods.Value.Single().GetType());
- return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
- });
- }
-
- [Test]
- public void TestSettingsAreRetainedOnReload()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
- AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
-
- createScreen();
- AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
- }
-
- [Test]
- public void TestExternallySetModIsReplacedByOverlayInstance()
- {
- Mod external = new OsuModDoubleTime();
- Mod overlayButtonMod = null;
-
- createScreen();
- changeRuleset(0);
-
- AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
-
- AddAssert("ensure button is selected", () =>
- {
- var button = getPanelForMod(SelectedMods.Value.Single().GetType());
- overlayButtonMod = button.Mod;
- return button.Active.Value;
- });
-
- // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
- AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
-
- AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
- AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
- AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
- }
-
- [Test]
- public void TestChangeIsValidChangesButtonVisibility()
- {
- createScreen();
- changeRuleset(0);
-
- AddAssert("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
-
- AddStep("make double time invalid", () => modSelectScreen.IsValidMod = m => !(m is OsuModDoubleTime));
- AddUntilStep("double time not visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
- AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
-
- AddStep("make double time valid again", () => modSelectScreen.IsValidMod = m => true);
- AddUntilStep("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
- AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
- }
-
- [Test]
- public void TestChangeIsValidPreservesSelection()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
- AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
-
- AddStep("make NF invalid", () => modSelectScreen.IsValidMod = m => !(m is ModNoFail));
- AddAssert("DT + HD still selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
- }
-
- [Test]
- public void TestUnimplementedModIsUnselectable()
- {
- var testRuleset = new TestUnimplementedModOsuRuleset();
-
- createScreen();
-
- AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
- waitForColumnLoad();
-
- AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
- }
-
- [Test]
- public void TestDeselectAllViaButton()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
- AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
-
- AddStep("click deselect all button", () =>
- {
- InputManager.MoveMouseTo(this.ChildrenOfType().Last());
- InputManager.Click(MouseButton.Left);
- });
- AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
- }
-
- [Test]
- public void TestCloseViaBackButton()
- {
- createScreen();
- changeRuleset(0);
-
- AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
- assertCustomisationToggleState(disabled: false, active: true);
- AddAssert("back button disabled", () => !this.ChildrenOfType().First().Enabled.Value);
-
- AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
- AddStep("click back button", () =>
- {
- InputManager.MoveMouseTo(this.ChildrenOfType().First());
- InputManager.Click(MouseButton.Left);
- });
- AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden);
- }
-
- [Test]
- public void TestColumnHiding()
- {
- AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
- {
- RelativeSizeAxes = Axes.Both,
- State = { Value = Visibility.Visible },
- SelectedMods = { BindTarget = SelectedMods },
- IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
- });
- waitForColumnLoad();
- changeRuleset(0);
-
- AddAssert("two columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2);
-
- AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true);
- AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
-
- AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false);
- AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent));
-
- AddStep("hide", () => modSelectScreen.Hide());
- AddStep("set filter for 3 columns", () => modSelectScreen.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
- || mod.Type == ModType.Automation
- || mod.Type == ModType.Conversion);
-
- AddStep("show", () => modSelectScreen.Show());
- AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3);
- }
-
- private void waitForColumnLoad() => AddUntilStep("all column content loaded",
- () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
-
- private void changeRuleset(int id)
- {
- AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
- waitForColumnLoad();
- }
-
- private void assertCustomisationToggleState(bool disabled, bool active)
- {
- ShearedToggleButton getToggle() => modSelectScreen.ChildrenOfType().Single();
-
- AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
- AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
- }
-
- private ModPanel getPanelForMod(Type modType)
- => modSelectScreen.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType);
-
- private class TestUnimplementedMod : Mod
- {
- public override string Name => "Unimplemented mod";
- public override string Acronym => "UM";
- public override string Description => "A mod that is not implemented.";
- public override double ScoreMultiplier => 1;
- public override ModType Type => ModType.Conversion;
- }
-
- private class TestUnimplementedModOsuRuleset : OsuRuleset
- {
- public override string ShortName => "unimplemented";
-
- public override IEnumerable GetModsFor(ModType type)
- {
- if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
-
- return base.GetModsFor(type);
- }
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
deleted file mode 100644
index 9a3083e8db..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Utils;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Configuration;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Mods;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Difficulty;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.UI;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- public class TestSceneModSettings : OsuManualInputManagerTestScene
- {
- private TestModSelectOverlay modSelect;
-
- private readonly Mod testCustomisableMod = new TestModCustomisable1();
-
- private readonly Mod testCustomisableAutoOpenMod = new TestModCustomisable2();
-
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- SelectedMods.Value = Array.Empty();
- Ruleset.Value = CreateTestRulesetInfo();
- });
-
- [Test]
- public void TestButtonShowsOnCustomisableMod()
- {
- createModSelect();
- openModSelect();
-
- AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value);
- AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded);
- AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod));
- AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
- AddStep("open Customisation", () => modSelect.CustomiseButton.TriggerClick());
- AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod));
- AddAssert("controls hidden", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
- }
-
- [Test]
- public void TestButtonShowsOnModAlreadyAdded()
- {
- AddStep("set active mods", () => SelectedMods.Value = new List { testCustomisableMod });
-
- createModSelect();
-
- AddAssert("mods still active", () => SelectedMods.Value.Count == 1);
-
- openModSelect();
- AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
- }
-
- [Test]
- public void TestCustomisationMenuVisibility()
- {
- createModSelect();
- openModSelect();
-
- AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
- AddStep("select mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
- AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible);
- AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
- AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
- }
-
- [Test]
- public void TestModSettingsUnboundWhenCopied()
- {
- OsuModDoubleTime original = null;
- OsuModDoubleTime copy = null;
-
- AddStep("create mods", () =>
- {
- original = new OsuModDoubleTime();
- copy = (OsuModDoubleTime)original.DeepClone();
- });
-
- AddStep("change property", () => original.SpeedChange.Value = 2);
-
- AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value));
- AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
- }
-
- [Test]
- public void TestMultiModSettingsUnboundWhenCopied()
- {
- MultiMod original = null;
- MultiMod copy = null;
-
- AddStep("create mods", () =>
- {
- original = new MultiMod(new OsuModDoubleTime());
- copy = (MultiMod)original.DeepClone();
- });
-
- AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
-
- AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
- AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
- }
-
- [Test]
- public void TestCustomisationMenuNoClickthrough()
- {
- createModSelect();
- openModSelect();
-
- AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
- AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
- AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible);
- AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
- AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
- AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
- AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
- AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
- AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
- }
-
- private void createModSelect()
- {
- AddStep("create mod select", () =>
- {
- Child = modSelect = new TestModSelectOverlay
- {
- Origin = Anchor.BottomCentre,
- Anchor = Anchor.BottomCentre,
- SelectedMods = { BindTarget = SelectedMods }
- };
- });
- }
-
- private void openModSelect()
- {
- AddStep("open", () => modSelect.Show());
- AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded);
- }
-
- private class TestModSelectOverlay : UserModSelectOverlay
- {
- public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer;
- public new TriangleButton CustomiseButton => base.CustomiseButton;
-
- public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
-
- public ModButton GetModButton(Mod mod)
- {
- return ModSectionsContainer.ChildrenOfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
- }
-
- public void SelectMod(Mod mod) =>
- GetModButton(mod).SelectNext(1);
-
- public void SetModSettingsWidth(float newWidth) =>
- ModSettingsContainer.Parent.Width = newWidth;
- }
-
- public static RulesetInfo CreateTestRulesetInfo() => new TestCustomisableModRuleset().RulesetInfo;
-
- public class TestCustomisableModRuleset : Ruleset
- {
- public override IEnumerable GetModsFor(ModType type)
- {
- if (type == ModType.Conversion)
- {
- return new Mod[]
- {
- new TestModCustomisable1(),
- new TestModCustomisable2()
- };
- }
-
- return Array.Empty();
- }
-
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
-
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
-
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
-
- public override string Description { get; } = "test";
- public override string ShortName { get; } = "tst";
- }
-
- private class TestModCustomisable1 : TestModCustomisable
- {
- public override string Name => "Customisable Mod 1";
-
- public override string Acronym => "CM1";
- }
-
- private class TestModCustomisable2 : TestModCustomisable
- {
- public override string Name => "Customisable Mod 2";
-
- public override string Acronym => "CM2";
-
- public override bool RequiresConfiguration => true;
- }
-
- private abstract class TestModCustomisable : Mod, IApplicableMod
- {
- public override double ScoreMultiplier => 1.0;
-
- public override string Description => "This is a customisable test mod.";
-
- public override ModType Type => ModType.Conversion;
-
- [SettingSource("Sample float", "Change something for a mod")]
- public BindableFloat SliderBindable { get; } = new BindableFloat
- {
- MinValue = 0,
- MaxValue = 10,
- Default = 5,
- Value = 7
- };
-
- [SettingSource("Sample bool", "Clicking this changes a setting")]
- public BindableBool TickBindable { get; } = new BindableBool();
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
index 9ccfba7c74..f45c55d912 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
@@ -1,44 +1,40 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneRoundedButton : OsuTestScene
+ public class TestSceneRoundedButton : ThemeComparisonTestScene
{
- [Test]
- public void TestBasic()
+ private readonly BindableBool enabled = new BindableBool(true);
+
+ protected override Drawable CreateContent() => new RoundedButton
{
- RoundedButton button = null;
+ Width = 400,
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Enabled = { BindTarget = enabled },
+ };
- AddStep("create button", () => Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Colour4.DarkGray
- },
- button = new RoundedButton
- {
- Width = 400,
- Text = "Test button",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Action = () => { }
- }
- }
- });
+ [Test]
+ public void TestDisabled()
+ {
+ AddToggleStep("toggle disabled", disabled => enabled.Value = !disabled);
+ }
- AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { });
+ [Test]
+ public void TestBackgroundColour()
+ {
+ AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
+ AddAssert("first button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
new file mode 100644
index 0000000000..8ef24e58a0
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
@@ -0,0 +1,62 @@
+// Copyright (c) ppy Pty Ltd . 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.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneSettingsToolboxGroup : OsuManualInputManagerTestScene
+ {
+ private SettingsToolboxGroup group;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = group = new SettingsToolboxGroup("example")
+ {
+ Children = new Drawable[]
+ {
+ new RoundedButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = @"Button",
+ Enabled = { Value = true },
+ },
+ new OsuCheckbox
+ {
+ LabelText = @"Checkbox",
+ },
+ new OutlinedTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 30,
+ PlaceholderText = @"Textbox",
+ }
+ },
+ };
+ });
+
+ [Test]
+ public void TestClickExpandButtonMultipleTimes()
+ {
+ AddAssert("group expanded by default", () => group.Expanded.Value);
+ AddStep("click expand button multiple times", () =>
+ {
+ InputManager.MoveMouseTo(group.ChildrenOfType().Single());
+ Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 100);
+ Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 200);
+ Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 300);
+ });
+ AddAssert("group contracted", () => !group.Expanded.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 0bcf533653..a1eef4ce47 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index c7314a4969..6fd53d923b 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 64e77384a2..026a83cceb 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@@ -164,6 +166,20 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorHitAnimations, false);
}
+ public IDictionary GetLoggableState() =>
+ new Dictionary(ConfigStore.Where(kvp => !keyContainsPrivateInformation(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()));
+
+ private static bool keyContainsPrivateInformation(OsuSetting argKey)
+ {
+ switch (argKey)
+ {
+ case OsuSetting.Token:
+ return true;
+ }
+
+ return false;
+ }
+
public OsuConfigManager(Storage storage)
: base(storage)
{
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index b0a70b51d0..937876a70e 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -344,6 +344,26 @@ namespace osu.Game.Database
}
}
+ ///
+ /// Write changes to realm.
+ ///
+ /// The work to run.
+ public T Write(Func action)
+ {
+ if (ThreadSafety.IsUpdateThread)
+ {
+ total_writes_update.Value++;
+ return Realm.Write(action);
+ }
+ else
+ {
+ total_writes_async.Value++;
+
+ using (var realm = getRealmInstance())
+ return realm.Write(action);
+ }
+ }
+
///
/// Write changes to realm.
///
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 29a797bd78..08514d94c3 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -33,9 +32,12 @@ namespace osu.Game.Graphics.UserInterface
private Color4? backgroundColour;
+ ///
+ /// Sets a custom background colour to this button, replacing the provided default.
+ ///
public Color4 BackgroundColour
{
- get => backgroundColour ?? Color4.White;
+ get => backgroundColour ?? defaultBackgroundColour;
set
{
backgroundColour = value;
@@ -43,6 +45,23 @@ namespace osu.Game.Graphics.UserInterface
}
}
+ private Color4 defaultBackgroundColour;
+
+ ///
+ /// Sets a default background colour to this button.
+ ///
+ protected Color4 DefaultBackgroundColour
+ {
+ get => defaultBackgroundColour;
+ set
+ {
+ defaultBackgroundColour = value;
+
+ if (backgroundColour == null)
+ Background.FadeColour(value);
+ }
+ }
+
protected override Container Content { get; }
protected Box Hover;
@@ -89,8 +108,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- if (backgroundColour == null)
- BackgroundColour = colours.BlueDark;
+ DefaultBackgroundColour = colours.BlueDark;
}
protected override void LoadComplete()
@@ -106,10 +124,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
- {
- Debug.Assert(backgroundColour != null);
- Background.FlashColour(backgroundColour.Value.Lighten(0.4f), 200);
- }
+ Background.FlashColour(BackgroundColour.Lighten(0.4f), 200);
return base.OnClick(e);
}
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index b1d4691938..4e391c8221 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -184,14 +184,12 @@ namespace osu.Game.Graphics.UserInterface
protected override void UpdateBackgroundColour()
{
- if (!IsPreSelected && !IsSelected)
- {
- Background.FadeOut(600, Easing.OutQuint);
- return;
- }
-
- Background.FadeIn(100, Easing.OutQuint);
Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint);
+
+ if (IsPreSelected || IsSelected)
+ Background.FadeIn(100, Easing.OutQuint);
+ else
+ Background.FadeOut(600, Easing.OutQuint);
}
protected override void UpdateForegroundColour()
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
index f535a32b39..ec56b6d784 100644
--- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
-using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -29,8 +28,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
{
- if (BackgroundColour == Color4.White)
- BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
+ DefaultBackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
}
protected override void LoadComplete()
diff --git a/osu.Game/Localisation/ModSelectScreenStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs
similarity index 94%
rename from osu.Game/Localisation/ModSelectScreenStrings.cs
rename to osu.Game/Localisation/ModSelectOverlayStrings.cs
index 0c113fd381..e9af7147e3 100644
--- a/osu.Game/Localisation/ModSelectScreenStrings.cs
+++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs
@@ -5,9 +5,9 @@ using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
- public static class ModSelectScreenStrings
+ public static class ModSelectOverlayStrings
{
- private const string prefix = @"osu.Game.Resources.Localisation.ModSelectScreen";
+ private const string prefix = @"osu.Game.Resources.Localisation.ModSelectOverlay";
///
/// "Mod Select"
@@ -26,4 +26,4 @@ namespace osu.Game.Localisation
private static string getKey(string key) => $@"{prefix}:{key}";
}
-}
\ No newline at end of file
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 54c4231b06..3d56d33689 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -14,6 +14,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -56,6 +57,8 @@ using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Graphics;
+using Sentry;
+using Logger = osu.Framework.Logging.Logger;
namespace osu.Game
{
@@ -258,7 +261,7 @@ namespace osu.Game
{
dependencies.CacheAs(this);
- dependencies.Cache(SentryLogger);
+ SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
@@ -1197,6 +1200,15 @@ namespace osu.Game
private void screenChanged(IScreen current, IScreen newScreen)
{
+ SentrySdk.ConfigureScope(scope =>
+ {
+ scope.Contexts[@"screen stack"] = new
+ {
+ Current = newScreen?.GetType().ReadableName(),
+ Previous = current?.GetType().ReadableName(),
+ };
+ });
+
switch (newScreen)
{
case IntroScreen intro:
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 324fcada89..2e4758a134 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -85,6 +86,8 @@ namespace osu.Game
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
+ internal const string BUILD_SUFFIX = "lazer";
+
public virtual string Version
{
get
@@ -93,7 +96,7 @@ namespace osu.Game
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
var version = AssemblyVersion;
- return $@"{version.Major}.{version.Minor}.{version.Build}-lazer";
+ return $@"{version.Major}.{version.Minor}.{version.Build}-{BUILD_SUFFIX}";
}
}
@@ -180,9 +183,21 @@ namespace osu.Game
///
protected DatabaseContextFactory EFContextFactory { get; private set; }
+ ///
+ /// Number of unhandled exceptions to allow before aborting execution.
+ ///
+ ///
+ /// When an unhandled exception is encountered, an internal count will be decremented.
+ /// If the count hits zero, the game will crash.
+ /// Each second, the count is incremented until reaching the value specified.
+ ///
+ protected virtual int UnhandledExceptionsBeforeCrash => DebugUtils.IsDebugBuild ? 0 : 1;
+
public OsuGameBase()
{
Name = @"osu!";
+
+ allowableExceptions = UnhandledExceptionsBeforeCrash;
}
[BackgroundDependencyLoader]
@@ -408,6 +423,8 @@ namespace osu.Game
LocalConfig ??= UseDevelopmentServer
? new DevelopmentOsuConfigManager(Storage)
: new OsuConfigManager(Storage);
+
+ host.ExceptionThrown += onExceptionThrown;
}
///
@@ -505,6 +522,23 @@ namespace osu.Game
AvailableMods.Value = dict;
}
+ private int allowableExceptions;
+
+ ///
+ /// Allows a maximum of one unhandled exception, per second of execution.
+ ///
+ private bool onExceptionThrown(Exception _)
+ {
+ bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
+
+ Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
+
+ // restore the stock of allowable exceptions after a short delay.
+ Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
+
+ return continueExecution;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -514,6 +548,9 @@ namespace osu.Game
LocalConfig?.Dispose();
realm?.Dispose();
+
+ if (Host != null)
+ Host.ExceptionThrown -= onExceptionThrown;
}
}
}
diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
index 1f18d181cb..d1ea91e51a 100644
--- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
+++ b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
@@ -19,13 +19,16 @@ namespace osu.Game.Overlays.FirstRunSetup
protected FillFlowContainer Content { get; private set; }
+ protected const float CONTENT_FONT_SIZE = 16;
+
+ protected const float HEADER_FONT_SIZE = 24;
+
[Resolved]
protected OverlayColourProvider OverlayColourProvider { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
- const float header_size = 40;
const float spacing = 20;
InternalChildren = new Drawable[]
@@ -33,23 +36,29 @@ namespace osu.Game.Overlays.FirstRunSetup
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
- ScrollbarOverlapsContent = false,
- Children = new Drawable[]
+ Masking = false,
+ Child = new Container
{
- new OsuSpriteText
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 30 },
+ Children = new Drawable[]
{
- Text = this.GetLocalisableDescription(),
- Font = OsuFont.Default.With(size: header_size),
- Colour = OverlayColourProvider.Light1,
+ new OsuSpriteText
+ {
+ Text = this.GetLocalisableDescription(),
+ Font = OsuFont.TorusAlternate.With(size: HEADER_FONT_SIZE),
+ Colour = OverlayColourProvider.Light1,
+ },
+ Content = new FillFlowContainer
+ {
+ Y = HEADER_FONT_SIZE + spacing,
+ Spacing = new Vector2(spacing),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ }
},
- Content = new FillFlowContainer
- {
- Y = header_size + spacing,
- Spacing = new Vector2(spacing),
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- }
},
}
};
@@ -59,7 +68,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
base.OnEntering(e);
this
- .FadeInFromZero(500)
+ .FadeInFromZero(100)
.MoveToX(offset)
.MoveToX(0, 500, Easing.OutQuint);
}
@@ -68,7 +77,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
base.OnResuming(e);
this
- .FadeInFromZero(500)
+ .FadeInFromZero(100)
.MoveToX(0, 500, Easing.OutQuint);
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
index 190a0badab..66acdca8c7 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
@@ -46,11 +46,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader(permitNulls: true)]
private void load(LegacyImportManager? legacyImportManager)
{
- Vector2 buttonSize = new Vector2(500, 60);
+ Vector2 buttonSize = new Vector2(400, 50);
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.Description,
@@ -63,7 +63,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Height = 30,
Children = new Drawable[]
{
- currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold))
+ currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: HEADER_FONT_SIZE, weight: FontWeight.SemiBold))
{
Colour = OverlayColourProvider.Content2,
TextAnchor = Anchor.Centre,
@@ -73,7 +73,7 @@ namespace osu.Game.Overlays.FirstRunSetup
},
}
},
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription,
@@ -89,7 +89,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.TutorialButton,
Action = downloadTutorial
},
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.BundledDescription,
@@ -105,7 +105,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.BundledButton,
Action = downloadBundled
},
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
@@ -131,7 +131,7 @@ namespace osu.Game.Overlays.FirstRunSetup
}));
}
},
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs
index dc3d40ad95..1a88e6a842 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs
@@ -9,7 +9,7 @@ using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
@@ -22,11 +22,11 @@ namespace osu.Game.Overlays.FirstRunSetup
private SearchContainer searchContainer;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuColour colours)
{
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Text = FirstRunSetupOverlayStrings.BehaviourDescription,
RelativeSizeAxes = Axes.X,
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
new[]
{
- new TriangleButton
+ new RoundedButton
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
@@ -59,10 +59,11 @@ namespace osu.Game.Overlays.FirstRunSetup
Action = applyStandard,
},
Empty(),
- new DangerousTriangleButton
+ new RoundedButton
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
+ BackgroundColour = colours.Pink3,
Text = FirstRunSetupOverlayStrings.ClassicDefaults,
RelativeSizeAxes = Axes.X,
Action = applyClassic
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
index 152d67ab27..8452691bb5 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
@@ -35,9 +35,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
+ const float screen_width = 640;
+
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Text = FirstRunSetupOverlayStrings.UIScaleDescription,
RelativeSizeAxes = Axes.X,
@@ -54,7 +56,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.None,
- Size = new Vector2(960, 960 / 16f * 9 / 2),
+ Size = new Vector2(screen_width, screen_width / 16f * 9 / 2),
Children = new Drawable[]
{
new GridContainer
@@ -123,6 +125,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private class SampleScreenContainer : CompositeDrawable
{
+ private readonly OsuScreen screen;
// Minimal isolation from main game.
[Cached]
@@ -142,6 +145,12 @@ namespace osu.Game.Overlays.FirstRunSetup
public override bool PropagatePositionalInputSubTree => false;
public override bool PropagateNonPositionalInputSubTree => false;
+ public SampleScreenContainer(OsuScreen screen)
+ {
+ this.screen = screen;
+ RelativeSizeAxes = Axes.Both;
+ }
+
[BackgroundDependencyLoader]
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{
@@ -149,13 +158,8 @@ namespace osu.Game.Overlays.FirstRunSetup
Beatmap.Value.LoadTrack();
Ruleset.Value = rulesets.AvailableRulesets.First();
- }
- public SampleScreenContainer(Screen screen)
- {
OsuScreenStack stack;
- RelativeSizeAxes = Axes.Both;
-
OsuLogo logo;
Padding = new MarginPadding(5);
@@ -189,7 +193,8 @@ namespace osu.Game.Overlays.FirstRunSetup
},
};
- stack.Push(screen);
+ // intentionally load synchronously so it is included in the initial load of the first run screen.
+ stack.PushSynchronously(screen);
}
}
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
index 10e15a7555..420d630857 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs
index 607bef76dd..cebb2f5e3b 100644
--- a/osu.Game/Overlays/FirstRunSetupOverlay.cs
+++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
+using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@@ -62,12 +63,15 @@ namespace osu.Game.Overlays
typeof(ScreenBehaviour),
};
- private Container stackContainer = null!;
+ private Container screenContent = null!;
private Bindable? overlayActivationMode;
private Container content = null!;
+ private LoadingSpinner loading = null!;
+ private ScheduledDelegate? loadingShowDelegate;
+
public FirstRunSetupOverlay()
: base(OverlayColourScheme.Purple)
{
@@ -86,36 +90,48 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
+ Padding = new MarginPadding { Bottom = 20, },
+ Child = new GridContainer
{
- Horizontal = 70 * 1.2f,
- Bottom = 20,
- },
- Child = new InputBlockingContainer
- {
- Masking = true,
- CornerRadius = 14,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ ColumnDimensions = new[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background6,
- },
- stackContainer = new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Vertical = 20,
- Horizontal = 70,
- },
- }
+ new Dimension(),
+ new Dimension(minSize: 640, maxSize: 800),
+ new Dimension(),
},
- },
+ Content = new[]
+ {
+ new[]
+ {
+ Empty(),
+ new InputBlockingContainer
+ {
+ Masking = true,
+ CornerRadius = 14,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourProvider.Background6,
+ },
+ loading = new LoadingSpinner(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Vertical = 20 },
+ Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
+ },
+ },
+ },
+ Empty(),
+ },
+ }
+ }
},
});
@@ -171,8 +187,7 @@ namespace osu.Game.Overlays
config.BindWith(OsuSetting.ShowFirstRunSetup, showFirstRunSetup);
- // TODO: uncomment when happy with the whole flow.
- // if (showFirstRunSetup.Value) Show();
+ if (showFirstRunSetup.Value) Show();
}
public override bool OnPressed(KeyBindingPressEvent e)
@@ -269,7 +284,7 @@ namespace osu.Game.Overlays
{
Debug.Assert(currentStepIndex == null);
- stackContainer.Child = stack = new ScreenStack
+ screenContent.Child = stack = new ScreenStack
{
RelativeSizeAxes = Axes.Both,
};
@@ -300,12 +315,20 @@ namespace osu.Game.Overlays
if (currentStepIndex < steps.Length)
{
- stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value]));
+ var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
+
+ loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
+ nextScreen.OnLoadComplete += _ =>
+ {
+ loadingShowDelegate?.Cancel();
+ loading.Hide();
+ };
+
+ stack.Push(nextScreen);
}
else
{
- // TODO: uncomment when happy with the whole flow.
- // showFirstRunSetup.Value = false;
+ showFirstRunSetup.Value = false;
currentStepIndex = null;
Hide();
}
diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs
deleted file mode 100644
index 6e2cb40596..0000000000
--- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Utils;
-using osuTK;
-
-namespace osu.Game.Overlays.Mods
-{
- public class IncompatibilityDisplayingModButton : ModButton
- {
- private readonly CompositeDrawable incompatibleIcon;
-
- [Resolved]
- private Bindable> selectedMods { get; set; }
-
- public IncompatibilityDisplayingModButton(Mod mod)
- : base(mod)
- {
- ButtonContent.Add(incompatibleIcon = new IncompatibleIcon
- {
- Anchor = Anchor.BottomRight,
- Origin = Anchor.Centre,
- Position = new Vector2(-13),
- });
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateCompatibility), true);
- }
-
- protected override void DisplayMod(Mod mod)
- {
- base.DisplayMod(mod);
-
- Scheduler.AddOnce(updateCompatibility);
- }
-
- private void updateCompatibility()
- {
- var m = SelectedMod ?? Mods.First();
-
- bool isIncompatible = false;
-
- if (selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(m))
- isIncompatible = !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(m));
-
- if (isIncompatible)
- incompatibleIcon.Show();
- else
- incompatibleIcon.Hide();
- }
-
- public override ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip();
- }
-}
diff --git a/osu.Game/Overlays/Mods/IncompatibleIcon.cs b/osu.Game/Overlays/Mods/IncompatibleIcon.cs
deleted file mode 100644
index df134fe4a4..0000000000
--- a/osu.Game/Overlays/Mods/IncompatibleIcon.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
-using osu.Game.Graphics;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.Mods
-{
- public class IncompatibleIcon : VisibilityContainer, IHasTooltip
- {
- private Circle circle;
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Size = new Vector2(20);
-
- State.Value = Visibility.Hidden;
- Alpha = 0;
-
- InternalChildren = new Drawable[]
- {
- circle = new Circle
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colours.Gray4,
- },
- new SpriteIcon
- {
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Size = new Vector2(0.6f),
- Icon = FontAwesome.Solid.Slash,
- Colour = Color4.White,
- Shadow = true,
- }
- };
- }
-
- protected override void PopIn()
- {
- this.FadeIn(200, Easing.OutQuint);
- circle.FlashColour(Color4.Red, 500, Easing.OutQuint);
- this.ScaleTo(1.8f).Then().ScaleTo(1, 500, Easing.OutQuint);
- }
-
- protected override void PopOut()
- {
- this.FadeOut(200, Easing.OutQuint);
- this.ScaleTo(0.8f, 200, Easing.In);
- }
-
- public LocalisableString TooltipText => "Incompatible with current selected mods";
- }
-}
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
deleted file mode 100644
index 979e2c8da3..0000000000
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osuTK;
-using osuTK.Graphics;
-using osuTK.Input;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.UI;
-using System;
-using System.Linq;
-using osu.Framework.Graphics.Cursor;
-using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
-using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
-
-namespace osu.Game.Overlays.Mods
-{
- ///
- /// Represents a clickable button which can cycle through one of more mods.
- ///
- public class ModButton : ModButtonEmpty, IHasCustomTooltip
- {
- private ModIcon foregroundIcon;
- private ModIcon backgroundIcon;
- private readonly SpriteText text;
- private readonly Container iconsContainer;
-
- ///
- /// Fired when the selection changes.
- ///
- public Action SelectionChanged;
-
- public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
-
- private const Easing mod_switch_easing = Easing.InOutSine;
- private const double mod_switch_duration = 120;
-
- // A selected index of -1 means not selected.
- private int selectedIndex = -1;
-
- ///
- /// Change the selected mod index of this button.
- ///
- /// The new index.
- /// Whether any settings applied to the mod should be reset on selection.
- /// Whether the selection changed.
- private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
- {
- if (newIndex == selectedIndex) return false;
-
- int direction = newIndex < selectedIndex ? -1 : 1;
-
- bool beforeSelected = Selected;
-
- Mod previousSelection = SelectedMod ?? Mods[0];
-
- if (newIndex >= Mods.Length)
- newIndex = -1;
- else if (newIndex < -1)
- newIndex = Mods.Length - 1;
-
- if (newIndex >= 0 && !Mods[newIndex].HasImplementation)
- return false;
-
- selectedIndex = newIndex;
-
- Mod newSelection = SelectedMod ?? Mods[0];
-
- if (resetSettings)
- newSelection.ResetSettingsToDefaults();
-
- Schedule(() =>
- {
- if (beforeSelected != Selected)
- {
- iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic);
- iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic);
- }
-
- if (previousSelection != newSelection)
- {
- const float rotate_angle = 16;
-
- foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing);
- backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing);
-
- backgroundIcon.Mod = newSelection;
-
- using (BeginDelayedSequence(mod_switch_duration))
- {
- foregroundIcon
- .RotateTo(-rotate_angle * direction)
- .RotateTo(0f, mod_switch_duration, mod_switch_easing);
-
- backgroundIcon
- .RotateTo(rotate_angle * direction)
- .RotateTo(0f, mod_switch_duration, mod_switch_easing);
-
- Schedule(() => DisplayMod(newSelection));
- }
- }
-
- foregroundIcon.Selected.Value = Selected;
- });
-
- SelectionChanged?.Invoke(SelectedMod);
-
- return true;
- }
-
- public bool Selected => selectedIndex != -1;
-
- private Color4 selectedColour;
-
- public Color4 SelectedColour
- {
- get => selectedColour;
- set
- {
- if (value == selectedColour) return;
-
- selectedColour = value;
- if (Selected) foregroundIcon.Colour = value;
- }
- }
-
- private Mod mod;
-
- protected readonly Container ButtonContent;
-
- public Mod Mod
- {
- get => mod;
- set
- {
- mod = value;
-
- if (mod == null)
- {
- Mods = Array.Empty();
- Alpha = 0;
- }
- else
- {
- Mods = (mod as MultiMod)?.Mods ?? new[] { mod };
- Alpha = 1;
- }
-
- createIcons();
-
- if (Mods.Length > 0)
- {
- DisplayMod(Mods[0]);
- }
- }
- }
-
- public Mod[] Mods { get; private set; }
-
- public virtual Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex);
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- ButtonContent.ScaleTo(0.9f, 800, Easing.Out);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- ButtonContent.ScaleTo(1, 500, Easing.OutElastic);
-
- // only trigger the event if we are inside the area of the button
- if (Contains(e.ScreenSpaceMousePosition))
- {
- switch (e.Button)
- {
- case MouseButton.Right:
- SelectNext(-1);
- break;
- }
- }
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- SelectNext(1);
-
- return true;
- }
-
- ///
- /// Select the next available mod in a specified direction.
- ///
- /// 1 for forwards, -1 for backwards.
- public void SelectNext(int direction)
- {
- int start = selectedIndex + direction;
- // wrap around if we are at an extremity.
- if (start >= Mods.Length)
- start = -1;
- else if (start < -1)
- start = Mods.Length - 1;
-
- for (int i = start; i < Mods.Length && i >= 0; i += direction)
- {
- if (SelectAt(i))
- return;
- }
-
- Deselect();
- }
-
- ///
- /// Select the mod at the provided index.
- ///
- /// The index to select.
- /// Whether any settings applied to the mod should be reset on selection.
- /// Whether the selection changed.
- public bool SelectAt(int index, bool resetSettings = true)
- {
- if (!Mods[index].HasImplementation) return false;
-
- changeSelectedIndex(index, resetSettings);
- return true;
- }
-
- public void Deselect() => changeSelectedIndex(-1);
-
- protected virtual void DisplayMod(Mod mod)
- {
- if (backgroundIcon != null)
- backgroundIcon.Mod = foregroundIcon.Mod;
- foregroundIcon.Mod = mod;
- text.Text = mod.Name;
- Colour = mod.HasImplementation ? Color4.White : Color4.Gray;
- }
-
- private void createIcons()
- {
- iconsContainer.Clear();
-
- if (Mods.Length > 1)
- {
- iconsContainer.AddRange(new[]
- {
- backgroundIcon = new ModIcon(Mods[1], false)
- {
- Origin = Anchor.BottomRight,
- Anchor = Anchor.BottomRight,
- Position = new Vector2(1.5f),
- },
- foregroundIcon = new ModIcon(Mods[0], false)
- {
- Origin = Anchor.BottomRight,
- Anchor = Anchor.BottomRight,
- Position = new Vector2(-1.5f),
- },
- });
- }
- else
- {
- iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false)
- {
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- });
- }
- }
-
- public ModButton(Mod mod)
- {
- Children = new Drawable[]
- {
- new Container
- {
- Size = new Vector2(77f, 80f),
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Children = new Drawable[]
- {
- ButtonContent = new Container
- {
- Children = new Drawable[]
- {
- iconsContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- },
- },
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- }
- }
- },
- text = new OsuSpriteText
- {
- Y = 75,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Font = OsuFont.GetFont(size: 18)
- },
- new HoverSounds()
- };
- Mod = mod;
- }
-
- public virtual ITooltip GetCustomTooltip() => new ModButtonTooltip();
-
- public Mod TooltipContent => SelectedMod ?? Mods.FirstOrDefault();
- }
-}
diff --git a/osu.Game/Overlays/Mods/ModButtonEmpty.cs b/osu.Game/Overlays/Mods/ModButtonEmpty.cs
deleted file mode 100644
index 03afe5adba..0000000000
--- a/osu.Game/Overlays/Mods/ModButtonEmpty.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osuTK;
-using osu.Framework.Graphics.Containers;
-
-namespace osu.Game.Overlays.Mods
-{
- ///
- /// A mod button used exclusively for providing an empty space the size of a mod button.
- ///
- public class ModButtonEmpty : Container
- {
- public ModButtonEmpty()
- {
- Size = new Vector2(100f);
- AlwaysPresent = true;
- }
- }
-}
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 3a2fda0bb0..b32ebb4a5c 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -384,7 +384,7 @@ namespace osu.Game.Overlays.Mods
///
///
/// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state.
- /// uses this to substitute any external mod references in
+ /// uses this to substitute any external mod references in
/// to references that are owned by this column.
///
internal void SetSelection(IReadOnlyList mods)
diff --git a/osu.Game/Overlays/Mods/ModControlSection.cs b/osu.Game/Overlays/Mods/ModControlSection.cs
deleted file mode 100644
index 10b3bc7c2b..0000000000
--- a/osu.Game/Overlays/Mods/ModControlSection.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Mods;
-using osuTK;
-
-namespace osu.Game.Overlays.Mods
-{
- public class ModControlSection : CompositeDrawable
- {
- protected FillFlowContainer FlowContent;
-
- public readonly Mod Mod;
-
- public ModControlSection(Mod mod, IEnumerable modControls)
- {
- Mod = mod;
-
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- FlowContent = new FillFlowContainer
- {
- Margin = new MarginPadding { Top = 30 },
- Spacing = new Vector2(0, 5),
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- ChildrenEnumerable = modControls
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- AddRangeInternal(new Drawable[]
- {
- new OsuSpriteText
- {
- Text = Mod.Name,
- Font = OsuFont.GetFont(weight: FontWeight.Bold),
- Colour = colours.Yellow,
- },
- FlowContent
- });
- }
- }
-}
diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
deleted file mode 100644
index a70191a864..0000000000
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osuTK;
-using osuTK.Input;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Mods;
-using System;
-using System.Linq;
-using System.Collections.Generic;
-using System.Threading;
-using Humanizer;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-
-namespace osu.Game.Overlays.Mods
-{
- public class ModSection : CompositeDrawable
- {
- private readonly Drawable header;
-
- public FillFlowContainer ButtonsContainer { get; }
-
- protected IReadOnlyList Buttons { get; private set; } = Array.Empty();
-
- public Action Action;
-
- public Key[] ToggleKeys;
-
- public readonly ModType ModType;
-
- public IEnumerable SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null);
-
- private CancellationTokenSource modsLoadCts;
-
- protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
-
- ///
- /// True when all mod icons have completed loading.
- ///
- public bool ModIconsLoaded { get; private set; } = true;
-
- public IEnumerable Mods
- {
- set
- {
- var modContainers = value.Select(m =>
- {
- if (m == null)
- return new ModButtonEmpty();
-
- return CreateModButton(m).With(b =>
- {
- b.SelectionChanged = mod =>
- {
- ModButtonStateChanged(mod);
- Action?.Invoke(mod);
- };
- });
- }).ToArray();
-
- modsLoadCts?.Cancel();
-
- if (modContainers.Length == 0)
- {
- ModIconsLoaded = true;
- header.Hide();
- Hide();
- return;
- }
-
- ModIconsLoaded = false;
-
- LoadComponentsAsync(modContainers, c =>
- {
- ModIconsLoaded = true;
- ButtonsContainer.ChildrenEnumerable = c;
- }, (modsLoadCts = new CancellationTokenSource()).Token);
-
- Buttons = modContainers.OfType().ToArray();
-
- header.FadeIn(200);
- this.FadeIn(200);
- }
- }
-
- protected virtual void ModButtonStateChanged(Mod mod)
- {
- }
-
- protected override bool OnKeyDown(KeyDownEvent e)
- {
- if (e.ControlPressed) return false;
-
- if (ToggleKeys != null)
- {
- int index = Array.IndexOf(ToggleKeys, e.Key);
- if (index > -1 && index < Buttons.Count)
- Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1);
- }
-
- return base.OnKeyDown(e);
- }
-
- private const double initial_multiple_selection_delay = 120;
-
- private double selectionDelay = initial_multiple_selection_delay;
- private double lastSelection;
-
- private readonly Queue pendingSelectionOperations = new Queue();
-
- protected override void Update()
- {
- base.Update();
-
- if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
- {
- if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
- {
- dequeuedAction();
-
- // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
- selectionDelay = Math.Max(30, selectionDelay * 0.8f);
- lastSelection = Time.Current;
- }
- else
- {
- // reset the selection delay after all animations have been completed.
- // this will cause the next action to be immediately performed.
- selectionDelay = initial_multiple_selection_delay;
- }
- }
- }
-
- ///
- /// Selects all mods.
- ///
- public void SelectAll()
- {
- pendingSelectionOperations.Clear();
-
- foreach (var button in Buttons.Where(b => !b.Selected))
- pendingSelectionOperations.Enqueue(() => button.SelectAt(0));
- }
-
- ///
- /// Deselects all mods.
- ///
- public void DeselectAll()
- {
- pendingSelectionOperations.Clear();
- DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null));
- }
-
- ///
- /// Deselect one or more mods in this section.
- ///
- /// The types of s which should be deselected.
- /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.
- /// If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in .
- public void DeselectTypes(IEnumerable modTypes, bool immediate = false, Mod newSelection = null)
- {
- foreach (var button in Buttons)
- {
- if (button.SelectedMod == null) continue;
-
- if (button.SelectedMod == newSelection)
- continue;
-
- foreach (var type in modTypes)
- {
- if (type.IsInstanceOfType(button.SelectedMod))
- {
- if (immediate)
- button.Deselect();
- else
- pendingSelectionOperations.Enqueue(button.Deselect);
- }
- }
- }
- }
-
- ///
- /// Updates all buttons with the given list of selected mods.
- ///
- /// The new list of selected mods to select.
- public void UpdateSelectedButtons(IReadOnlyList newSelectedMods)
- {
- foreach (var button in Buttons)
- updateButtonSelection(button, newSelectedMods);
- }
-
- private void updateButtonSelection(ModButton button, IReadOnlyList newSelectedMods)
- {
- foreach (var mod in newSelectedMods)
- {
- int index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType());
- if (index < 0)
- continue;
-
- var buttonMod = button.Mods[index];
-
- // as this is likely coming from an external change, ensure the settings of the mod are in sync.
- buttonMod.CopyFrom(mod);
-
- button.SelectAt(index, false);
- return;
- }
-
- button.Deselect();
- }
-
- public ModSection(ModType type)
- {
- ModType = type;
-
- AutoSizeAxes = Axes.Y;
- RelativeSizeAxes = Axes.X;
-
- Origin = Anchor.TopCentre;
- Anchor = Anchor.TopCentre;
-
- InternalChildren = new[]
- {
- header = CreateHeader(type.Humanize(LetterCasing.Title)),
- ButtonsContainer = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Spacing = new Vector2(50f, 0f),
- Margin = new MarginPadding
- {
- Top = 20,
- },
- AlwaysPresent = true
- },
- };
- }
-
- protected virtual Drawable CreateHeader(string text) => new OsuSpriteText
- {
- Font = OsuFont.GetFont(weight: FontWeight.Bold),
- Text = text
- };
-
- protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
-
- ///
- /// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
- ///
- public void FlushPendingSelections()
- {
- while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
- dequeuedAction();
- }
- }
-}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index cf57322594..b3c3eee15a 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -1,532 +1,646 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Lists;
+using osu.Framework.Utils;
+using osu.Game.Audio;
+using osu.Game.Configuration;
using osu.Game.Graphics;
-using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
-using osu.Game.Resources.Localisation.Web;
+using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
-using osu.Game.Screens;
-using osu.Game.Utils;
using osuTK;
-using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
- public abstract class ModSelectOverlay : WaveOverlayContainer
+ public abstract class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler
{
- public const float HEIGHT = 510;
+ protected const int BUTTON_WIDTH = 200;
- protected readonly TriangleButton DeselectAllButton;
- protected readonly TriangleButton CustomiseButton;
- protected readonly TriangleButton CloseButton;
+ [Cached]
+ public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty());
- protected readonly FillFlowContainer FooterContainer;
-
- protected override bool BlockNonPositionalInput => false;
-
- protected override bool DimMainContent => false;
-
- ///
- /// Whether s underneath the same instance should appear as stacked buttons.
- ///
- protected virtual bool Stacked => true;
-
- ///
- /// Whether configurable s can be configured by the local user.
- ///
- protected virtual bool AllowConfiguration => true;
-
- [NotNull]
private Func isValidMod = m => true;
///
- /// A function that checks whether a given mod is selectable.
+ /// A function determining whether each mod in the column should be displayed.
+ /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed.
+ /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden.
///
- [NotNull]
public Func IsValidMod
{
get => isValidMod;
set
{
isValidMod = value ?? throw new ArgumentNullException(nameof(value));
- updateAvailableMods();
+
+ if (IsLoaded)
+ updateAvailableMods();
}
}
- protected readonly FillFlowContainer ModSectionsContainer;
+ ///
+ /// Whether the total score multiplier calculated from the current selected set of mods should be shown.
+ ///
+ protected virtual bool ShowTotalMultiplier => true;
- protected readonly ModSettingsContainer ModSettingsContainer;
+ protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys);
- [Cached]
- public readonly Bindable> SelectedMods = new Bindable>(Array.Empty());
+ protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection;
- private Bindable>> availableMods;
+ protected virtual IEnumerable CreateFooterButtons() => createDefaultFooterButtons();
- protected Color4 LowMultiplierColour;
- protected Color4 HighMultiplierColour;
+ private readonly BindableBool customisationVisible = new BindableBool();
- private const float content_width = 0.8f;
- private const float footer_button_spacing = 20;
+ private ModSettingsArea modSettingsArea = null!;
+ private ColumnScrollContainer columnScroll = null!;
+ private ColumnFlowContainer columnFlow = null!;
+ private FillFlowContainer footerButtonFlow = null!;
+ private ShearedButton backButton = null!;
- private Sample sampleOn, sampleOff;
+ private DifficultyMultiplierDisplay? multiplierDisplay;
- protected ModSelectOverlay()
+ private ShearedToggleButton? customisationButton;
+
+ protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
+ : base(colourScheme)
{
- Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2");
- Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2");
- Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
- Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e");
+ }
- RelativeSizeAxes = Axes.X;
- Height = HEIGHT;
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Header.Title = ModSelectOverlayStrings.ModSelectTitle;
+ Header.Description = ModSelectOverlayStrings.ModSelectDescription;
- Padding = new MarginPadding { Horizontal = -OsuScreen.HORIZONTAL_OVERFLOW_PADDING };
+ AddRange(new Drawable[]
+ {
+ new ClickToReturnContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ HandleMouse = { BindTarget = customisationVisible },
+ OnClicked = () => customisationVisible.Value = false
+ },
+ modSettingsArea = new ModSettingsArea
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Height = 0
+ }
+ });
- Children = new Drawable[]
+ MainAreaContent.AddRange(new Drawable[]
{
new Container
{
+ Padding = new MarginPadding
+ {
+ Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING,
+ Bottom = PADDING
+ },
RelativeSizeAxes = Axes.Both,
- Masking = true,
+ RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
- new Box
+ columnScroll = new ColumnScrollContainer
{
RelativeSizeAxes = Axes.Both,
- Colour = new Color4(36, 50, 68, 255)
- },
- new Triangles
- {
- TriangleScale = 5,
- RelativeSizeAxes = Axes.Both,
- ColourLight = new Color4(53, 66, 82, 255),
- ColourDark = new Color4(41, 54, 70, 255),
- },
- },
- },
- new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.Absolute, 90),
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize),
- },
- Content = new[]
- {
- new Drawable[]
- {
- new Container
+ Masking = false,
+ ClampExtension = 100,
+ ScrollbarOverlapsContent = false,
+ Child = columnFlow = new ColumnFlowContainer
{
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Children = new Drawable[]
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Direction = FillDirection.Horizontal,
+ Shear = new Vector2(SHEAR, 0),
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Margin = new MarginPadding { Horizontal = 70 },
+ Padding = new MarginPadding { Bottom = 10 },
+ Children = new[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.Gray(10).Opacity(100),
- },
- new FillFlowContainer
- {
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Width = content_width,
- Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Text = @"Gameplay Mods",
- Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold),
- Shadow = true,
- Margin = new MarginPadding
- {
- Bottom = 4,
- },
- },
- new OsuTextFlowContainer(text =>
- {
- text.Font = text.Font.With(size: 18);
- text.Shadow = true;
- })
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Text = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play.\nOthers are just for fun.",
- },
- },
- },
- },
- },
- },
- new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- // Body
- new OsuScrollContainer
- {
- ScrollbarVisible = false,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Vertical = 10,
- Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING
- },
- Children = new Drawable[]
- {
- ModSectionsContainer = new FillFlowContainer
- {
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0f, 10f),
- Width = content_width,
- LayoutDuration = 200,
- LayoutEasing = Easing.OutQuint,
- Children = new[]
- {
- CreateModSection(ModType.DifficultyReduction).With(s =>
- {
- s.ToggleKeys = new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P };
- s.Action = modButtonPressed;
- }),
- CreateModSection(ModType.DifficultyIncrease).With(s =>
- {
- s.ToggleKeys = new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L };
- s.Action = modButtonPressed;
- }),
- CreateModSection(ModType.Automation).With(s =>
- {
- s.ToggleKeys = new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M };
- s.Action = modButtonPressed;
- }),
- CreateModSection(ModType.Conversion).With(s =>
- {
- s.Action = modButtonPressed;
- }),
- CreateModSection(ModType.Fun).With(s =>
- {
- s.Action = modButtonPressed;
- }),
- }
- },
- }
- },
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Padding = new MarginPadding(30),
- Width = 0.3f,
- Children = new Drawable[]
- {
- ModSettingsContainer = new ModSettingsContainer
- {
- Alpha = 0,
- SelectedMods = { BindTarget = SelectedMods },
- },
- }
- },
+ createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }),
+ createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }),
+ createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }),
+ createModColumnContent(ModType.Conversion),
+ createModColumnContent(ModType.Fun)
}
- },
- },
- new Drawable[]
- {
- new Container
- {
- Name = "Footer content",
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = new Color4(172, 20, 116, 255),
- Alpha = 0.5f,
- },
- FooterContainer = new FillFlowContainer
- {
- Origin = Anchor.BottomCentre,
- Anchor = Anchor.BottomCentre,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- RelativePositionAxes = Axes.X,
- Width = content_width,
- Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
- Padding = new MarginPadding
- {
- Vertical = 15,
- Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING
- },
- Children = new[]
- {
- DeselectAllButton = new TriangleButton
- {
- Width = 180,
- Text = "Deselect All",
- Action = deselectAll,
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- },
- CustomiseButton = new TriangleButton
- {
- Width = 180,
- Text = "Customisation",
- Action = () => ModSettingsContainer.ToggleVisibility(),
- Enabled = { Value = false },
- Alpha = AllowConfiguration ? 1 : 0,
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- },
- CloseButton = new TriangleButton
- {
- Width = 180,
- Text = CommonStrings.ButtonsClose,
- Action = Hide,
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- },
- }
- }
- },
}
- },
+ }
+ }
+ }
+ });
+
+ if (ShowTotalMultiplier)
+ {
+ MainAreaContent.Add(new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.X,
+ Height = DifficultyMultiplierDisplay.HEIGHT,
+ Margin = new MarginPadding { Horizontal = 100 },
+ Child = multiplierDisplay = new DifficultyMultiplierDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
},
+ });
+ }
+
+ FooterContent.Child = footerButtonFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Padding = new MarginPadding
+ {
+ Vertical = PADDING,
+ Horizontal = 70
},
+ Spacing = new Vector2(10),
+ ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH)
+ {
+ Text = CommonStrings.Back,
+ Action = Hide,
+ DarkerColour = colours.Pink2,
+ LighterColour = colours.Pink1
+ })
};
-
- ((IBindable)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
- }
-
- [BackgroundDependencyLoader(true)]
- private void load(AudioManager audio, OsuGameBase osu)
- {
- availableMods = osu.AvailableMods.GetBoundCopy();
-
- sampleOn = audio.Samples.Get(@"UI/check-on");
- sampleOff = audio.Samples.Get(@"UI/check-off");
- }
-
- private void deselectAll()
- {
- foreach (var section in ModSectionsContainer.Children)
- section.DeselectAll();
-
- refreshSelectedMods();
}
protected override void LoadComplete()
{
base.LoadComplete();
- availableMods.BindValueChanged(_ => updateAvailableMods(), true);
+ State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true);
- // intentionally bound after the above line to avoid a potential update feedback cycle.
- // i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
- SelectedMods.BindValueChanged(_ => updateSelectedButtons());
+ // This is an optimisation to prevent refreshing the available settings controls when it can be
+ // reasonably assumed that the settings panel is never to be displayed (e.g. FreeModSelectOverlay).
+ if (customisationButton != null)
+ ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
+
+ SelectedMods.BindValueChanged(val =>
+ {
+ updateMultiplier();
+ updateCustomisation(val);
+ updateSelectionFromBindable();
+ }, true);
+
+ foreach (var column in columnFlow.Columns)
+ {
+ column.SelectionChangedByUser += updateBindableFromSelection;
+ }
+
+ customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
+
+ updateAvailableMods();
+
+ // Start scrolled slightly to the right to give the user a sense that
+ // there is more horizontal content available.
+ ScheduleAfterChildren(() =>
+ {
+ columnScroll.ScrollTo(200, false);
+ columnScroll.ScrollToStart();
+ });
+ }
+
+ ///
+ /// Select all visible mods in all columns.
+ ///
+ protected void SelectAll()
+ {
+ foreach (var column in columnFlow.Columns)
+ column.SelectAll();
+ }
+
+ ///
+ /// Deselect all visible mods in all columns.
+ ///
+ protected void DeselectAll()
+ {
+ foreach (var column in columnFlow.Columns)
+ column.DeselectAll();
+ }
+
+ private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null)
+ {
+ var column = CreateModColumn(modType, toggleKeys).With(column =>
+ {
+ column.Filter = IsValidMod;
+ // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden.
+ column.Margin = new MarginPadding { Right = 10 };
+ });
+
+ return new ColumnDimContainer(column)
+ {
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140),
+ };
+ }
+
+ private ShearedButton[] createDefaultFooterButtons()
+ => new[]
+ {
+ customisationButton = new ShearedToggleButton(BUTTON_WIDTH)
+ {
+ Text = ModSelectOverlayStrings.ModCustomisation,
+ Active = { BindTarget = customisationVisible }
+ },
+ new ShearedButton(BUTTON_WIDTH)
+ {
+ Text = CommonStrings.DeselectAll,
+ Action = DeselectAll
+ }
+ };
+
+ private void updateMultiplier()
+ {
+ if (multiplierDisplay == null)
+ return;
+
+ double multiplier = 1.0;
+
+ foreach (var mod in SelectedMods.Value)
+ multiplier *= mod.ScoreMultiplier;
+
+ multiplierDisplay.Current.Value = multiplier;
+ }
+
+ private void updateAvailableMods()
+ {
+ foreach (var column in columnFlow.Columns)
+ column.Filter = m => m.HasImplementation && isValidMod.Invoke(m);
+ }
+
+ private void updateCustomisation(ValueChangedEvent> valueChangedEvent)
+ {
+ if (customisationButton == null)
+ return;
+
+ bool anyCustomisableMod = false;
+ bool anyModWithRequiredCustomisationAdded = false;
+
+ foreach (var mod in SelectedMods.Value)
+ {
+ anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
+ anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
+ }
+
+ if (anyCustomisableMod)
+ {
+ customisationVisible.Disabled = false;
+
+ if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
+ customisationVisible.Value = true;
+ }
+ else
+ {
+ if (customisationVisible.Value)
+ customisationVisible.Value = false;
+
+ customisationVisible.Disabled = true;
+ }
+ }
+
+ private void updateCustomisationVisualState()
+ {
+ const double transition_duration = 300;
+
+ MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic);
+
+ foreach (var button in footerButtonFlow)
+ {
+ if (button != customisationButton)
+ button.Enabled.Value = !customisationVisible.Value;
+ }
+
+ float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0;
+
+ modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
+ TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
+ }
+
+ private void updateSelectionFromBindable()
+ {
+ // `SelectedMods` may contain mod references that come from external sources.
+ // to ensure isolation, first pull in the potentially-external change into the mod columns...
+ foreach (var column in columnFlow.Columns)
+ column.SetSelection(SelectedMods.Value);
+
+ // and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own.
+ updateBindableFromSelection();
+ }
+
+ private void updateBindableFromSelection()
+ {
+ var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray();
+
+ // the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion.
+ // TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6
+ if (candidateSelection.SequenceEqual(SelectedMods.Value, new FuncEqualityComparer(ReferenceEquals)))
+ return;
+
+ SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection);
+ }
+
+ #region Transition handling
+
+ private const float distance = 700;
+
+ protected override void PopIn()
+ {
+ const double fade_in_duration = 400;
+
+ base.PopIn();
+
+ multiplierDisplay?
+ .Delay(fade_in_duration * 0.65f)
+ .FadeIn(fade_in_duration / 2, Easing.OutQuint)
+ .ScaleTo(1, fade_in_duration, Easing.OutElastic);
+
+ int nonFilteredColumnCount = 0;
+
+ for (int i = 0; i < columnFlow.Count; i++)
+ {
+ var column = columnFlow[i].Column;
+
+ double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30;
+ double duration = column.AllFiltered.Value ? 0 : fade_in_duration;
+ float startingYPosition = 0;
+ if (!column.AllFiltered.Value)
+ startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
+
+ column.TopLevelContent
+ .MoveToY(startingYPosition)
+ .Delay(delay)
+ .MoveToY(0, duration, Easing.OutQuint)
+ .FadeIn(duration, Easing.OutQuint);
+
+ if (!column.AllFiltered.Value)
+ nonFilteredColumnCount += 1;
+ }
}
protected override void PopOut()
{
+ const double fade_out_duration = 500;
+
base.PopOut();
- foreach (var section in ModSectionsContainer)
+ multiplierDisplay?
+ .FadeOut(fade_out_duration / 2, Easing.OutQuint)
+ .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
+
+ int nonFilteredColumnCount = 0;
+
+ for (int i = 0; i < columnFlow.Count; i++)
{
- section.FlushPendingSelections();
+ var column = columnFlow[i].Column;
+
+ double duration = column.AllFiltered.Value ? 0 : fade_out_duration;
+ float newYPosition = 0;
+ if (!column.AllFiltered.Value)
+ newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
+
+ column.FlushPendingSelections();
+ column.TopLevelContent
+ .MoveToY(newYPosition, duration, Easing.OutQuint)
+ .FadeOut(duration, Easing.OutQuint);
+
+ if (!column.AllFiltered.Value)
+ nonFilteredColumnCount += 1;
}
-
- FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
- FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-
- foreach (var section in ModSectionsContainer.Children)
- {
- section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
- section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
- section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
- }
- }
-
- protected override void PopIn()
- {
- base.PopIn();
-
- FooterContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
- FooterContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-
- foreach (var section in ModSectionsContainer.Children)
- {
- section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint);
- section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
- section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
- }
- }
-
- protected override bool OnKeyDown(KeyDownEvent e)
- {
- // don't absorb control as ToolbarRulesetSelector uses control + number to navigate
- if (e.ControlPressed) return false;
-
- switch (e.Key)
- {
- case Key.Number1:
- DeselectAllButton.TriggerClick();
- return true;
-
- case Key.Number2:
- CloseButton.TriggerClick();
- return true;
- }
-
- return base.OnKeyDown(e);
- }
-
- public override bool OnPressed(KeyBindingPressEvent e) => false; // handled by back button
-
- private void updateAvailableMods()
- {
- if (availableMods?.Value == null)
- return;
-
- foreach (var section in ModSectionsContainer.Children)
- {
- IEnumerable modEnumeration = availableMods.Value[section.ModType];
-
- if (!Stacked)
- modEnumeration = ModUtils.FlattenMods(modEnumeration);
-
- section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.DeepClone());
- }
-
- updateSelectedButtons();
- OnAvailableModsChanged();
- }
-
- ///
- /// Returns a valid form of a given if possible, or null otherwise.
- ///
- ///
- /// This is a recursive process during which any invalid mods are culled while preserving structures where possible.
- ///
- /// The to check.
- /// A valid form of if exists, or null otherwise.
- [CanBeNull]
- private Mod getValidModOrNull([NotNull] Mod mod)
- {
- if (!(mod is MultiMod multi))
- return IsValidMod(mod) ? mod : null;
-
- var validSubset = multi.Mods.Select(getValidModOrNull).Where(m => m != null).ToArray();
-
- if (validSubset.Length == 0)
- return null;
-
- return validSubset.Length == 1 ? validSubset[0] : new MultiMod(validSubset);
- }
-
- private void updateSelectedButtons()
- {
- // Enumeration below may update the bindable list.
- var selectedMods = SelectedMods.Value.ToList();
-
- foreach (var section in ModSectionsContainer.Children)
- section.UpdateSelectedButtons(selectedMods);
- }
-
- private void modButtonPressed(Mod selectedMod)
- {
- if (selectedMod != null)
- {
- if (State.Value == Visibility.Visible)
- Scheduler.AddOnce(playSelectedSound);
-
- OnModSelected(selectedMod);
-
- if (selectedMod.RequiresConfiguration && AllowConfiguration)
- ModSettingsContainer.Show();
- }
- else
- {
- if (State.Value == Visibility.Visible)
- Scheduler.AddOnce(playDeselectedSound);
- }
-
- refreshSelectedMods();
- }
-
- private void playSelectedSound() => sampleOn?.Play();
- private void playDeselectedSound() => sampleOff?.Play();
-
- ///
- /// Invoked after has changed.
- ///
- protected virtual void OnAvailableModsChanged()
- {
- }
-
- ///
- /// Invoked when a new has been selected.
- ///
- /// The that has been selected.
- protected virtual void OnModSelected(Mod mod)
- {
- }
-
- private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
-
- ///
- /// Creates a that groups s with the same .
- ///
- /// The of s in the section.
- /// The .
- protected virtual ModSection CreateModSection(ModType type) => new ModSection(type);
-
- #region Disposal
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- availableMods?.UnbindAll();
- SelectedMods?.UnbindAll();
}
#endregion
+
+ #region Input handling
+
+ public override bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Repeat)
+ return false;
+
+ switch (e.Action)
+ {
+ case GlobalAction.Back:
+ // Pressing the back binding should only go back one step at a time.
+ hideOverlay(false);
+ return true;
+
+ // This is handled locally here because this overlay is being registered at the game level
+ // and therefore takes away keyboard focus from the screen stack.
+ case GlobalAction.ToggleModSelection:
+ case GlobalAction.Select:
+ {
+ // Pressing toggle or select should completely hide the overlay in one shot.
+ hideOverlay(true);
+ return true;
+ }
+ }
+
+ return base.OnPressed(e);
+
+ void hideOverlay(bool immediate)
+ {
+ if (customisationVisible.Value)
+ {
+ Debug.Assert(customisationButton != null);
+ customisationButton.TriggerClick();
+
+ if (!immediate)
+ return;
+ }
+
+ backButton.TriggerClick();
+ }
+ }
+
+ #endregion
+
+ #region Sample playback control
+
+ private readonly Bindable samplePlaybackDisabled = new BindableBool(true);
+ IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
+
+ #endregion
+
+ ///
+ /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
+ ///
+ internal class ColumnScrollContainer : OsuScrollContainer
+ {
+ public ColumnScrollContainer()
+ : base(Direction.Horizontal)
+ {
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
+ // note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
+ float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent);
+ float rightVisibleBound = leftVisibleBound + DrawWidth;
+
+ // if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
+ // this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
+ float leftMovementBound = Math.Min(Current, Target);
+ float rightMovementBound = Math.Max(Current, Target) + DrawWidth;
+
+ foreach (var column in Child)
+ {
+ // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
+ // so we have to manually compensate.
+ var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent);
+ var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent);
+
+ bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound)
+ && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X);
+ bool isBeingScrolledToward = Precision.AlmostBigger(topLeft.X, leftMovementBound)
+ && Precision.DefinitelyBigger(rightMovementBound, bottomRight.X);
+
+ column.Active.Value = isCurrentlyVisible || isBeingScrolledToward;
+ }
+ }
+ }
+
+ ///
+ /// Manages layout of mod columns.
+ ///
+ internal class ColumnFlowContainer : FillFlowContainer
+ {
+ public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column);
+
+ public override void Add(ColumnDimContainer dimContainer)
+ {
+ base.Add(dimContainer);
+
+ Debug.Assert(dimContainer != null);
+ dimContainer.Column.Shear = Vector2.Zero;
+ }
+ }
+
+ ///
+ /// Encapsulates a column and provides dim and input blocking based on an externally managed "active" state.
+ ///
+ internal class ColumnDimContainer : Container
+ {
+ public ModColumn Column { get; }
+
+ ///
+ /// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen.
+ ///
+ public readonly Bindable Active = new BindableBool();
+
+ ///
+ /// Invoked when the column is clicked while not active, requesting a scroll to be performed to bring it on-screen.
+ ///
+ public Action? RequestScroll { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ColumnDimContainer(ModColumn column)
+ {
+ Child = Column = column;
+ column.Active.BindTo(Active);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Active.BindValueChanged(_ => updateState());
+ Column.AllFiltered.BindValueChanged(_ => updateState(), true);
+ FinishTransforms();
+ }
+
+ protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning;
+
+ private void updateState()
+ {
+ Colour4 targetColour;
+
+ Column.Alpha = Column.AllFiltered.Value ? 0 : 1;
+
+ if (Column.Active.Value)
+ targetColour = Colour4.White;
+ else
+ targetColour = IsHovered ? colours.GrayC : colours.Gray8;
+
+ this.FadeColour(targetColour, 800, Easing.OutQuint);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (!Active.Value)
+ RequestScroll?.Invoke(this);
+
+ return true;
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ base.OnHover(e);
+ updateState();
+ return Active.Value;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ updateState();
+ }
+ }
+
+ ///
+ /// A container which blocks and handles input, managing the "return from customisation" state change.
+ ///
+ private class ClickToReturnContainer : Container
+ {
+ public BindableBool HandleMouse { get; } = new BindableBool();
+
+ public Action? OnClicked { get; set; }
+
+ public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value;
+
+ protected override bool Handle(UIEvent e)
+ {
+ if (!HandleMouse.Value)
+ return base.Handle(e);
+
+ switch (e)
+ {
+ case ClickEvent _:
+ OnClicked?.Invoke();
+ return true;
+
+ case MouseEvent _:
+ return true;
+ }
+
+ return base.Handle(e);
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs
deleted file mode 100644
index 8b19e38954..0000000000
--- a/osu.Game/Overlays/Mods/ModSelectScreen.cs
+++ /dev/null
@@ -1,664 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable enable
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Input.Events;
-using osu.Framework.Layout;
-using osu.Framework.Lists;
-using osu.Framework.Utils;
-using osu.Game.Audio;
-using osu.Game.Configuration;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Input.Bindings;
-using osu.Game.Localisation;
-using osu.Game.Rulesets.Mods;
-using osuTK;
-using osuTK.Input;
-
-namespace osu.Game.Overlays.Mods
-{
- public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler
- {
- protected const int BUTTON_WIDTH = 200;
-
- [Cached]
- public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty());
-
- private Func isValidMod = m => true;
-
- ///
- /// A function determining whether each mod in the column should be displayed.
- /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed.
- /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden.
- ///
- public Func IsValidMod
- {
- get => isValidMod;
- set
- {
- isValidMod = value ?? throw new ArgumentNullException(nameof(value));
-
- if (IsLoaded)
- updateAvailableMods();
- }
- }
-
- ///
- /// Whether the total score multiplier calculated from the current selected set of mods should be shown.
- ///
- protected virtual bool ShowTotalMultiplier => true;
-
- protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys);
-
- protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList