diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6ba6ae82c8..dd53eefd23 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -13,6 +13,24 @@ "commands": [ "dotnet-format" ] + }, + "jetbrains.resharper.globaltools": { + "version": "2020.2.4", + "commands": [ + "jb" + ] + }, + "nvika": { + "version": "2.0.0", + "commands": [ + "nvika" + ] + }, + "codefilesanity": { + "version": "15.0.0", + "commands": [ + "CodeFileSanity" + ] } } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index aa77d4f055..a70e5ac3a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -40,7 +38,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:GenerateFullPaths=true", "-m", @@ -55,7 +52,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -71,7 +67,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:GenerateFullPaths=true", "-m", @@ -86,7 +81,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -102,7 +96,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Benchmarks", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -111,16 +104,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore (netcoreapp3.1)", - "type": "shell", - "command": "dotnet", - "args": [ - "restore", - "build/Desktop.proj" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/README.md b/README.md index 86c42dae12..c9443ba063 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ git pull Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). - Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. -- Visual Studio Code users must run the `Restore` task before any build attempt. You can also build and run *osu!* from the command-line with a single command: diff --git a/build/InspectCode.cake b/build/InspectCode.cake index c8f4f37c94..6836d9071b 100644 --- a/build/InspectCode.cake +++ b/build/InspectCode.cake @@ -1,7 +1,4 @@ #addin "nuget:?package=CodeFileSanity&version=0.0.36" -#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2020.1.3" -#tool "nuget:?package=NVika.MSBuild&version=1.0.1" -var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First(); /////////////////////////////////////////////////////////////////////////////// // ARGUMENTS @@ -18,23 +15,15 @@ var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf"); // TASKS /////////////////////////////////////////////////////////////////////////////// -// windows only because both inspectcode and nvika depend on net45 Task("InspectCode") - .WithCriteria(IsRunningOnWindows()) .Does(() => { - InspectCode(desktopSlnf, new InspectCodeSettings { - CachesHome = "inspectcode", - OutputFile = "inspectcodereport.xml", - ArgumentCustomization = arg => { - if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output - arg.Append("--verbosity:WARN"); - return arg; - }, - }); + var inspectcodereport = "inspectcodereport.xml"; + var cacheDir = "inspectcode"; + var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output - int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors"); - if (returnCode != 0) - throw new Exception($"inspectcode failed with return code {returnCode}"); + DotNetCoreTool(rootDirectory.FullPath, + "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}"); + DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors"); }); Task("CodeFileSanity") diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 2c915a31b7..d8feacc8a7 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index 04e6dea376..eae07daa3d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -12,17 +12,32 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class CatchLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })] + private static readonly object[][] catch_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(CatchModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(CatchModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(CatchModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(CatchModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + }; + + [TestCaseSource(nameof(catch_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModFlashlight), typeof(CatchModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + + [TestCaseSource(nameof(catch_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new CatchRuleset(); } diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index ca03924c70..323110b605 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index b22687a0a7..a28c188051 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -12,19 +12,44 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class ManiaLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(ManiaModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })] + private static readonly object[][] mania_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(ManiaModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(ManiaModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(ManiaModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(ManiaModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } }, + new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } }, + new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } }, + new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } }, + new object[] { LegacyMods.Key7, new[] { typeof(ManiaModKey7) } }, + new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } }, + new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } }, + new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } }, + new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } }, + new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } }, + new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } }, + new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, + new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, + new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + }; + + [TestCaseSource(nameof(mania_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModFlashlight), typeof(ManiaModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + + [TestCaseSource(nameof(mania_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b92e042686..906f7382c5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.Key9)) yield return new ManiaModKey9(); + if (mods.HasFlag(LegacyMods.KeyCoop)) + yield return new ManiaModDualStages(); + if (mods.HasFlag(LegacyMods.NoFail)) yield return new ManiaModNoFail(); @@ -173,13 +176,22 @@ namespace osu.Game.Rulesets.Mania value |= LegacyMods.Key9; break; + case ManiaModDualStages _: + value |= LegacyMods.KeyCoop; + break; + case ManiaModFadeIn _: value |= LegacyMods.FadeIn; + value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that. break; case ManiaModMirror _: value |= LegacyMods.Mirror; break; + + case ManiaModRandom _: + value |= LegacyMods.Random; + break; } } diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index 14ffbfb4ae..590bedb8b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index d8064d36ea..7b909d2907 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Mod = new OsuModSpunOut(), Autoplay = false, Beatmap = singleSpinnerBeatmap, - PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1 + PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1 }); [TestCase(null)] @@ -45,7 +45,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Mods = mods, Autoplay = false, Beatmap = singleSpinnerBeatmap, - PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1) + PassCondition = () => + { + var counter = Player.ChildrenOfType().SingleOrDefault(); + return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1); + } }); } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 495f2738b5..51da5b85cd 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -12,18 +12,36 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class OsuLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(OsuModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })] + private static readonly object[][] osu_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(OsuModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(OsuModEasy) } }, + new object[] { LegacyMods.TouchDevice, new[] { typeof(OsuModTouchDevice) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(OsuModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(OsuModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } }, + new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, + new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } }, + new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + }; + + [TestCaseSource(nameof(osu_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModFlashlight), typeof(OsuModFlashlight) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })] - [TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + + [TestCaseSource(nameof(osu_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new OsuRuleset(); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index fb1ebbb0d0..084af7dafe 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -1,20 +1,30 @@ // 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderApplication : OsuTestScene { + [Resolved] + private SkinManager skinManager { get; set; } + [Test] public void TestApplyNewSlider() { @@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests }), null)); } + [Test] + public void TestBallTintChangedOnAccentChange() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info); + tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; + + Child = new SkinProvidingContainer(tintingSkin) + { + RelativeSizeAxes = Axes.Both, + Child = dho = new DrawableSlider(prepareObject(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })) + }; + }); + + AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); + AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White); + + AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); + AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red); + } + private Slider prepareObject(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 3d100e4b1c..b71400b71d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } - private DrawableSlider slider; + private DrawableSlider drawableSlider; [SetUpSteps] public override void SetUpSteps() @@ -68,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveDrawableSlider(sliderIndex); + addSeekStep(startTime); + retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); setSnaking(true); ensureSnakingIn(startTime + fade_in_modifier); @@ -93,7 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveDrawableSlider(sliderIndex); + addSeekStep(startTime); + retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); setSnaking(false); ensureNoSnakingIn(startTime + fade_in_modifier); @@ -127,9 +129,8 @@ namespace osu.Game.Rulesets.Osu.Tests checkPositionChange(16600, sliderRepeat, positionDecreased); } - private void retrieveDrawableSlider(int index) => - AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => - slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index)); + private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () => + (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); @@ -150,13 +151,13 @@ namespace osu.Game.Rulesets.Osu.Tests private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex; private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd; - private List sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve; + private List sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; private Vector2 sliderStart() => sliderCurve.First(); private Vector2 sliderEnd() => sliderCurve.Last(); private Vector2 sliderRepeat() { - var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1); + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]); var repeat = drawable.ChildrenOfType>().First().Children.First(); return repeat.Position; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 0558dad30d..d7fbc7ac48 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneSpinnerApplication : OsuTestScene { [Test] - public void TestApplyNewCircle() + public void TestApplyNewSpinner() { DrawableSpinner dho = null; @@ -23,18 +23,23 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, - Duration = 0, + Duration = 500, })) { Clock = new FramedClock(new StopwatchClock()) }); + AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); + AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); + AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { Position = new Vector2(256, 192), ComboIndex = 1, Duration = 1000, }), null)); + + AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 53bf1ea566..ac8d5c81bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); } [Test] @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalSpinnerSymbolRotation = spinnerSymbol.Rotation; spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); addSeekStep(2500); AddAssert("disc rotation rewound", @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); + () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); addSeekStep(5000); AddAssert("is disc rotation almost same", @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is symbol rotation almost same", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); } [Test] @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs new file mode 100644 index 0000000000..776aacd143 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs @@ -0,0 +1,63 @@ +// 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 osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class DrawableOsuEditPool : DrawableOsuPool + where T : DrawableHitObject, new() + { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + private const double editor_hit_object_fade_out_extension = 700; + + public DrawableOsuEditPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + : base(checkHittable, onLoaded, initialSize, maximumSize) + { + } + + protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState); + + private void updateState(DrawableHitObject hitObject, ArmedState state) + { + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) + { + default: + // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) + return; + + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 746ff4ac19..547dff88b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -2,13 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -17,62 +13,21 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditRuleset : DrawableOsuRuleset { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { } - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) - => base.CreateDrawableRepresentation(h)?.With(d => d.ApplyCustomUpdateState += updateState); - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle) - return; - - // adjust the visuals of certain object types to make them stay on screen for longer than usual. - switch (hitObject) - { - default: - // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) - return; - - case DrawableSlider _: - // no specifics to sliders but let them fade slower below. - break; - - case DrawableHitCircle circle: // also handles slider heads - circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) - .Expire(); - break; - } - - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); - } - - protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); + protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One }; - private class OsuPlayfieldNoCursor : OsuPlayfield + private class OsuEditPlayfield : OsuPlayfield { protected override GameplayCursorContainer CreateCursor() => null; + + protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) + => new DrawableOsuEditPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 24bf79f9ae..ec8c68005f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -207,11 +207,17 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = getSurroundingQuad(hitObjects); - if (quad.TopLeft.X + delta.X < 0 || - quad.TopLeft.Y + delta.Y < 0 || - quad.BottomRight.X + delta.X > DrawWidth || - quad.BottomRight.Y + delta.Y > DrawHeight) - return false; + Vector2 newTopLeft = quad.TopLeft + delta; + if (newTopLeft.X < 0) + delta.X -= newTopLeft.X; + if (newTopLeft.Y < 0) + delta.Y -= newTopLeft.Y; + + Vector2 newBottomRight = quad.BottomRight + delta; + if (newBottomRight.X > DrawWidth) + delta.X -= newBottomRight.X - DrawWidth; + if (newBottomRight.Y > DrawHeight) + delta.Y -= newBottomRight.Y - DrawHeight; foreach (var h in hitObjects) h.Position += delta; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs new file mode 100644 index 0000000000..e58aacd86e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuSpinnerJudgementResult : OsuJudgementResult + { + /// + /// The . + /// + public Spinner Spinner => (Spinner)HitObject; + + /// + /// The total rotation performed on the spinner disc, disregarding the spin direction, + /// adjusted for the track's playback rate. + /// + /// + /// + /// This value is always non-negative and is monotonically increasing with time + /// (i.e. will only increase if time is passing forward, but can decrease during rewind). + /// + /// + /// The rotation from each frame is multiplied by the clock's current playback rate. + /// The reason this is done is to ensure that spinners give the same score and require the same number of spins + /// regardless of whether speed-modifying mods are applied. + /// + /// + /// + /// Assuming no speed-modifying mods are active, + /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, + /// this property will return the value of 720 (as opposed to 0). + /// If Double Time is active instead (with a speed multiplier of 1.5x), + /// in the same scenario the property will return 720 * 1.5 = 1080. + /// + public float RateAdjustedRotation; + + /// + /// Time instant at which the spinner has been completed (the user has executed all required spins). + /// Will be null if all required spins haven't been completed. + /// + public double? TimeCompleted; + + public OsuSpinnerJudgementResult(HitObject hitObject, Judgement judgement) + : base(hitObject, judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 84a335750a..7c1dd46c02 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -28,10 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var d in drawables) - { d.HitObjectApplied += applyFadeInAdjustment; - applyFadeInAdjustment(d); - } base.ApplyToDrawableHitObjects(drawables); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 77d24db084..2e63160d36 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container scaleContainer; private InputManager inputManager; + public DrawableHitCircle() + : this(null) + { + } + public DrawableHitCircle([CanBeNull] HitCircle h = null) : base(h) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index d17bf93fa0..bcaf73d34f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private OsuInputManager osuActionInputManager; internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager; - protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); + public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); protected override void UpdateInitialTransforms() { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs new file mode 100644 index 0000000000..1b5fd50022 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs @@ -0,0 +1,32 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableOsuPool : DrawablePool + where T : DrawableHitObject, new() + { + private readonly Func checkHittable; + private readonly Action onLoaded; + + public DrawableOsuPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + this.checkHittable = checkHittable; + this.onLoaded = onLoaded; + } + + protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o => + { + var osuObject = (DrawableOsuHitObject)(object)o; + + osuObject.CheckHittable = checkHittable; + osuObject.OnLoadComplete += onLoaded; + }); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3f91a31066..f7b1894058 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -40,6 +40,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tailContainer; private Container tickContainer; private Container repeatContainer; + private Container samplesContainer; + + public DrawableSlider() + : this(null) + { + } public DrawableSlider([CanBeNull] Slider s = null) : base(s) @@ -63,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, + samplesContainer = new Container { RelativeSizeAxes = Axes.Both } }; PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -73,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; + updateBallTint(); }, true); Tracking.BindValueChanged(updateSlidingSample); @@ -100,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - slidingSample?.Expire(); + samplesContainer.Clear(); slidingSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); @@ -110,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - AddInternal(slidingSample = new PausableSkinnableSound(clone) + samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) { Looping = true }); @@ -159,10 +167,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ClearNestedHitObjects(); - headContainer.Clear(); - tailContainer.Clear(); - repeatContainer.Clear(); - tickContainer.Clear(); + headContainer.Clear(false); + tailContainer.Clear(false); + repeatContainer.Clear(false); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -173,17 +181,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return new DrawableSliderTail(tail); case SliderHeadCircle head: - return new DrawableSliderHead(HitObject, head) - { - OnShake = Shake, - CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true - }; + return new DrawableSliderHead(head); case SliderTick tick: - return new DrawableSliderTick(tick) { Position = tick.Position - HitObject.Position }; + return new DrawableSliderTick(tick); case SliderRepeat repeat: - return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - HitObject.Position }; + return new DrawableSliderRepeat(repeat); } return base.CreateNestedHitObject(hitObject); @@ -241,7 +245,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ApplySkin(skin, allowFallback); - bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; + updateBallTint(); + } + + private void updateBallTint() + { + if (CurrentSkin == null) + return; + + bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 49ed9f12e3..fd0f35d20d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,6 +4,8 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -14,21 +16,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; - private readonly Slider slider; + private DrawableSlider drawableSlider; - public DrawableSliderHead(Slider slider, SliderHeadCircle h) + private Slider slider => drawableSlider?.HitObject; + + public DrawableSliderHead() + { + } + + public DrawableSliderHead(SliderHeadCircle h) : base(h) { - this.slider = slider; } [BackgroundDependencyLoader] private void load() { - pathVersion.BindTo(slider.Path.Version); - PositionBindable.BindValueChanged(_ => updatePosition()); - pathVersion.BindValueChanged(_ => updatePosition(), true); + pathVersion.BindValueChanged(_ => updatePosition()); + } + + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + pathVersion.UnbindFrom(drawableSlider.PathVersion); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + + pathVersion.BindTo(drawableSlider.PathVersion); + + OnShake = drawableSlider.Shake; + CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true; } protected override void Update() @@ -44,8 +68,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Action OnShake; - protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); + public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); - private void updatePosition() => Position = HitObject.Position - slider.Position; + private void updatePosition() + { + if (slider != null) + Position = HitObject.Position - slider.Position; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9c382bd0a7..0735d48ae1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly SliderRepeat sliderRepeat; - private readonly DrawableSlider drawableSlider; + public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; private double animDuration; @@ -27,11 +26,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) + private DrawableSlider drawableSlider; + + public DrawableSliderRepeat() + : base(null) + { + } + + public DrawableSliderRepeat(SliderRepeat sliderRepeat) : base(sliderRepeat) { - this.sliderRepeat = sliderRepeat; - this.drawableSlider = drawableSlider; } [BackgroundDependencyLoader] @@ -53,18 +57,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + + Position = HitObject.Position - drawableSlider.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (sliderRepeat.StartTime <= Time.Current) + if (HitObject.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, sliderRepeat.SpanDuration); + animDuration = Math.Min(300, HitObject.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -100,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // When the repeat is hit, the arrow should fade out on spot rather than following the slider if (IsHit) return; - bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; + bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 3be5983c57..eff72168ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { - private readonly SliderTailCircle tailCircle; + public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; /// /// The judgement text is provided by the . @@ -25,10 +25,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableDrawable circlePiece; private Container scaleContainer; + public DrawableSliderTail() + : base(null) + { + } + public DrawableSliderTail(SliderTailCircle tailCircle) : base(tailCircle) { - this.tailCircle = tailCircle; } [BackgroundDependencyLoader] @@ -52,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } protected override void UpdateInitialTransforms() @@ -92,6 +96,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } public void UpdateSnakingPosition(Vector2 start, Vector2 end) => - Position = tailCircle.RepeatIndex % 2 == 0 ? end : start; + Position = HitObject.RepeatIndex % 2 == 0 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 2af51ea486..faccf5d4d1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableDrawable scaleContainer; + public DrawableSliderTick() + : base(null) + { + } + public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { @@ -54,7 +59,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index eb125969b0..87c7146a64 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -10,8 +10,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; @@ -24,15 +26,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new Spinner HitObject => (Spinner)base.HitObject; + public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; + public SpinnerRotationTracker RotationTracker { get; private set; } public SpinnerSpmCounter SpmCounter { get; private set; } private Container ticks; private SpinnerBonusDisplay bonusDisplay; + private Container samplesContainer; private Bindable isSpinning; private bool spinnerFrequencyModulate; + public DrawableSpinner() + : this(null) + { + } + public DrawableSpinner([CanBeNull] Spinner s = null) : base(s) { @@ -70,7 +80,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, Y = -120, - } + }, + samplesContainer = new Container { RelativeSizeAxes = Axes.Both } }; PositionBindable.BindValueChanged(pos => Position = pos.NewValue); @@ -92,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - spinningSample?.Expire(); + samplesContainer.Clear(); spinningSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); @@ -102,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - AddInternal(spinningSample = new PausableSkinnableSound(clone) + samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) { Volume = { Value = 0 }, Looping = true, @@ -155,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -190,15 +201,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); } } + protected override JudgementResult CreateResult(Judgement judgement) => new OsuSpinnerJudgementResult(HitObject, judgement); + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; - RotationTracker.Complete.Value = Progress >= 1; + if (Progress >= 1) + Result.TimeCompleted ??= Time.Current; if (userTriggered || Time.Current < HitObject.EndTime) return; @@ -237,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation); + SpmCounter.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } @@ -249,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(RotationTracker.RateAdjustedRotation / 360); + int spins = (int)(Result.RateAdjustedRotation / 360); if (spins < wholeSpins) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs index 2e1c07c4c6..ffeb14b0a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs @@ -5,6 +5,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerBonusTick : DrawableSpinnerTick { + public DrawableSpinnerBonusTick() + : base(null) + { + } + public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick) : base(spinnerTick) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index e9cede1398..fc9a7c00e6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -7,6 +7,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; + public DrawableSpinnerTick() + : base(null) + { + } + public DrawableSpinnerTick(SpinnerTick spinnerTick) : base(spinnerTick) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 731852c221..14ce3b014d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private SpinnerTicks ticks; private int wholeRotationCount; + private readonly BindableBool complete = new BindableBool(); private SpinnerFill fill; private Container mainContainer; @@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.LoadComplete(); - drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); + complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); @@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); - if (drawableSpinner.RotationTracker.Complete.Value) + complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted; + + if (complete.Value) { if (checkNewRotationCount) { @@ -194,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { get { - int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 910899c307..f82003edb8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Play; using osuTK; @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public SpinnerRotationTracker(DrawableSpinner drawableSpinner) { this.drawableSpinner = drawableSpinner; + drawableSpinner.HitObjectApplied += resetState; RelativeSizeAxes = Axes.Both; } @@ -30,32 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public bool Tracking { get; set; } - public readonly BindableBool Complete = new BindableBool(); - - /// - /// The total rotation performed on the spinner disc, disregarding the spin direction, - /// adjusted for the track's playback rate. - /// - /// - /// - /// This value is always non-negative and is monotonically increasing with time - /// (i.e. will only increase if time is passing forward, but can decrease during rewind). - /// - /// - /// The rotation from each frame is multiplied by the clock's current playback rate. - /// The reason this is done is to ensure that spinners give the same score and require the same number of spins - /// regardless of whether speed-modifying mods are applied. - /// - /// - /// - /// Assuming no speed-modifying mods are active, - /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, - /// this property will return the value of 720 (as opposed to 0 for ). - /// If Double Time is active instead (with a speed multiplier of 1.5x), - /// in the same scenario the property will return 720 * 1.5 = 1080. - /// - public float RateAdjustedRotation { get; private set; } - /// /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. /// @@ -131,7 +107,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); + drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); + } + + private void resetState(DrawableHitObject obj) + { + Tracking = false; + IsSpinning.Value = false; + mousePosition = default; + lastAngle = currentRotation = Rotation = 0; + rotationTransferred = false; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.HitObjectApplied -= resetState; } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 678fb8aba6..d8180b0e58 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -107,6 +107,35 @@ namespace osu.Game.Rulesets.Osu yield return new OsuModTouchDevice(); } + public override LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = base.ConvertToLegacyMods(mods); + + foreach (var mod in mods) + { + switch (mod) + { + case OsuModAutopilot _: + value |= LegacyMods.Autopilot; + break; + + case OsuModSpunOut _: + value |= LegacyMods.SpunOut; + break; + + case OsuModTarget _: + value |= LegacyMods.Target; + break; + + case OsuModTouchDevice _: + value |= LegacyMods.TouchDevice; + break; + } + } + + return value; + } + public override IEnumerable GetModsFor(ModType type) { switch (type) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index eb9fa85fde..5aa136cf7e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - completed.BindTo(DrawableSpinner.RotationTracker.Complete); completed.BindValueChanged(onCompletedChanged, true); DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; @@ -93,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Skinning } } + protected override void Update() + { + base.Update(); + completed.Value = Time.Current >= DrawableSpinner.Result.TimeCompleted; + } + protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { switch (drawableHitObject) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index b2299398e1..69179137a6 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; @@ -24,11 +23,15 @@ namespace osu.Game.Rulesets.Osu.UI { protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { } + public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor protected override Playfield CreatePlayfield() => new OsuPlayfield(); @@ -39,23 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) - { - switch (h) - { - case HitCircle circle: - return new DrawableHitCircle(circle); - - case Slider slider: - return new DrawableSlider(slider); - - case Spinner spinner: - return new DrawableSpinner(spinner); - } - - return null; - } - protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 321eeeab65..c816502d61 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Scoring; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { + public readonly Func CheckHittable; + private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); + CheckHittable = hitPolicy.IsHittable; var hitWindows = new OsuHitWindows(); @@ -85,45 +89,70 @@ namespace osu.Game.Rulesets.Osu.UI poolDictionary.Add(result, new DrawableJudgementPool(result)); AddRangeInternal(poolDictionary.Values); + + NewResult += onNewResult; } [BackgroundDependencyLoader(true)] private void load(OsuRulesetConfigManager config) { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); + + registerPool(10, 100); + + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(5, 50); + + registerPool(2, 20); + registerPool(10, 100); + registerPool(10, 100); } - public override void Add(DrawableHitObject h) - { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + private void registerPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(CreatePool(initialSize, maximumSize)); - h.OnNewResult += onNewResult; - h.OnLoadComplete += d => + protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) + where TDrawable : DrawableHitObject, new() + => new DrawableOsuPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); + + protected override void OnHitObjectAdded(HitObject hitObject) + { + base.OnHitObjectAdded(hitObject); + followPoints.AddFollowPoints((OsuHitObject)hitObject); + } + + protected override void OnHitObjectRemoved(HitObject hitObject) + { + base.OnHitObjectRemoved(hitObject); + followPoints.RemoveFollowPoints((OsuHitObject)hitObject); + } + + public void OnHitObjectLoaded(Drawable drawable) + { + switch (drawable) { - if (d is DrawableSpinner) - spinnerProxies.Add(d.CreateProxy()); + case DrawableSliderHead _: + case DrawableSliderTail _: + case DrawableSliderTick _: + case DrawableSliderRepeat _: + case DrawableSpinnerTick _: + break; - if (d is IDrawableHitObjectWithProxiedApproach c) - approachCircles.Add(c.ProxiedLayer.CreateProxy()); - }; + case DrawableSpinner _: + spinnerProxies.Add(drawable.CreateProxy()); + break; - base.Add(h); - - osuHitObject.CheckHittable = hitPolicy.IsHittable; - - followPoints.AddFollowPoints(osuHitObject.HitObject); - } - - public override bool Remove(DrawableHitObject h) - { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - - bool result = base.Remove(h); - - if (result) - followPoints.RemoveFollowPoints(osuHitObject.HitObject); - - return result; + case IDrawableHitObjectWithProxiedApproach approach: + approachCircles.Add(approach.ProxiedLayer.CreateProxy()); + break; + } } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) @@ -166,5 +195,15 @@ namespace osu.Game.Rulesets.Osu.UI return judgement; } } + + private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public OsuHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 09340f6f9f..63f25c2402 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index a59544386b..a039e84106 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -12,17 +12,33 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestFixture] public class TaikoLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(TaikoModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })] + private static readonly object[][] taiko_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(TaikoModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(TaikoModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(TaikoModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(TaikoModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } }, + new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + }; + + [TestCaseSource(nameof(taiko_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModFlashlight), typeof(TaikoModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime), typeof(TaikoModPerfect) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + + [TestCaseSource(nameof(taiko_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new TaikoRuleset(); } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 73e9c16d07..2a49dd655c 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -92,6 +92,19 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlag(LegacyMods.Relax)) yield return new TaikoModRelax(); + + if (mods.HasFlag(LegacyMods.Random)) + yield return new TaikoModRandom(); + } + + public override LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = base.ConvertToLegacyMods(mods); + + if (mods.OfType().Any()) + value |= LegacyMods.Random; + + return value; } public override IEnumerable GetModsFor(ModType type) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 6b95931b21..64eaafbe75 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.IO.Stores; -using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; @@ -12,7 +11,6 @@ using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Tests.Gameplay { - [HeadlessTest] public class TestSceneHitObjectSamples : HitObjectSampleTest { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index d76905dab8..a3db20ce83 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -167,6 +167,21 @@ namespace osu.Game.Tests.Visual.Components AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0); } + [Test] + public void TestOwnerNotRegistered() + { + PreviewTrack track = null; + + AddStep("get track", () => Add(new TestTrackOwner(track = getTrack(), registerAsOwner: false))); + AddUntilStep("wait for loaded", () => track.IsLoaded); + + AddStep("start track", () => track.Start()); + AddUntilStep("track is running", () => track.IsRunning); + + AddStep("cancel from anyone", () => trackManager.StopAnyPlaying(this)); + AddAssert("track stopped", () => !track.IsRunning); + } + private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null); private TestPreviewTrack getOwnedTrack() @@ -181,10 +196,12 @@ namespace osu.Game.Tests.Visual.Components private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner { private readonly PreviewTrack track; + private readonly bool registerAsOwner; - public TestTrackOwner(PreviewTrack track) + public TestTrackOwner(PreviewTrack track, bool registerAsOwner = true) { this.track = track; + this.registerAsOwner = registerAsOwner; } [BackgroundDependencyLoader] @@ -196,7 +213,8 @@ namespace osu.Game.Tests.Visual.Components protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(this); + if (registerAsOwner) + dependencies.CacheAs(this); return dependencies; } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs new file mode 100644 index 0000000000..9efd299fba --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -0,0 +1,88 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Tests.Beatmaps; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorQuickDelete : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private BlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); + + [Test] + public void TestQuickDeleteRemovesObject() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("move mouse to object", () => + { + var pos = blueprintContainer.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; + InputManager.MoveMouseTo(pos); + }); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestQuickDeleteRemovesSliderControlPoint() + { + Slider slider = new Slider { StartTime = 1000 }; + + PathControlPoint[] points = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50, 0)), + new PathControlPoint(new Vector2(100, 0)) + }; + + AddStep("add slider", () => + { + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + + AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("move mouse to controlpoint", () => + { + var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre; + InputManager.MoveMouseTo(pos); + }); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2); + + // second click should nuke the object completely. + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index b86cb69eb4..7c6a213fe2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -23,11 +23,13 @@ namespace osu.Game.Tests.Visual.Gameplay DrawableSample[] samples = null; ISamplePlaybackDisabler sampleDisabler = null; - AddStep("get variables", () => + AddUntilStep("get variables", () => { sampleDisabler = Player; - slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - samples = slider.ChildrenOfType().ToArray(); + slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault(); + samples = slider?.ChildrenOfType().ToArray(); + + return slider != null; }); AddUntilStep("wait for slider sliding then seek", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 9b31dd045a..88fbf09ef4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -23,17 +23,15 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePlayerLoader : OsuManualInputManagerTestScene + public class TestScenePlayerLoader : ScreenTestScene { private TestPlayerLoader loader; - private TestPlayerLoaderContainer container; private TestPlayer player; private bool epilepsyWarning; @@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SessionStatics sessionStatics { get; set; } + [Cached] + private readonly NotificationOverlay notificationOverlay; + + [Cached] + private readonly VolumeOverlay volumeOverlay; + + private readonly ChangelogOverlay changelogOverlay; + + public TestScenePlayerLoader() + { + AddRange(new Drawable[] + { + notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + volumeOverlay = new VolumeOverlay + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + changelogOverlay = new ChangelogOverlay() + }); + } + + [SetUp] + public void Setup() => Schedule(() => + { + player = null; + audioManager.Volume.SetDefault(); + }); + /// /// Sets the input manager child to a new test player loader container instance. /// /// If the test player should behave like the production one. /// An action to run before player load but after bindable leases are returned. - public void ResetPlayer(bool interactive, Action beforeLoadAction = null) + private void resetPlayer(bool interactive, Action beforeLoadAction = null) { - player = null; - - audioManager.Volume.SetDefault(); - - InputManager.Clear(); - - container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); - beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); @@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); - InputManager.Child = container; + LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); } [Test] public void TestEarlyExitBeforePlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); @@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEarlyExitAfterPlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddUntilStep("wait for non-null player", () => player != null); @@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaMouseMovement() { - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for load ready", () => @@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaFocus() { - OsuFocusedOverlayContainer overlay = null; - - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); }); - AddUntilStep("overlay visible", () => overlay.IsPresent); + AddStep("show focused overlay", () => changelogOverlay.Show()); + AddUntilStep("overlay visible", () => changelogOverlay.IsPresent); - AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready); + AddUntilStep("wait for load ready", () => player?.LoadState == LoadState.Ready); AddRepeatStep("twiddle thumbs", () => { }, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); - AddStep("hide overlay", () => overlay.Hide()); + AddStep("hide overlay", () => changelogOverlay.Hide()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); } @@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay { SlowLoadPlayer slowPlayer = null; - AddStep("load dummy beatmap", () => ResetPlayer(false)); - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); AddStep("load slow dummy beatmap", () => { - InputManager.Child = container = new TestPlayerLoaderContainer( - loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); - + LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); }); @@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay TestMod playerMod1 = null; TestMod playerMod2 = null; - AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); + AddStep("load player", () => { resetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); @@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var testMod = new TestMod(); - AddStep("load player", () => ResetPlayer(true)); + AddStep("load player", () => resetPlayer(true)); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod }); @@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMutedNotificationMuteButton() { - addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); + addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value); } /// @@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false); - AddStep("load player", () => ResetPlayer(false, beforeLoad)); + AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); - AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { - var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last(); + var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); var flowContainer = scrollContainer.Children.OfType>().First(); var notification = flowContainer.First(); @@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarning(bool warning) { AddStep("change epilepsy warning", () => epilepsyWarning = warning); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarningEarlyExit() { AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -287,42 +302,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } - private class TestPlayerLoaderContainer : Container - { - [Cached] - public readonly NotificationOverlay NotificationOverlay; - - [Cached] - public readonly VolumeOverlay VolumeOverlay; - - public TestPlayerLoaderContainer(IScreen screen) - { - RelativeSizeAxes = Axes.Both; - - OsuScreenStack stack; - - InternalChildren = new Drawable[] - { - stack = new OsuScreenStack - { - RelativeSizeAxes = Axes.Both, - }, - NotificationOverlay = new NotificationOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - VolumeOverlay = new VolumeOverlay - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - } - }; - - stack.Push(screen); - } - } - private class TestPlayerLoader : PlayerLoader { public new VisualSettings VisualSettings => base.VisualSettings; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 242eaf7b7d..3e777119c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -57,6 +57,43 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); } + [Test] + public void TestCustomTransformsClearedBetweenReuses() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = 2000 } + } + }, 1, () => new FramedClock(clock = new ManualClock())); + + DrawableTestHitObject firstObject = null; + Vector2 position = default; + + AddUntilStep("first object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]); + AddStep("get DHO", () => firstObject = this.ChildrenOfType().Single()); + AddStep("store position", () => position = firstObject.Position); + AddStep("add custom transform", () => firstObject.ApplyCustomUpdateState += onStateUpdate); + + AddStep("fast forward past first object", () => clock.CurrentTime = 1500); + AddStep("unapply custom transform", () => firstObject.ApplyCustomUpdateState -= onStateUpdate); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + AddUntilStep("second object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]); + AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); + AddAssert("object in new position", () => firstObject.Position != position); + + void onStateUpdate(DrawableHitObject hitObject, ArmedState state) + { + using (hitObject.BeginAbsoluteSequence(hitObject.StateUpdateTime)) + hitObject.MoveToOffset(new Vector2(-100, 0)); + } + } + [Test] public void TestNotReusedWithHitObjectsSpacedClose() { @@ -134,41 +171,44 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [BackgroundDependencyLoader] - private void load() - { - RegisterPool(PoolSize); - } - - protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); - public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); - protected override Playfield CreatePlayfield() => new TestPlayfield(); - - private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry - { - public TestHitObjectLifetimeEntry(HitObject hitObject) - : base(hitObject) - { - } - - protected override double InitialLifetimeOffset => 0; - } + protected override Playfield CreatePlayfield() => new TestPlayfield(PoolSize); } private class TestPlayfield : Playfield { - public TestPlayfield() + private readonly int poolSize; + + public TestPlayfield(int poolSize) { + this.poolSize = poolSize; AddInternal(HitObjectContainer); } + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(poolSize); + } + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + protected override GameplayCursorContainer CreateCursor() => null; } + private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public TestHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => 0; + } + private class TestBeatmapConverter : BeatmapConverter { public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) @@ -207,7 +247,6 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre; Origin = Anchor.Centre; - Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); Size = new Vector2(50, 50); Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); @@ -222,6 +261,12 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset > HitObject.Duration) diff --git a/osu.Game.Tournament.Tests/.vscode/tasks.json b/osu.Game.Tournament.Tests/.vscode/tasks.json index c69ac0391a..04ec7275ac 100644 --- a/osu.Game.Tournament.Tests/.vscode/tasks.json +++ b/osu.Game.Tournament.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 655beb4bdd..f2065e7e88 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -144,9 +144,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components if (selected) { selectionBox.Show(); - if (editor) + if (editor && editorInfo != null) editorInfo.Selected.Value = Match; - else + else if (ladderInfo != null) ladderInfo.CurrentMatch.Value = Match; } else diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 98bc292901..e78d3a9e83 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -83,8 +83,8 @@ namespace osu.Game.Tournament.Screens }, new ActionableInfo { - Label = "Current User", - ButtonText = "Change Login", + Label = "Current user", + ButtonText = "Change sign-in", Action = () => { api.Logout(); @@ -102,12 +102,12 @@ namespace osu.Game.Tournament.Screens }, Value = api?.LocalUser.Value.Username, Failing = api?.IsLoggedIn != true, - Description = "In order to access the API and display metadata, a login is required." + Description = "In order to access the API and display metadata, signing in is required." }, new LabelledDropdown { Label = "Ruleset", - Description = "Decides what stats are displayed and which ranks are retrieved for players", + Description = "Decides what stats are displayed and which ranks are retrieved for players.", Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 2c539cdd43..870ea466cc 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -127,10 +127,10 @@ namespace osu.Game.Tournament new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen }, + new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen }, new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen }, + new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen }, new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen }, new Separator(), new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen }, diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 862be41c1a..8d02af6574 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Game.Beatmaps; namespace osu.Game.Audio @@ -76,7 +77,7 @@ namespace osu.Game.Audio /// The which may be the owner of the . public void StopAnyPlaying(IPreviewTrackOwner source) { - if (CurrentTrack == null || CurrentTrack.Owner != source) + if (CurrentTrack == null || (CurrentTrack.Owner != null && CurrentTrack.Owner != source)) return; CurrentTrack.Stop(); @@ -86,11 +87,12 @@ namespace osu.Game.Audio /// /// Creates the . /// - protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); + protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => + new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); public class TrackManagerPreviewTrack : PreviewTrack { - [Resolved] + [Resolved(canBeNull: true)] public IPreviewTrackOwner Owner { get; private set; } private readonly BeatmapSetInfo beatmapSetInfo; @@ -102,6 +104,12 @@ namespace osu.Game.Audio this.trackManager = trackManager; } + protected override void LoadComplete() + { + base.LoadComplete(); + Logger.Log($"A {nameof(PreviewTrack)} was created without a containing {nameof(IPreviewTrackOwner)}. An owner should be added for correct behaviour."); + } + protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3"); } diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 5d9cf612bb..bed95344c6 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -46,12 +46,15 @@ namespace osu.Game.Online { if (modelInfo.NewValue == null) attachDownload(null); - else if (manager.IsAvailableLocally(modelInfo.NewValue)) + else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true) State.Value = DownloadState.LocallyAvailable; else - attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); + attachDownload(manager?.GetExistingDownload(modelInfo.NewValue)); }, true); + if (manager == null) + return; + managerDownloadBegan = manager.DownloadBegan.GetBoundCopy(); managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 3a5c2e181f..d18f189a70 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -248,7 +248,9 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load() { - apiState.BindTo(api.State); + if (api != null) + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } @@ -303,7 +305,7 @@ namespace osu.Game.Online.Leaderboards PlaceholderState = PlaceholderState.NetworkFailure; }); - api.Queue(getScoresRequest); + api?.Queue(getScoresRequest); }); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 193f6fe61b..e7b5d3304d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -194,6 +194,20 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); + // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. + SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo => + { + if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) + { + Schedule(() => + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID) + SkinManager.CurrentSkinInfo.Value = SkinInfo.Default; + }); + } + }); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index 5375476c9e..b2096968fe 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.AccountCreation public override void OnEntering(IScreen last) { - if (string.IsNullOrEmpty(api.ProvidedUsername)) + if (string.IsNullOrEmpty(api?.ProvidedUsername)) { this.FadeOut(); this.Push(new ScreenEntry()); @@ -43,7 +43,7 @@ namespace osu.Game.Overlays.AccountCreation [BackgroundDependencyLoader(true)] private void load(OsuColour colours, OsuGame game, TextureStore textures) { - if (string.IsNullOrEmpty(api.ProvidedUsername)) + if (string.IsNullOrEmpty(api?.ProvidedUsername)) return; InternalChildren = new Drawable[] diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 873272bf12..8f757f7a36 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings.Sections.General private void performLogin() { if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) - api.Login(username.Text, password.Text); + api?.Login(username.Text, password.Text); else shakeSignIn.Shake(); } diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 5898c116dd..e2d4fc6051 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -76,7 +76,7 @@ namespace osu.Game // a dialog may be blocking the execution for now. if (checkForDialog(current)) return; - game.CloseAllOverlays(false); + game?.CloseAllOverlays(false); // we may already be at the target screen type. if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index e8b051b4d9..a44967c21c 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -18,14 +18,17 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime, IApplicableToDrawableRuleset - where TObject : HitObject + public abstract class ModNightcore : ModDoubleTime { public override string Name => "Nightcore"; public override string Acronym => "NC"; public override IconUsage? Icon => OsuIcon.ModNightcore; public override string Description => "Uguuuuuuuu..."; + } + public abstract class ModNightcore : ModNightcore, IApplicableToDrawableRuleset + where TObject : HitObject + { private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3e3936b45a..ca49ed9e75 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private HitObjectLifetimeEntry lifetimeEntry; [Resolved(CanBeNull = true)] - private DrawableRuleset drawableRuleset { get; set; } + private IPooledHitObjectProvider pooledObjectProvider { get; set; } private Container samplesContainer; @@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // Copy any existing result from the entry (required for rewind / judgement revert). Result = lifetimeEntry.Result; } + else + LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) @@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); @@ -285,8 +287,11 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(HitObject); HitObject = null; + Result = null; lifetimeEntry = null; + clearExistingStateTransforms(); + hasHitObjectApplied = false; } @@ -403,8 +408,7 @@ namespace osu.Game.Rulesets.Objects.Drawables double transformTime = HitObject.StartTime - InitialLifetimeOffset; - base.ApplyTransformsAt(double.MinValue, true); - base.ClearTransformsAfter(double.MinValue, true); + clearExistingStateTransforms(); using (BeginAbsoluteSequence(transformTime, true)) UpdateInitialTransforms(); @@ -432,6 +436,12 @@ namespace osu.Game.Rulesets.Objects.Drawables PlaySamples(); } + private void clearExistingStateTransforms() + { + base.ApplyTransformsAt(double.MinValue, true); + base.ClearTransformsAfter(double.MinValue, true); + } + /// /// Apply (generally fade-in) transforms leading into the start time. /// The local drawable hierarchy is recursively delayed to for convenience. @@ -638,6 +648,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This is only used as an optimisation to delay the initial update of this and may be tuned more aggressively if required. /// It is indirectly used to decide the automatic transform offset provided to . /// A more accurate should be set for further optimisation (in , for example). + /// + /// Only has an effect if this is not being pooled. + /// For pooled s, use instead. + /// /// protected virtual double InitialLifetimeOffset => 10000; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 8caadffd1d..b3b3d11ab3 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -81,10 +81,18 @@ namespace osu.Game.Rulesets value |= LegacyMods.HardRock; break; + case ModPerfect _: + value |= LegacyMods.Perfect; + break; + case ModSuddenDeath _: value |= LegacyMods.SuddenDeath; break; + case ModNightcore _: + value |= LegacyMods.Nightcore; + break; + case ModDoubleTime _: value |= LegacyMods.DoubleTime; break; @@ -100,6 +108,14 @@ namespace osu.Game.Rulesets case ModFlashlight _: value |= LegacyMods.Flashlight; break; + + case ModCinema _: + value |= LegacyMods.Cinema; + break; + + case ModAutoplay _: + value |= LegacyMods.Autoplay; + break; } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index c912348604..c1a601eaae 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,10 +15,7 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -248,7 +245,7 @@ namespace osu.Game.Rulesets.UI if (drawableRepresentation != null) Playfield.Add(drawableRepresentation); else - Playfield.Add(GetLifetimeEntry(hitObject)); + Playfield.Add(hitObject); } /// @@ -260,15 +257,10 @@ namespace osu.Game.Rulesets.UI /// The to remove. public bool RemoveHitObject(TObject hitObject) { - var entry = GetLifetimeEntry(hitObject); - - // May have been newly-created by the above call - remove it anyway. - RemoveLifetimeEntry(hitObject); - - if (Playfield.Remove(entry)) + if (Playfield.Remove(hitObject)) return true; - // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal. + // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct drawable removal. var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); if (drawableObject != null) return Playfield.Remove(drawableObject); @@ -276,16 +268,6 @@ namespace osu.Game.Rulesets.UI return false; } - protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) - { - if (!(hitObject is TObject tHitObject)) - throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}"); - - return CreateLifetimeEntry(tHitObject); - } - - protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject); - public override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) @@ -327,9 +309,8 @@ namespace osu.Game.Rulesets.UI /// Creates a to represent a . /// /// - /// If this method returns null, then this will assume the requested type is being pooled, - /// and will instead attempt to retrieve the s at the point they should become alive via pools registered through - /// or . + /// If this method returns null, then this will assume the requested type is being pooled inside the , + /// and will instead attempt to retrieve the s at the point they should become alive via pools registered in the . /// /// The to represent. /// The representing . @@ -549,99 +530,6 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); - - private readonly Dictionary pools = new Dictionary(); - private readonly Dictionary lifetimeEntries = new Dictionary(); - - /// - /// Registers a default pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The number of s to be initially stored in the pool. - /// - /// The maximum number of s that can be stored in the pool. - /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, - /// until some of the existing s are returned to the pool. - /// - /// The type. - /// The receiver for s. - protected void RegisterPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(new DrawablePool(initialSize, maximumSize)); - - /// - /// Registers a custom pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The to register. - /// The type. - /// The receiver for s. - protected void RegisterPool([NotNull] DrawablePool pool) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - { - pools[typeof(TObject)] = pool; - AddInternal(pool); - } - - /// - /// Attempts to retrieve the poolable representation of a . - /// - /// The to retrieve the representation of. - /// The representing , or null if no poolable representation exists. - [CanBeNull] - public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) - { - if (!pools.TryGetValue(hitObject.GetType(), out var pool)) - return null; - - return (DrawableHitObject)pool.Get(d => - { - var dho = (DrawableHitObject)d; - - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) - { - foreach (var m in Mods.OfType()) - m.ApplyToDrawableHitObjects(dho.Yield()); - } - - dho.Apply(hitObject, GetLifetimeEntry(hitObject)); - }); - } - - /// - /// Creates the for a given . - /// - /// - /// This may be overridden to provide custom lifetime control (e.g. via . - /// - /// The to create the entry for. - /// The . - [NotNull] - protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject); - - /// - /// Retrieves or creates the for a given . - /// - /// The to retrieve or create the for. - /// The for . - [NotNull] - protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) - { - if (lifetimeEntries.TryGetValue(hitObject, out var entry)) - return entry; - - return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); - } - - /// - /// Removes the for a . - /// - /// The to remove the for. - internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 25fb7ab9f3..5fbda305c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.UI private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); [Resolved(CanBeNull = true)] - private DrawableRuleset drawableRuleset { get; set; } + private IPooledHitObjectProvider pooledObjectProvider { get; set; } public HitObjectContainer() { @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs new file mode 100644 index 0000000000..315926dfc6 --- /dev/null +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.UI +{ + internal interface IPooledHitObjectProvider + { + /// + /// Attempts to retrieve the poolable representation of a . + /// + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6747145d50..82ec653f31 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -17,7 +19,8 @@ using osuTK; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : CompositeDrawable + [Cached(typeof(IPooledHitObjectProvider))] + public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider { /// /// Invoked when a is judged. @@ -138,39 +141,6 @@ namespace osu.Game.Rulesets.UI return false; } - /// - /// Adds a for a pooled to this . - /// - /// The controlling the lifetime of the . - public virtual void Add(HitObjectLifetimeEntry entry) - { - HitObjectContainer.Add(entry); - lifetimeEntryMap[entry.HitObject] = entry; - OnHitObjectAdded(entry.HitObject); - } - - /// - /// Removes a for a pooled from this . - /// - /// The controlling the lifetime of the . - /// Whether the was successfully removed. - public virtual bool Remove(HitObjectLifetimeEntry entry) - { - if (HitObjectContainer.Remove(entry)) - { - lifetimeEntryMap.Remove(entry.HitObject); - OnHitObjectRemoved(entry.HitObject); - return true; - } - - bool removedFromNested = false; - - if (nestedPlayfields.IsValueCreated) - removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); - - return removedFromNested; - } - /// /// Invoked when a is added to this . /// @@ -246,6 +216,128 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + #region Pooling support + + private readonly Dictionary pools = new Dictionary(); + + /// + /// Adds a for a pooled to this . + /// + /// + public virtual void Add(HitObject hitObject) + { + var entry = CreateLifetimeEntry(hitObject); + lifetimeEntryMap[entry.HitObject] = entry; + + HitObjectContainer.Add(entry); + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a for a pooled from this . + /// + /// + /// Whether the was successfully removed. + public virtual bool Remove(HitObject hitObject) + { + if (lifetimeEntryMap.Remove(hitObject, out var entry)) + { + HitObjectContainer.Remove(entry); + OnHitObjectRemoved(hitObject); + return true; + } + + bool removedFromNested = false; + + if (nestedPlayfields.IsValueCreated) + removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject)); + + return removedFromNested; + } + + /// + /// Creates the for a given . + /// + /// + /// This may be overridden to provide custom lifetime control (e.g. via . + /// + /// The to create the entry for. + /// The . + [NotNull] + protected virtual HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject) => new HitObjectLifetimeEntry(hitObject); + + /// + /// Registers a default pool with this which is to be used whenever + /// representations are requested for the given type. + /// + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// + /// The type. + /// The receiver for s. + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a custom pool with this which is to be used whenever + /// representations are requested for the given type. + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject) + { + var lookupType = hitObject.GetType(); + + IDrawablePool pool; + + // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. + if (!pools.TryGetValue(lookupType, out pool)) + { + foreach (var (t, p) in pools) + { + if (!t.IsInstanceOfType(hitObject)) + continue; + + pools[lookupType] = pool = p; + break; + } + } + + return (DrawableHitObject)pool?.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); + + dho.Apply(hitObject, entry); + }); + } + + #endregion + #region Editor logic /// diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3229719d5a..53b6e14940 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -342,8 +342,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (!blueprint.IsHovered) continue; - if (SelectionHandler.HandleSelectionRequested(blueprint, e)) - return clickSelectionBegan = true; + return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e); } return false; @@ -457,6 +456,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprint == null) return false; + if (snapProvider == null) + return true; + Debug.Assert(movementBlueprintOriginalPosition != null); HitObject draggedObject = movementBlueprint.HitObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 21810379cc..adf22a3370 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void OnOperationBegan() { - ChangeHandler.BeginChange(); + ChangeHandler?.BeginChange(); } /// @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void OnOperationEnded() { - ChangeHandler.EndChange(); + ChangeHandler?.EndChange(); } #region User Input Handling diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 0271b2def9..eef02e61a6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (lastDragEvent != null) OnDrag(lastDragEvent); - if (Composer != null) + if (Composer != null && timeline != null) { Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 09d861522a..eab909b798 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing controlPointGroups.BindCollectionChanged((sender, args) => { table.ControlGroups = controlPointGroups; - changeHandler.SaveState(); + changeHandler?.SaveState(); }, true); } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index be1083ce8d..3fc1359006 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -59,6 +59,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components { scheduledFilterUpdate?.Cancel(); + if (filter == null) + return; + filter.Value = new FilterCriteria { SearchString = Search.Current.Value ?? string.Empty, diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 6f51771c12..71ce157296 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -35,7 +36,8 @@ using osuTK; namespace osu.Game.Screens.Play { - public class Spectator : OsuScreen + [Cached(typeof(IPreviewTrackOwner))] + public class Spectator : OsuScreen, IPreviewTrackOwner { private readonly User targetUser; @@ -62,6 +64,9 @@ namespace osu.Game.Screens.Play [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } + private Score score; private readonly object scoreLock = new object(); @@ -275,6 +280,7 @@ namespace osu.Game.Screens.Play { watchButton.Enabled.Value = false; beatmapPanelContainer.Clear(); + previewTrackManager.StopAnyPlaying(this); } private void attemptStart() @@ -326,7 +332,6 @@ namespace osu.Game.Screens.Play { if (state?.BeatmapID == null) { - beatmapPanelContainer.Clear(); onlineBeatmap = null; return; } @@ -359,6 +364,12 @@ namespace osu.Game.Screens.Play beatmaps.Download(onlineBeatmap); } + public override bool OnExiting(IScreen next) + { + previewTrackManager.StopAnyPlaying(this); + return base.OnExiting(next); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bef3e86a4d..9b69a1eecd 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -48,16 +48,6 @@ namespace osu.Game.Skinning this.audio = audio; this.legacyDefaultResources = legacyDefaultResources; - ItemRemoved.BindValueChanged(weakRemovedInfo => - { - if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) - { - // check the removed skin is not the current user choice. if it is, switch back to default. - if (removedInfo.ID == CurrentSkinInfo.Value.ID) - CurrentSkinInfo.Value = SkinInfo.Default; - } - }); - CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkin.ValueChanged += skin => { diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 1e43e5d148..e3557222d5 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -9,11 +9,14 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.IO.Stores; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Visual; @@ -21,6 +24,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Beatmaps { + [HeadlessTest] public abstract class HitObjectSampleTest : PlayerTestScene { protected abstract IResourceStore Resources { get; } @@ -44,7 +48,9 @@ namespace osu.Game.Tests.Beatmaps private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); private SkinSourceDependencyContainer dependencies; private IBeatmap currentTestBeatmap; + protected sealed override bool HasCustomSteps => true; + protected override bool Autoplay => true; protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); @@ -54,6 +60,8 @@ namespace osu.Game.Tests.Beatmaps protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); + protected void CreateTestWithBeatmap(string filename) { CreateTest(() => @@ -73,6 +81,9 @@ namespace osu.Game.Tests.Beatmaps currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); }); }); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + AddUntilStep("results displayed", () => Stack.CurrentScreen is ResultsScreen); } protected void SetupSkins(string beatmapFile, string userFile) diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index e93bf916c7..76f97db59f 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps /// protected abstract Ruleset CreateRuleset(); - protected void Test(LegacyMods legacyMods, Type[] expectedMods) + protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) { var ruleset = CreateRuleset(); var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList(); @@ -31,5 +31,15 @@ namespace osu.Game.Tests.Beatmaps Assert.IsNotNull(mods.SingleOrDefault(mod => mod.GetType() == modType)); } } + + protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes) + { + var ruleset = CreateRuleset(); + var modInstances = ruleset.GetAllMods() + .Where(mod => providedModTypes.Contains(mod.GetType())) + .ToArray(); + var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances); + Assert.AreEqual(expectedLegacyMods, actualLegacyMods); + } } }