1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 18:03:11 +08:00

Merge branch 'master' into change-taiko-ez

This commit is contained in:
Bartłomiej Dach 2020-11-16 19:37:47 +01:00
commit 128adce017
53 changed files with 690 additions and 305 deletions

View File

@ -13,6 +13,24 @@
"commands": [ "commands": [
"dotnet-format" "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"
]
} }
} }
} }

17
.vscode/tasks.json vendored
View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Desktop", "osu.Desktop",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Desktop", "osu.Desktop",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -40,7 +38,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tests", "osu.Game.Tests",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -55,7 +52,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tests", "osu.Game.Tests",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -71,7 +67,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests", "osu.Game.Tournament.Tests",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -86,7 +81,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests", "osu.Game.Tournament.Tests",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -102,7 +96,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Benchmarks", "osu.Game.Benchmarks",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -111,16 +104,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore (netcoreapp3.1)",
"type": "shell",
"command": "dotnet",
"args": [
"restore",
"build/Desktop.proj"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -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). 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 / 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: You can also build and run *osu!* from the command-line with a single command:

View File

@ -1,7 +1,4 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.36" #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 // ARGUMENTS
@ -18,23 +15,15 @@ var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
// TASKS // TASKS
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// windows only because both inspectcode and nvika depend on net45
Task("InspectCode") Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
.Does(() => { .Does(() => {
InspectCode(desktopSlnf, new InspectCodeSettings { var inspectcodereport = "inspectcodereport.xml";
CachesHome = "inspectcode", var cacheDir = "inspectcode";
OutputFile = "inspectcodereport.xml", var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
ArgumentCustomization = arg => {
if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output
arg.Append("--verbosity:WARN");
return arg;
},
});
int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors"); DotNetCoreTool(rootDirectory.FullPath,
if (returnCode != 0) "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
throw new Exception($"inspectcode failed with return code {returnCode}"); DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
}); });
Task("CodeFileSanity") Task("CodeFileSanity")

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj", "osu.Game.Rulesets.Catch.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj", "osu.Game.Rulesets.Catch.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -12,17 +12,32 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture] [TestFixture]
public class CatchLegacyModConversionTest : LegacyModConversionTest public class CatchLegacyModConversionTest : LegacyModConversionTest
{ {
[TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })] private static readonly object[][] catch_mod_mapping =
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })] {
[TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })] new object[] { LegacyMods.NoFail, new[] { typeof(CatchModNoFail) } },
[TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })] 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.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, new[] { typeof(CatchModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(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(); protected override Ruleset CreateRuleset() => new CatchRuleset();
} }

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj", "osu.Game.Rulesets.Mania.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj", "osu.Game.Rulesets.Mania.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -12,19 +12,44 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class ManiaLegacyModConversionTest : LegacyModConversionTest public class ManiaLegacyModConversionTest : LegacyModConversionTest
{ {
[TestCase(LegacyMods.Easy, new[] { typeof(ManiaModEasy) })] private static readonly object[][] mania_mod_mapping =
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) })] {
[TestCase(LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) })] new object[] { LegacyMods.NoFail, new[] { typeof(ManiaModNoFail) } },
[TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })] 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.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, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] [TestCaseSource(nameof(mania_mod_mapping))]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();
} }

View File

@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.Key9)) if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9(); yield return new ManiaModKey9();
if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages();
if (mods.HasFlag(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail(); yield return new ManiaModNoFail();
@ -173,13 +176,22 @@ namespace osu.Game.Rulesets.Mania
value |= LegacyMods.Key9; value |= LegacyMods.Key9;
break; break;
case ManiaModDualStages _:
value |= LegacyMods.KeyCoop;
break;
case ManiaModFadeIn _: case ManiaModFadeIn _:
value |= LegacyMods.FadeIn; value |= LegacyMods.FadeIn;
value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that.
break; break;
case ManiaModMirror _: case ManiaModMirror _:
value |= LegacyMods.Mirror; value |= LegacyMods.Mirror;
break; break;
case ManiaModRandom _:
value |= LegacyMods.Random;
break;
} }
} }

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj", "osu.Game.Rulesets.Osu.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj", "osu.Game.Rulesets.Osu.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -12,18 +12,36 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture] [TestFixture]
public class OsuLegacyModConversionTest : LegacyModConversionTest public class OsuLegacyModConversionTest : LegacyModConversionTest
{ {
[TestCase(LegacyMods.Easy, new[] { typeof(OsuModEasy) })] private static readonly object[][] osu_mod_mapping =
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) })] {
[TestCase(LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) })] new object[] { LegacyMods.NoFail, new[] { typeof(OsuModNoFail) } },
[TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })] 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.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, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(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(); protected override Ruleset CreateRuleset() => new OsuRuleset();
} }

View File

@ -1,20 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; 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 osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneSliderApplication : OsuTestScene public class TestSceneSliderApplication : OsuTestScene
{ {
[Resolved]
private SkinManager skinManager { get; set; }
[Test] [Test]
public void TestApplyNewSlider() public void TestApplyNewSlider()
{ {
@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests
}), null)); }), 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<SliderBall>().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red);
}
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)
{ {
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public class TestSceneSpinnerApplication : OsuTestScene public class TestSceneSpinnerApplication : OsuTestScene
{ {
[Test] [Test]
public void TestApplyNewCircle() public void TestApplyNewSpinner()
{ {
DrawableSpinner dho = null; DrawableSpinner dho = null;
@ -23,18 +23,23 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
IndexInCurrentCombo = 0, IndexInCurrentCombo = 0,
Duration = 0, Duration = 500,
})) }))
{ {
Clock = new FramedClock(new StopwatchClock()) 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 AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner
{ {
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
ComboIndex = 1, ComboIndex = 1,
Duration = 1000, Duration = 1000,
}), null)); }), null));
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
} }
private Spinner prepareObject(Spinner circle) private Spinner prepareObject(Spinner circle)

View File

@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Osu.Tests
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); 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 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); addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); 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] [Test]
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
finalSpinnerSymbolRotation = spinnerSymbol.Rotation; finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f); 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); addSeekStep(2500);
AddAssert("disc rotation rewound", AddAssert("disc rotation rewound",
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound", 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. // 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); addSeekStep(5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same", AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
} }
[Test] [Test]
@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
// multipled by 2 to nullify the score multiplier. (autoplay mod selected) // multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; 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); addSeekStep(0);

View File

@ -207,11 +207,17 @@ namespace osu.Game.Rulesets.Osu.Edit
Quad quad = getSurroundingQuad(hitObjects); Quad quad = getSurroundingQuad(hitObjects);
if (quad.TopLeft.X + delta.X < 0 || Vector2 newTopLeft = quad.TopLeft + delta;
quad.TopLeft.Y + delta.Y < 0 || if (newTopLeft.X < 0)
quad.BottomRight.X + delta.X > DrawWidth || delta.X -= newTopLeft.X;
quad.BottomRight.Y + delta.Y > DrawHeight) if (newTopLeft.Y < 0)
return false; 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) foreach (var h in hitObjects)
h.Position += delta; h.Position += delta;

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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
{
/// <summary>
/// The <see cref="Spinner"/>.
/// </summary>
public Spinner Spinner => (Spinner)HitObject;
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// </summary>
/// <remarks>
/// <para>
/// 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).
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <example>
/// 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.
/// </example>
public float RateAdjustedRotation;
/// <summary>
/// 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.
/// </summary>
public double? TimeCompleted;
public OsuSpinnerJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
}
}
}

View File

@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
foreach (var drawableHitObject in NestedHitObjects) foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue; drawableHitObject.AccentColour.Value = colour.NewValue;
updateBallTint();
}, true); }, true);
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
@ -244,7 +245,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.ApplySkin(skin, allowFallback); base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; updateBallTint();
}
private void updateBallTint()
{
if (CurrentSkin == null)
return;
bool allowBallTint = CurrentSkin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
} }

View File

@ -10,8 +10,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; 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.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -24,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public new Spinner HitObject => (Spinner)base.HitObject; public new Spinner HitObject => (Spinner)base.HitObject;
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; } public SpinnerRotationTracker RotationTracker { get; private set; }
public SpinnerSpmCounter SpmCounter { get; private set; } public SpinnerSpmCounter SpmCounter { get; private set; }
@ -197,15 +201,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// these become implicitly hit. // these become implicitly hit.
return 1; 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) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (Time.Current < HitObject.StartTime) return; if (Time.Current < HitObject.StartTime) return;
RotationTracker.Complete.Value = Progress >= 1; if (Progress >= 1)
Result.TimeCompleted ??= Time.Current;
if (userTriggered || Time.Current < HitObject.EndTime) if (userTriggered || Time.Current < HitObject.EndTime)
return; return;
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking) if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn); SpmCounter.FadeIn(HitObject.TimeFadeIn);
SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation); SpmCounter.SetRotation(Result.RateAdjustedRotation);
updateBonusScore(); updateBonusScore();
} }
@ -256,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0) if (ticks.Count == 0)
return; return;
int spins = (int)(RotationTracker.RateAdjustedRotation / 360); int spins = (int)(Result.RateAdjustedRotation / 360);
if (spins < wholeSpins) if (spins < wholeSpins)
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private SpinnerTicks ticks; private SpinnerTicks ticks;
private int wholeRotationCount; private int wholeRotationCount;
private readonly BindableBool complete = new BindableBool();
private SpinnerFill fill; private SpinnerFill fill;
private Container mainContainer; private Container mainContainer;
@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
base.LoadComplete(); base.LoadComplete();
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
base.Update(); base.Update();
if (drawableSpinner.RotationTracker.Complete.Value) complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
if (complete.Value)
{ {
if (checkNewRotationCount) if (checkNewRotationCount)
{ {
@ -194,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
get get
{ {
int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360); int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false; if (wholeRotationCount == rotations) return false;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public SpinnerRotationTracker(DrawableSpinner drawableSpinner) public SpinnerRotationTracker(DrawableSpinner drawableSpinner)
{ {
this.drawableSpinner = drawableSpinner; this.drawableSpinner = drawableSpinner;
drawableSpinner.HitObjectApplied += resetState;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -30,32 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public bool Tracking { get; set; } public bool Tracking { get; set; }
public readonly BindableBool Complete = new BindableBool();
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// </summary>
/// <remarks>
/// <para>
/// 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).
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <example>
/// 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 <see cref="Drawable.Rotation"/>).
/// 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.
/// </example>
public float RateAdjustedRotation { get; private set; }
/// <summary> /// <summary>
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
/// </summary> /// </summary>
@ -131,7 +107,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += angle; currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp) // (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;
} }
} }
} }

View File

@ -107,6 +107,35 @@ namespace osu.Game.Rulesets.Osu
yield return new OsuModTouchDevice(); 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<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type)
{ {
switch (type) switch (type)

View File

@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
completed.BindTo(DrawableSpinner.RotationTracker.Complete);
completed.BindValueChanged(onCompletedChanged, true); completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; 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) protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
switch (drawableHitObject) switch (drawableHitObject)

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj", "osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj", "osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -12,17 +12,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestFixture] [TestFixture]
public class TaikoLegacyModConversionTest : LegacyModConversionTest public class TaikoLegacyModConversionTest : LegacyModConversionTest
{ {
[TestCase(LegacyMods.Easy, new[] { typeof(TaikoModEasy) })] private static readonly object[][] taiko_mod_mapping =
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) })] {
[TestCase(LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) })] new object[] { LegacyMods.NoFail, new[] { typeof(TaikoModNoFail) } },
[TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })] 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.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, new[] { typeof(TaikoModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime), typeof(TaikoModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(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(); protected override Ruleset CreateRuleset() => new TaikoRuleset();
} }

View File

@ -92,6 +92,19 @@ namespace osu.Game.Rulesets.Taiko
if (mods.HasFlag(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax(); 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<TaikoModRandom>().Any())
value |= LegacyMods.Random;
return value;
} }
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type)

View File

@ -167,6 +167,21 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0); 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 getTrack() => (TestPreviewTrack)trackManager.Get(null);
private TestPreviewTrack getOwnedTrack() private TestPreviewTrack getOwnedTrack()
@ -181,10 +196,12 @@ namespace osu.Game.Tests.Visual.Components
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{ {
private readonly PreviewTrack track; private readonly PreviewTrack track;
private readonly bool registerAsOwner;
public TestTrackOwner(PreviewTrack track) public TestTrackOwner(PreviewTrack track, bool registerAsOwner = true)
{ {
this.track = track; this.track = track;
this.registerAsOwner = registerAsOwner;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -196,7 +213,8 @@ namespace osu.Game.Tests.Visual.Components
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IPreviewTrackOwner>(this); if (registerAsOwner)
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies; return dependencies;
} }
} }

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.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<BlueprintContainer>().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<HitCirclePiece>().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<PathControlPointPiece>().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));
}
}
}

View File

@ -23,17 +23,15 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestScenePlayerLoader : OsuManualInputManagerTestScene public class TestScenePlayerLoader : ScreenTestScene
{ {
private TestPlayerLoader loader; private TestPlayerLoader loader;
private TestPlayerLoaderContainer container;
private TestPlayer player; private TestPlayer player;
private bool epilepsyWarning; private bool epilepsyWarning;
@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } 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();
});
/// <summary> /// <summary>
/// Sets the input manager child to a new test player loader container instance. /// Sets the input manager child to a new test player loader container instance.
/// </summary> /// </summary>
/// <param name="interactive">If the test player should behave like the production one.</param> /// <param name="interactive">If the test player should behave like the production one.</param>
/// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param> /// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param>
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(); beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container; LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
} }
[Test] [Test]
public void TestEarlyExitBeforePlayerConstruction() 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()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("exit loader", () => loader.Exit()); AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestEarlyExitAfterPlayerConstruction() 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()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddUntilStep("wait for non-null player", () => player != null); AddUntilStep("wait for non-null player", () => player != null);
@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestBlockLoadViaMouseMovement() public void TestBlockLoadViaMouseMovement()
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for load ready", () => AddUntilStep("wait for load ready", () =>
@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestBlockLoadViaFocus() public void TestBlockLoadViaFocus()
{ {
OsuFocusedOverlayContainer overlay = null; AddStep("load dummy beatmap", () => resetPlayer(false));
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); }); AddStep("show focused overlay", () => changelogOverlay.Show());
AddUntilStep("overlay visible", () => overlay.IsPresent); 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); AddRepeatStep("twiddle thumbs", () => { }, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen()); AddAssert("loader still active", () => loader.IsCurrentScreen());
AddStep("hide overlay", () => overlay.Hide()); AddStep("hide overlay", () => changelogOverlay.Hide());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
} }
@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
SlowLoadPlayer slowPlayer = null; 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", () => AddStep("load slow dummy beatmap", () =>
{ {
InputManager.Child = container = new TestPlayerLoaderContainer( LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
}); });
@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
TestMod playerMod1 = null; TestMod playerMod1 = null;
TestMod playerMod2 = 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()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var testMod = new TestMod(); var testMod = new TestMod();
AddStep("load player", () => ResetPlayer(true)); AddStep("load player", () => resetPlayer(true));
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod }); AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod });
@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestMutedNotificationMuteButton() 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);
} }
/// <remarks> /// <remarks>
@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false); AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad)); AddStep("load player", () => resetPlayer(false, beforeLoad));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); 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", () => AddStep("click notification", () =>
{ {
var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last(); var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First(); var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First(); var notification = flowContainer.First();
@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarning(bool warning) public void TestEpilepsyWarning(bool warning)
{ {
AddStep("change epilepsy warning", () => epilepsyWarning = 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()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); 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); 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 private class TestPlayerLoader : PlayerLoader
{ {
public new VisualSettings VisualSettings => base.VisualSettings; public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -57,6 +57,43 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().Single() == firstObject); AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().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<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]);
AddStep("get DHO", () => firstObject = this.ChildrenOfType<DrawableTestHitObject>().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<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]);
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().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] [Test]
public void TestNotReusedWithHitObjectsSpacedClose() public void TestNotReusedWithHitObjectsSpacedClose()
{ {
@ -210,7 +247,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
Size = new Vector2(50, 50); Size = new Vector2(50, 50);
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
@ -225,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) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (timeOffset > HitObject.Duration) if (timeOffset > HitObject.Duration)

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -144,9 +144,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
if (selected) if (selected)
{ {
selectionBox.Show(); selectionBox.Show();
if (editor) if (editor && editorInfo != null)
editorInfo.Selected.Value = Match; editorInfo.Selected.Value = Match;
else else if (ladderInfo != null)
ladderInfo.CurrentMatch.Value = Match; ladderInfo.CurrentMatch.Value = Match;
} }
else else

View File

@ -83,8 +83,8 @@ namespace osu.Game.Tournament.Screens
}, },
new ActionableInfo new ActionableInfo
{ {
Label = "Current User", Label = "Current user",
ButtonText = "Change Login", ButtonText = "Change sign-in",
Action = () => Action = () =>
{ {
api.Logout(); api.Logout();
@ -102,12 +102,12 @@ namespace osu.Game.Tournament.Screens
}, },
Value = api?.LocalUser.Value.Username, Value = api?.LocalUser.Value.Username,
Failing = api?.IsLoggedIn != true, 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<RulesetInfo> new LabelledDropdown<RulesetInfo>
{ {
Label = "Ruleset", 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, Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset, Current = LadderInfo.Ruleset,
}, },

View File

@ -127,10 +127,10 @@ namespace osu.Game.Tournament
new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen }, new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
new Separator(), 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 ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen },
new Separator(), 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 ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen }, new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },

View File

@ -11,6 +11,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
namespace osu.Game.Audio namespace osu.Game.Audio
@ -76,7 +77,7 @@ namespace osu.Game.Audio
/// <param name="source">The <see cref="IPreviewTrackOwner"/> which may be the owner of the <see cref="PreviewTrack"/>.</param> /// <param name="source">The <see cref="IPreviewTrackOwner"/> which may be the owner of the <see cref="PreviewTrack"/>.</param>
public void StopAnyPlaying(IPreviewTrackOwner source) public void StopAnyPlaying(IPreviewTrackOwner source)
{ {
if (CurrentTrack == null || CurrentTrack.Owner != source) if (CurrentTrack == null || (CurrentTrack.Owner != null && CurrentTrack.Owner != source))
return; return;
CurrentTrack.Stop(); CurrentTrack.Stop();
@ -86,11 +87,12 @@ namespace osu.Game.Audio
/// <summary> /// <summary>
/// Creates the <see cref="TrackManagerPreviewTrack"/>. /// Creates the <see cref="TrackManagerPreviewTrack"/>.
/// </summary> /// </summary>
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 public class TrackManagerPreviewTrack : PreviewTrack
{ {
[Resolved] [Resolved(canBeNull: true)]
public IPreviewTrackOwner Owner { get; private set; } public IPreviewTrackOwner Owner { get; private set; }
private readonly BeatmapSetInfo beatmapSetInfo; private readonly BeatmapSetInfo beatmapSetInfo;
@ -102,6 +104,12 @@ namespace osu.Game.Audio
this.trackManager = trackManager; 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"); protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3");
} }

View File

@ -46,12 +46,15 @@ namespace osu.Game.Online
{ {
if (modelInfo.NewValue == null) if (modelInfo.NewValue == null)
attachDownload(null); attachDownload(null);
else if (manager.IsAvailableLocally(modelInfo.NewValue)) else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true)
State.Value = DownloadState.LocallyAvailable; State.Value = DownloadState.LocallyAvailable;
else else
attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); attachDownload(manager?.GetExistingDownload(modelInfo.NewValue));
}, true); }, true);
if (manager == null)
return;
managerDownloadBegan = manager.DownloadBegan.GetBoundCopy(); managerDownloadBegan = manager.DownloadBegan.GetBoundCopy();
managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed = manager.DownloadFailed.GetBoundCopy();

View File

@ -248,7 +248,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
apiState.BindTo(api.State); if (api != null)
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true); apiState.BindValueChanged(onlineStateChanged, true);
} }
@ -303,7 +305,7 @@ namespace osu.Game.Online.Leaderboards
PlaceholderState = PlaceholderState.NetworkFailure; PlaceholderState = PlaceholderState.NetworkFailure;
}); });
api.Queue(getScoresRequest); api?.Queue(getScoresRequest);
}); });
} }

View File

@ -194,6 +194,20 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy"))); dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.CacheAs<ISkinSource>(SkinManager); dependencies.CacheAs<ISkinSource>(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(API ??= new APIAccess(LocalConfig));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient());

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays.AccountCreation
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
if (string.IsNullOrEmpty(api.ProvidedUsername)) if (string.IsNullOrEmpty(api?.ProvidedUsername))
{ {
this.FadeOut(); this.FadeOut();
this.Push(new ScreenEntry()); this.Push(new ScreenEntry());
@ -43,7 +43,7 @@ namespace osu.Game.Overlays.AccountCreation
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours, OsuGame game, TextureStore textures) private void load(OsuColour colours, OsuGame game, TextureStore textures)
{ {
if (string.IsNullOrEmpty(api.ProvidedUsername)) if (string.IsNullOrEmpty(api?.ProvidedUsername))
return; return;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]

View File

@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
private void performLogin() private void performLogin()
{ {
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api.Login(username.Text, password.Text); api?.Login(username.Text, password.Text);
else else
shakeSignIn.Shake(); shakeSignIn.Shake();
} }

View File

@ -76,7 +76,7 @@ namespace osu.Game
// a dialog may be blocking the execution for now. // a dialog may be blocking the execution for now.
if (checkForDialog(current)) return; if (checkForDialog(current)) return;
game.CloseAllOverlays(false); game?.CloseAllOverlays(false);
// we may already be at the target screen type. // we may already be at the target screen type.
if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled)

View File

@ -18,14 +18,17 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModNightcore<TObject> : ModDoubleTime, IApplicableToDrawableRuleset<TObject> public abstract class ModNightcore : ModDoubleTime
where TObject : HitObject
{ {
public override string Name => "Nightcore"; public override string Name => "Nightcore";
public override string Acronym => "NC"; public override string Acronym => "NC";
public override IconUsage? Icon => OsuIcon.ModNightcore; public override IconUsage? Icon => OsuIcon.ModNightcore;
public override string Description => "Uguuuuuuuu..."; public override string Description => "Uguuuuuuuu...";
}
public abstract class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1); private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1); private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);

View File

@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Copy any existing result from the entry (required for rewind / judgement revert). // Copy any existing result from the entry (required for rewind / judgement revert).
Result = lifetimeEntry.Result; Result = lifetimeEntry.Result;
} }
else
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
// Ensure this DHO has a result. // Ensure this DHO has a result.
Result ??= CreateResult(HitObject.CreateJudgement()) Result ??= CreateResult(HitObject.CreateJudgement())
@ -285,8 +287,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnFree(HitObject); OnFree(HitObject);
HitObject = null; HitObject = null;
Result = null;
lifetimeEntry = null; lifetimeEntry = null;
clearExistingStateTransforms();
hasHitObjectApplied = false; hasHitObjectApplied = false;
} }
@ -403,8 +408,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
double transformTime = HitObject.StartTime - InitialLifetimeOffset; double transformTime = HitObject.StartTime - InitialLifetimeOffset;
base.ApplyTransformsAt(double.MinValue, true); clearExistingStateTransforms();
base.ClearTransformsAfter(double.MinValue, true);
using (BeginAbsoluteSequence(transformTime, true)) using (BeginAbsoluteSequence(transformTime, true))
UpdateInitialTransforms(); UpdateInitialTransforms();
@ -432,6 +436,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
PlaySamples(); PlaySamples();
} }
private void clearExistingStateTransforms()
{
base.ApplyTransformsAt(double.MinValue, true);
base.ClearTransformsAfter(double.MinValue, true);
}
/// <summary> /// <summary>
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time. /// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience. /// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> 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 <see cref="DrawableHitObject"/> and may be tuned more aggressively if required. /// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>. /// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example). /// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
/// <para>
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
/// </para>
/// </remarks> /// </remarks>
protected virtual double InitialLifetimeOffset => 10000; protected virtual double InitialLifetimeOffset => 10000;

View File

@ -81,10 +81,18 @@ namespace osu.Game.Rulesets
value |= LegacyMods.HardRock; value |= LegacyMods.HardRock;
break; break;
case ModPerfect _:
value |= LegacyMods.Perfect;
break;
case ModSuddenDeath _: case ModSuddenDeath _:
value |= LegacyMods.SuddenDeath; value |= LegacyMods.SuddenDeath;
break; break;
case ModNightcore _:
value |= LegacyMods.Nightcore;
break;
case ModDoubleTime _: case ModDoubleTime _:
value |= LegacyMods.DoubleTime; value |= LegacyMods.DoubleTime;
break; break;
@ -100,6 +108,14 @@ namespace osu.Game.Rulesets
case ModFlashlight _: case ModFlashlight _:
value |= LegacyMods.Flashlight; value |= LegacyMods.Flashlight;
break; break;
case ModCinema _:
value |= LegacyMods.Cinema;
break;
case ModAutoplay _:
value |= LegacyMods.Autoplay;
break;
} }
} }

View File

@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI
{ {
Debug.Assert(!drawableMap.ContainsKey(entry)); Debug.Assert(!drawableMap.ContainsKey(entry));
var drawable = pooledObjectProvider.GetPooledDrawableRepresentation(entry.HitObject); var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject);
if (drawable == null) if (drawable == null)
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");

View File

@ -218,9 +218,6 @@ namespace osu.Game.Rulesets.UI
#region Pooling support #region Pooling support
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider parentPooledObjectProvider { get; set; }
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>(); private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
/// <summary> /// <summary>
@ -320,10 +317,7 @@ namespace osu.Game.Rulesets.UI
} }
} }
if (pool == null) return (DrawableHitObject)pool?.Get(d =>
return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject);
return (DrawableHitObject)pool.Get(d =>
{ {
var dho = (DrawableHitObject)d; var dho = (DrawableHitObject)d;

View File

@ -342,8 +342,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
if (!blueprint.IsHovered) continue; if (!blueprint.IsHovered) continue;
if (SelectionHandler.HandleSelectionRequested(blueprint, e)) return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e);
return clickSelectionBegan = true;
} }
return false; return false;
@ -457,6 +456,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprint == null) if (movementBlueprint == null)
return false; return false;
if (snapProvider == null)
return true;
Debug.Assert(movementBlueprintOriginalPosition != null); Debug.Assert(movementBlueprintOriginalPosition != null);
HitObject draggedObject = movementBlueprint.HitObject; HitObject draggedObject = movementBlueprint.HitObject;

View File

@ -110,7 +110,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
protected virtual void OnOperationBegan() protected virtual void OnOperationBegan()
{ {
ChangeHandler.BeginChange(); ChangeHandler?.BeginChange();
} }
/// <summary> /// <summary>
@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
protected virtual void OnOperationEnded() protected virtual void OnOperationEnded()
{ {
ChangeHandler.EndChange(); ChangeHandler?.EndChange();
} }
#region User Input Handling #region User Input Handling

View File

@ -96,7 +96,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (lastDragEvent != null) if (lastDragEvent != null)
OnDrag(lastDragEvent); OnDrag(lastDragEvent);
if (Composer != null) if (Composer != null && timeline != null)
{ {
Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2;
Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2;

View File

@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
table.ControlGroups = controlPointGroups; table.ControlGroups = controlPointGroups;
changeHandler.SaveState(); changeHandler?.SaveState();
}, true); }, true);
} }

View File

@ -59,6 +59,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{ {
scheduledFilterUpdate?.Cancel(); scheduledFilterUpdate?.Cancel();
if (filter == null)
return;
filter.Value = new FilterCriteria filter.Value = new FilterCriteria
{ {
SearchString = Search.Current.Value ?? string.Empty, SearchString = Search.Current.Value ?? string.Empty,

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -35,7 +36,8 @@ using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class Spectator : OsuScreen [Cached(typeof(IPreviewTrackOwner))]
public class Spectator : OsuScreen, IPreviewTrackOwner
{ {
private readonly User targetUser; private readonly User targetUser;
@ -62,6 +64,9 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private Score score; private Score score;
private readonly object scoreLock = new object(); private readonly object scoreLock = new object();
@ -275,6 +280,7 @@ namespace osu.Game.Screens.Play
{ {
watchButton.Enabled.Value = false; watchButton.Enabled.Value = false;
beatmapPanelContainer.Clear(); beatmapPanelContainer.Clear();
previewTrackManager.StopAnyPlaying(this);
} }
private void attemptStart() private void attemptStart()
@ -326,7 +332,6 @@ namespace osu.Game.Screens.Play
{ {
if (state?.BeatmapID == null) if (state?.BeatmapID == null)
{ {
beatmapPanelContainer.Clear();
onlineBeatmap = null; onlineBeatmap = null;
return; return;
} }
@ -359,6 +364,12 @@ namespace osu.Game.Screens.Play
beatmaps.Download(onlineBeatmap); beatmaps.Download(onlineBeatmap);
} }
public override bool OnExiting(IScreen next)
{
previewTrackManager.StopAnyPlaying(this);
return base.OnExiting(next);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -48,16 +48,6 @@ namespace osu.Game.Skinning
this.audio = audio; this.audio = audio;
this.legacyDefaultResources = legacyDefaultResources; 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); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkin.ValueChanged += skin => CurrentSkin.ValueChanged += skin =>
{ {

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps
/// <returns></returns> /// <returns></returns>
protected abstract Ruleset CreateRuleset(); protected abstract Ruleset CreateRuleset();
protected void Test(LegacyMods legacyMods, Type[] expectedMods) protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
{ {
var ruleset = CreateRuleset(); var ruleset = CreateRuleset();
var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList(); var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList();
@ -31,5 +31,15 @@ namespace osu.Game.Tests.Beatmaps
Assert.IsNotNull(mods.SingleOrDefault(mod => mod.GetType() == modType)); 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);
}
} }
} }