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.Android.props b/osu.Android.props
index 6e3d5eec1f..6dab6edc5e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b17611f23f..0feab9a717 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -139,7 +139,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
- desktopWindow.CursorState.Value |= CursorState.Hidden;
+ desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
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.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 385d8ed7fa..89063319d6 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -18,71 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests
base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
+ AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
+ AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
- private Drawable createDrawableTinyDroplet()
+ private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
+ setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
+
+ private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
+
+ private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
+
+ private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
{
- var droplet = new TestCatchTinyDroplet
- {
- Scale = 1.5f,
- };
+ var hitObject = d.HitObject;
+ hitObject.StartTime = 1000000000000;
+ hitObject.Scale = 1.5f;
- return new DrawableTinyDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
+ if (hyperdash)
+ hitObject.HyperDashTarget = new Banana();
- private Drawable createDrawableDroplet(bool hyperdash = false)
- {
- var droplet = new TestCatchDroplet
+ d.Anchor = Anchor.Centre;
+ d.RelativePositionAxes = Axes.None;
+ d.Position = Vector2.Zero;
+ d.HitObjectApplied += _ =>
{
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
- {
- Fruit fruit = new TestCatchFruit(rep)
- {
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableFruit(fruit)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
+ d.LifetimeStart = double.NegativeInfinity;
+ d.LifetimeEnd = double.PositiveInfinity;
};
+ return d;
}
public class TestCatchFruit : Fruit
@@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
- StartTime = 1000000000000;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
-
- public class TestCatchDroplet : Droplet
- {
- public TestCatchDroplet()
- {
- StartTime = 1000000000000;
- }
- }
-
- public class TestCatchTinyDroplet : TinyDroplet
- {
- public TestCatchTinyDroplet()
- {
- StartTime = 1000000000000;
- }
- }
}
}
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.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index d24c81dac6..96444fd316 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
+using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
///
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
SetContents(() => new FillFlowContainer
{
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
},
}
});
- }
+ });
protected abstract DrawableManiaHitObject CreateHitObject();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index a4d4ec50f8..dcb25f21ba 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (hitWindows.IsHitResultAllowed(result))
{
AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
+ {
+ Type = result
+ }, null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
index 9c4c2b3d5b..e88ff8e2ac 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
@@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
});
}
+ [Test]
+ public void TestFadeOnMiss()
+ {
+ AddStep("miss tick", () =>
+ {
+ foreach (var holdNote in holdNotes)
+ holdNote.ChildrenOfType().First().MissForcefully();
+ });
+ }
+
+ private IEnumerable holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType());
+
protected override DrawableManiaHitObject CreateHitObject()
{
var note = new HoldNote { Duration = 1000 };
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.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 59899637f9..a64cc6dc67 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
///
- /// Whether the hold note has been released too early and shouldn't give full score for the release.
+ /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
///
- public bool HasBroken { get; private set; }
+ public double? HoldBrokenTime { get; private set; }
///
/// Whether the hold note has been released potentially without having caused a break.
@@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
if (Tail.Judged && !Tail.IsHit)
- HasBroken = true;
+ HoldBrokenTime = Time.Current;
}
public bool OnPressed(ManiaAction action)
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
- HasBroken = true;
+ HoldBrokenTime = Time.Current;
releaseTime = Time.Current;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index c780c0836e..a4029e7893 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
- if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
+ if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
result = HitResult.Meh;
r.Type = result;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index f265419aa0..98931dceed 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteTick : DrawableManiaHitObject
{
- public override bool DisplayResult => false;
-
///
/// References the time at which the user started holding the hold note.
///
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index c0f0fcb4af..8902d82f33 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyBodyPiece : LegacyManiaColumnElement
{
+ private DrawableHoldNote holdNote;
+
private readonly IBindable direction = new Bindable();
private readonly IBindable isHitting = new Bindable();
+ ///
+ /// Stores the start time of the fade animation that plays when any of the nested
+ /// hitobjects of the hold note are missed.
+ ///
+ private readonly Bindable missFadeTime = new Bindable();
+
[CanBeNull]
private Drawable bodySprite;
@@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{
+ holdNote = (DrawableHoldNote)drawableObject;
+
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
@@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(onDirectionChanged, true);
-
- var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHitting);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true);
+ missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
+
+ holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
+ applyCustomUpdateState(holdNote, holdNote.State.Value);
+ }
+
+ private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
+ {
+ // ensure that the hold note is also faded out when the head/tail/any tick is missed.
+ if (state == ArmedState.Miss)
+ missFadeTime.Value ??= hitObject.HitStateUpdateTime;
}
private void onIsHittingChanged(ValueChangedEvent isHitting)
@@ -158,10 +183,38 @@ namespace osu.Game.Rulesets.Mania.Skinning
}
}
+ private void onMissFadeTimeChanged(ValueChangedEvent missFadeTimeChange)
+ {
+ if (missFadeTimeChange.NewValue == null)
+ return;
+
+ // this update could come from any nested object of the hold note (or even from an input).
+ // make sure the transforms are consistent across all affected parts.
+ using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
+ {
+ // colour and duration matches stable
+ // transforms not applied to entire hold note in order to not affect hit lighting
+ const double fade_duration = 60;
+
+ holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
+ holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
+ bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ missFadeTime.Value ??= holdNote.HoldBrokenTime;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ if (holdNote != null)
+ holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
+
lightContainer?.Expire();
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index c28a1c13d8..9aabcc6699 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
- if (!result.IsHit || !DisplayJudgements.Value)
+ if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index d99f6cb8d3..a3dcd0e57f 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -1,10 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -19,22 +20,52 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
- [BackgroundDependencyLoader]
- private void load()
+ protected override void ApplyMissAnimations()
{
- if (JudgementText != null)
- JudgementText.Font = JudgementText.Font.With(size: 25);
- }
+ if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
+ {
+ // this is temporary logic until mania's skin transformer returns IAnimatableJudgements
+ JudgementBody.ScaleTo(1.6f);
+ JudgementBody.ScaleTo(1, 100, Easing.In);
- protected override double FadeInDuration => 50;
+ JudgementBody.MoveTo(Vector2.Zero);
+ JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ JudgementBody.RotateTo(0);
+ JudgementBody.RotateTo(40, 800, Easing.InQuint);
+ JudgementBody.FadeOutFromOne(800);
+
+ LifetimeEnd = JudgementBody.LatestTransformEndTime;
+ }
+
+ base.ApplyMissAnimations();
+ }
protected override void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
- JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
- this.Delay(FadeInDuration).FadeOut(200);
+ JudgementBody.Delay(50)
+ .ScaleTo(0.75f, 250)
+ .FadeOut(200);
+ }
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
+
+ private class DefaultManiaJudgementPiece : DefaultJudgementPiece
+ {
+ public DefaultManiaJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ JudgementText.Font = JudgementText.Font.With(size: 25);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index e7a2de266d..3d7960ffe3 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
+ // Tick judgements should not display text.
+ if (judgedObject is DrawableHoldNoteTick)
+ return;
+
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{
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/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/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 646f12f710..e4158d8f07 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1));
- AddAssert("hit lighting hidden",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
+ AddAssert("hit lighting has no transforms", () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any()));
+ AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
}
[Test]
@@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body not immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
- AddAssert("hit lighting shown",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0));
+ AddUntilStep("hit lighting shown", () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Alpha > 0));
}
private void showResult(HitResult result)
@@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Children = new Drawable[]
{
pool,
- pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
+ pool.Get(j => j.Apply(new JudgementResult(new HitObject
+ {
+ StartTime = Time.Current
+ }, new Judgement())
+ {
+ Type = result,
+ }, null)).With(j =>
{
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestDrawableOsuJudgement : DrawableOsuJudgement
{
public new SkinnableSprite Lighting => base.Lighting;
- public new Container JudgementBody => base.JudgementBody;
+ public new SkinnableDrawable JudgementBody => base.JudgementBody;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
index 6c077eb214..fe67b63252 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
addMultipleObjectsStep();
- AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
+ AddStep("move hitobject", () =>
+ {
+ var manualClock = new ManualClock();
+ followPointRenderer.Clock = new FramedClock(manualClock);
+
+ manualClock.CurrentTime = getObject(1).HitObject.StartTime;
+ followPointRenderer.UpdateSubTree();
+
+ getObject(2).HitObject.Position = new Vector2(300, 100);
+ });
assertGroups();
+ assertDirections();
}
[TestCase(0, 0)] // Start -> Start
@@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertGroups()
{
- AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
+ AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () =>
{
for (int i = 0; i < hitObjectContainer.Count; i++)
@@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
- if (getGroup(i).Start != expectedStart.HitObject)
+ if (getEntry(i).Start != expectedStart.HitObject)
throw new AssertionException($"Object {i} expected to be the start of group {i}.");
- if (getGroup(i).End != expectedEnd?.HitObject)
+ if (getEntry(i).End != expectedEnd?.HitObject)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
}
@@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
if (expectedEnd == null)
continue;
+ var manualClock = new ManualClock();
+ followPointRenderer.Clock = new FramedClock(manualClock);
+
+ manualClock.CurrentTime = expectedStart.HitObject.StartTime;
+ followPointRenderer.UpdateSubTree();
+
var points = getGroup(i).ChildrenOfType().ToArray();
if (points.Length == 0)
continue;
@@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
- private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
+ private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index];
+
+ private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType().Single(c => c.Entry == getEntry(index));
private class TestHitObjectContainer : Container
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 596bc06c68..1278a0ff2d 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -1,17 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osuTK;
-using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
+ {
+ var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
+
+ var playfield = new TestOsuPlayfield();
+ playfield.Add(drawable);
+ return playfield;
+ }
+
+ private Drawable testStream(float circleSize, bool auto = false)
+ {
+ var playfield = new TestOsuPlayfield();
+
+ Vector2 pos = new Vector2(-250, 0);
+
+ for (int i = 0; i <= 1000; i += 100)
+ {
+ playfield.Add(createSingle(circleSize, auto, i, pos));
+ pos.X += 50;
+ }
+
+ return playfield;
+ }
+
+ private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
{
positionOffset ??= Vector2.Zero;
var circle = new HitCircle
{
StartTime = Time.Current + 1000 + timeOffset,
- Position = positionOffset.Value,
+ Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
};
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
@@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
-
return drawable;
}
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
{
- Anchor = Anchor.Centre,
Depth = depthIndex++
};
- private Drawable testStream(float circleSize, bool auto = false)
- {
- var container = new Container { RelativeSizeAxes = Axes.Both };
-
- Vector2 pos = new Vector2(-250, 0);
-
- for (int i = 0; i <= 1000; i += 100)
- {
- container.Add(testSingle(circleSize, auto, i, pos));
- pos.X += 50;
- }
-
- return container;
- }
-
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
@@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests
base.CheckForResult(userTriggered, timeOffset);
}
}
+
+ protected class TestOsuPlayfield : OsuPlayfield
+ {
+ public TestOsuPlayfield()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
index d692be89b2..7e973d0971 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
@@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Diagnostics;
+using osu.Framework.Threading;
using osu.Framework.Utils;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
@@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneShaking : TestSceneHitCircle
{
+ private readonly List scheduledTasks = new List();
+
+ protected override IBeatmap CreateBeatmapForSkinProvider()
+ {
+ // best way to run cleanup before a new step is run
+ foreach (var task in scheduledTasks)
+ task.Cancel();
+
+ scheduledTasks.Clear();
+
+ return base.CreateBeatmapForSkinProvider();
+ }
+
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
@@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
- Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay);
+ scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
return drawableHitObject;
}
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/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/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
index ba1d35c35c..eb7011e8b0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Path path;
private readonly Slider slider;
- private readonly int controlPointIndex;
+ public int ControlPointIndex { get; set; }
private IBindable sliderPosition;
private IBindable pathVersion;
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
- this.controlPointIndex = controlPointIndex;
+ ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
- int nextIndex = controlPointIndex + 1;
+ int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 17541866ec..ce5dc4855e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
+
internal readonly Container Pieces;
internal readonly Container Connections;
@@ -66,6 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ // If inserting in the path (not appending),
+ // update indices of existing connections after insert location
+ if (e.NewStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.NewStartingIndex)
+ connection.ControlPointIndex += e.NewItems.Count;
+ }
+ }
+
for (int i = 0; i < e.NewItems.Count; i++)
{
var point = (PathControlPoint)e.NewItems[i];
@@ -88,6 +102,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Connections.RemoveAll(c => c.ControlPoint == point);
}
+ // If removing before the end of the path,
+ // update indices of connections after remove location
+ if (e.OldStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.OldStartingIndex)
+ connection.ControlPointIndex -= e.OldItems.Count;
+ }
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 7ae4f387ca..d592e129d9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
+ private readonly BindableList controlPoints = new BindableList();
+ private readonly IBindable pathVersion = new Bindable();
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
}
- private IBindable pathVersion;
-
protected override void LoadComplete()
{
base.LoadComplete();
- pathVersion = HitObject.Path.Version.GetBoundCopy();
+ controlPoints.BindTo(HitObject.Path.ControlPoints);
+
+ pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
@@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
- private BindableList controlPoints => HitObject.Path.ControlPoints;
-
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
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/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index a981648444..3e2ab65bb2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning;
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
///
/// A single follow point positioned between two adjacent s.
///
- public class FollowPoint : Container, IAnimationTimeReference
+ public class FollowPoint : PoolableDrawable, IAnimationTimeReference
{
private const float width = 8;
@@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
Origin = Anchor.Centre;
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
+ InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 3a9e19b361..700d96eff3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Diagnostics;
-using JetBrains.Annotations;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
using osuTK;
@@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
///
/// Visualises the s between two s.
///
- public class FollowPointConnection : CompositeDrawable
+ public class FollowPointConnection : PoolableDrawable
{
// Todo: These shouldn't be constants
- private const int spacing = 32;
- private const double preempt = 800;
+ public const int SPACING = 32;
+ public const double PREEMPT = 800;
- public override bool RemoveWhenNotAlive => false;
+ public FollowPointLifetimeEntry Entry;
+ public DrawablePool Pool;
- ///
- /// The start time of .
- ///
- public readonly Bindable StartTime = new BindableDouble();
-
- ///
- /// The which s will exit from.
- ///
- [NotNull]
- public readonly OsuHitObject Start;
-
- ///
- /// Creates a new .
- ///
- /// The which s will exit from.
- public FollowPointConnection([NotNull] OsuHitObject start)
+ protected override void PrepareForUse()
{
- Start = start;
+ base.PrepareForUse();
- RelativeSizeAxes = Axes.Both;
+ Entry.Invalidated += onEntryInvalidated;
- StartTime.BindTo(start.StartTimeBindable);
+ refreshPoints();
}
- protected override void LoadComplete()
+ protected override void FreeAfterUse()
{
- base.LoadComplete();
- bindEvents(Start);
+ base.FreeAfterUse();
+
+ Entry.Invalidated -= onEntryInvalidated;
+
+ // Return points to the pool.
+ ClearInternal(false);
+
+ Entry = null;
}
- private OsuHitObject end;
+ private void onEntryInvalidated() => refreshPoints();
- ///
- /// The which s will enter.
- ///
- [CanBeNull]
- public OsuHitObject End
+ private void refreshPoints()
{
- get => end;
- set
- {
- end = value;
+ ClearInternal(false);
- if (end != null)
- bindEvents(end);
+ OsuHitObject start = Entry.Start;
+ OsuHitObject end = Entry.End;
- if (IsLoaded)
- scheduleRefresh();
- else
- refresh();
- }
- }
+ double startTime = start.GetEndTime();
- private void bindEvents(OsuHitObject obj)
- {
- obj.PositionBindable.BindValueChanged(_ => scheduleRefresh());
- obj.DefaultsApplied += _ => scheduleRefresh();
- }
-
- private void scheduleRefresh()
- {
- Scheduler.AddOnce(refresh);
- }
-
- private void refresh()
- {
- double startTime = Start.GetEndTime();
-
- LifetimeStart = startTime;
-
- if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
- {
- // ensure we always set a lifetime for full LifetimeManagementContainer benefits
- LifetimeEnd = LifetimeStart;
- return;
- }
-
- Vector2 startPosition = Start.StackedEndPosition;
- Vector2 endPosition = End.StackedPosition;
- double endTime = End.StartTime;
+ Vector2 startPosition = start.StackedEndPosition;
+ Vector2 endPosition = end.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
- double duration = endTime - startTime;
- double? firstTransformStartTime = null;
double finalTransformEndTime = startTime;
- int point = 0;
-
- ClearInternal();
-
- for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
+ for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
{
float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
- double fadeOutTime = startTime + fraction * duration;
- double fadeInTime = fadeOutTime - preempt;
+
+ GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
FollowPoint fp;
- AddInternal(fp = new FollowPoint());
-
- Debug.Assert(End != null);
+ AddInternal(fp = Pool.Get());
+ fp.ClearTransforms();
fp.Position = pointStartPosition;
fp.Rotation = rotation;
fp.Alpha = 0;
- fp.Scale = new Vector2(1.5f * End.Scale);
-
- firstTransformStartTime ??= fadeInTime;
+ fp.Scale = new Vector2(1.5f * end.Scale);
fp.AnimationStartTime = fadeInTime;
using (fp.BeginAbsoluteSequence(fadeInTime))
{
- fp.FadeIn(End.TimeFadeIn);
- fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out);
- fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out);
- fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn);
+ fp.FadeIn(end.TimeFadeIn);
+ fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
+ fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
+ fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn);
- finalTransformEndTime = fadeOutTime + End.TimeFadeIn;
+ finalTransformEndTime = fadeOutTime + end.TimeFadeIn;
}
-
- point++;
}
- int excessPoints = InternalChildren.Count - point;
- for (int i = 0; i < excessPoints; i++)
- RemoveInternal(InternalChildren[^1]);
-
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
- LifetimeStart = firstTransformStartTime ?? startTime;
- LifetimeEnd = finalTransformEndTime;
+ Entry.LifetimeEnd = finalTransformEndTime;
+ }
+
+ ///
+ /// Computes the fade time of follow point positioned between two hitobjects.
+ ///
+ /// The first , where follow points should originate from.
+ /// The second , which follow points should target.
+ /// The fractional distance along and at which the follow point is to be located.
+ /// The fade-in time of the follow point/
+ /// The fade-out time of the follow point.
+ public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime)
+ {
+ double startTime = start.GetEndTime();
+ double duration = end.StartTime - startTime;
+
+ fadeOutTime = startTime + fraction * duration;
+ fadeInTime = fadeOutTime - PREEMPT;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
new file mode 100644
index 0000000000..a167cb2f0f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
@@ -0,0 +1,98 @@
+// 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.Bindables;
+using osu.Framework.Graphics.Performance;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
+{
+ public class FollowPointLifetimeEntry : LifetimeEntry
+ {
+ public event Action Invalidated;
+ public readonly OsuHitObject Start;
+
+ public FollowPointLifetimeEntry(OsuHitObject start)
+ {
+ Start = start;
+ LifetimeStart = Start.StartTime;
+
+ bindEvents();
+ }
+
+ private OsuHitObject end;
+
+ public OsuHitObject End
+ {
+ get => end;
+ set
+ {
+ UnbindEvents();
+
+ end = value;
+
+ bindEvents();
+
+ refreshLifetimes();
+ }
+ }
+
+ private void bindEvents()
+ {
+ UnbindEvents();
+
+ // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
+ Start.DefaultsApplied += onDefaultsApplied;
+ Start.PositionBindable.ValueChanged += onPositionChanged;
+
+ if (End != null)
+ {
+ End.DefaultsApplied += onDefaultsApplied;
+ End.PositionBindable.ValueChanged += onPositionChanged;
+ }
+ }
+
+ public void UnbindEvents()
+ {
+ if (Start != null)
+ {
+ Start.DefaultsApplied -= onDefaultsApplied;
+ Start.PositionBindable.ValueChanged -= onPositionChanged;
+ }
+
+ if (End != null)
+ {
+ End.DefaultsApplied -= onDefaultsApplied;
+ End.PositionBindable.ValueChanged -= onPositionChanged;
+ }
+ }
+
+ private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
+
+ private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes();
+
+ private void refreshLifetimes()
+ {
+ if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
+ {
+ LifetimeEnd = LifetimeStart;
+ return;
+ }
+
+ Vector2 startPosition = Start.StackedEndPosition;
+ Vector2 endPosition = End.StackedPosition;
+ Vector2 distanceVector = endPosition - startPosition;
+
+ // The lifetime start will match the fade-in time of the first follow point.
+ float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
+ FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
+
+ LifetimeStart = fadeInTime;
+ LifetimeEnd = double.MaxValue; // This will be set by the connection.
+
+ Invalidated?.Invoke();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index be1392d7c3..3e85e528e8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -2,53 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
///
/// Visualises connections between s.
///
- public class FollowPointRenderer : LifetimeManagementContainer
+ public class FollowPointRenderer : CompositeDrawable
{
- ///
- /// All the s contained by this .
- ///
- internal IReadOnlyList Connections => connections;
-
- private readonly List connections = new List();
-
public override bool RemoveCompletedTransforms => false;
- ///
- /// Adds the s around an .
- /// This includes s leading into , and s exiting .
- ///
- /// The to add s for.
- public void AddFollowPoints(OsuHitObject hitObject)
- => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
+ public IReadOnlyList Entries => lifetimeEntries;
- ///
- /// Removes the s around an .
- /// This includes s leading into , and s exiting .
- ///
- /// The to remove s for.
- public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
+ private DrawablePool connectionPool;
+ private DrawablePool pointPool;
- ///
- /// Adds a to this .
- ///
- /// The to add.
- /// The index of in .
- private void addConnection(FollowPointConnection connection)
+ private readonly List lifetimeEntries = new List();
+ private readonly Dictionary connectionsInUse = new Dictionary();
+ private readonly Dictionary startTimeMap = new Dictionary();
+ private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
+
+ public FollowPointRenderer()
{
- // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
- int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) =>
+ lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
+ lifetimeManager.EntryBecameDead += onEntryBecameDead;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
{
- int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
+ connectionPool = new DrawablePoolNoLifetime(1, 200),
+ pointPool = new DrawablePoolNoLifetime(50, 1000)
+ };
+ }
+
+ public void AddFollowPoints(OsuHitObject hitObject)
+ {
+ addEntry(hitObject);
+
+ var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
+ startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
+ startTimeMap[hitObject] = startTimeBindable;
+ }
+
+ public void RemoveFollowPoints(OsuHitObject hitObject)
+ {
+ removeEntry(hitObject);
+
+ startTimeMap[hitObject].UnbindAll();
+ startTimeMap.Remove(hitObject);
+ }
+
+ private void addEntry(OsuHitObject hitObject)
+ {
+ var newEntry = new FollowPointLifetimeEntry(hitObject);
+
+ var index = lifetimeEntries.AddInPlace(newEntry, Comparer.Create((e1, e2) =>
+ {
+ int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
if (comp != 0)
return comp;
@@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
return -1;
}));
- if (index < connections.Count - 1)
+ if (index < lifetimeEntries.Count - 1)
{
// Update the connection's end point to the next connection's start point
// h1 -> -> -> h2
// connection nextGroup
- FollowPointConnection nextConnection = connections[index + 1];
- connection.End = nextConnection.Start;
+ FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
+ newEntry.End = nextEntry.Start;
}
else
{
// The end point may be non-null during re-ordering
- connection.End = null;
+ newEntry.End = null;
}
if (index > 0)
@@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2
// prevGroup connection
- FollowPointConnection previousConnection = connections[index - 1];
- previousConnection.End = connection.Start;
+ FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
+ previousEntry.End = newEntry.Start;
}
- AddInternal(connection);
+ lifetimeManager.AddEntry(newEntry);
}
- ///
- /// Removes a from this .
- ///
- /// The to remove.
- /// Whether was removed.
- private void removeGroup(FollowPointConnection connection)
+ private void removeEntry(OsuHitObject hitObject)
{
- RemoveInternal(connection);
+ int index = lifetimeEntries.FindIndex(e => e.Start == hitObject);
- int index = connections.IndexOf(connection);
+ var entry = lifetimeEntries[index];
+ entry.UnbindEvents();
+
+ lifetimeEntries.RemoveAt(index);
+ lifetimeManager.RemoveEntry(entry);
if (index > 0)
{
@@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection
- FollowPointConnection previousConnection = connections[index - 1];
- previousConnection.End = connection.End;
+ FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
+ previousEntry.End = entry.End;
}
-
- connections.Remove(connection);
}
- private void onStartTimeChanged(FollowPointConnection connection)
+ protected override bool CheckChildrenLife()
{
- // Naive but can be improved if performance becomes an issue
- removeGroup(connection);
- addConnection(connection);
+ bool anyAliveChanged = base.CheckChildrenLife();
+ anyAliveChanged |= lifetimeManager.Update(Time.Current);
+ return anyAliveChanged;
+ }
+
+ private void onEntryBecameAlive(LifetimeEntry entry)
+ {
+ var connection = connectionPool.Get(c =>
+ {
+ c.Entry = (FollowPointLifetimeEntry)entry;
+ c.Pool = pointPool;
+ });
+
+ connectionsInUse[entry] = connection;
+
+ AddInternal(connection);
+ }
+
+ private void onEntryBecameDead(LifetimeEntry entry)
+ {
+ RemoveInternal(connectionsInUse[entry]);
+ connectionsInUse.Remove(entry);
+ }
+
+ private void onStartTimeChanged(OsuHitObject hitObject)
+ {
+ removeEntry(hitObject);
+ addEntry(hitObject);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ foreach (var entry in lifetimeEntries)
+ entry.UnbindEvents();
+ lifetimeEntries.Clear();
+ }
+
+ private class DrawablePoolNoLifetime : DrawablePool
+ where T : PoolableDrawable, new()
+ {
+ public override bool RemoveWhenNotAlive => false;
+
+ public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
+ : base(initialSize, maximumSize)
+ {
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 2e63160d36..d1ceca6d8f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.Delay(800).FadeOut();
break;
}
+
+ Expire();
}
public Drawable ProxiedLayer => ApproachCircle;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index bcaf73d34f..a26db06ede 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindTo(HitObject.PositionBindable);
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
+
+ // Manually set to reduce the number of future alive objects to a bare minimum.
+ LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
+
+ // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
+ // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
+ LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
}
protected override void OnFree(HitObject hitObject)
@@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // Manually set to reduce the number of future alive objects to a bare minimum.
- LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
- }
-
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 98898ce1b4..13f5960bd4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -4,9 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
-using osuTK;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved]
private OsuConfigManager config { get; set; }
- public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
- : base(result, judgedObject)
- {
- }
-
- public DrawableOsuJudgement()
- {
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -39,48 +30,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
- public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
- {
- base.Apply(result, judgedObject);
-
- if (judgedObject?.HitObject is OsuHitObject osuObject)
- {
- Position = osuObject.StackedPosition;
- Scale = new Vector2(osuObject.Scale);
- }
- }
-
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
- }
- private double fadeOutDelay;
- protected override double FadeOutDelay => fadeOutDelay;
+ if (JudgedObject?.HitObject is OsuHitObject osuObject)
+ {
+ Position = osuObject.StackedPosition;
+ Scale = new Vector2(osuObject.Scale);
+ }
+ }
protected override void ApplyHitAnimations()
{
bool hitLightingEnabled = config.Get(OsuSetting.HitLighting);
- if (hitLightingEnabled)
- {
- JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
+ Lighting.Alpha = 0;
+ if (hitLightingEnabled && Lighting.Drawable != null)
+ {
+ // todo: this animation changes slightly based on new/old legacy skin versions.
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
- }
- else
- {
- JudgementBody.Alpha = 1;
+
+ // extend the lifetime to cover lighting fade
+ LifetimeEnd = Lighting.LatestTransformEndTime;
}
- fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
-
- JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
}
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
+
+ private class OsuJudgementPiece : DefaultJudgementPiece
+ {
+ public OsuJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ public override void PlayAnimation()
+ {
+ base.PlayAnimation();
+
+ if (Result != HitResult.Miss)
+ JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 04fc755da5..14c494d909 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -80,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);
@@ -192,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- Body.FadeInFromZero(HitObject.TimeFadeIn);
- }
-
public readonly Bindable Tracking = new Bindable();
protected override void Update()
@@ -244,7 +238,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;
}
@@ -264,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples();
}
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
@@ -288,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
- this.FadeOut(fade_out_time, Easing.OutQuint);
+ this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 824b8806e5..2a14a7c975 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,6 +26,8 @@ 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; }
@@ -153,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- this.FadeOut(160);
+ this.FadeOut(160).Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();
@@ -197,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;
@@ -244,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();
}
@@ -256,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/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
index c455c66e8d..d0e1055dce 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
@@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class CirclePiece : CompositeDrawable
{
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ private TrianglesPiece triangles;
+
public CirclePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures, DrawableHitObject drawableHitObject)
+ private void load(TextureStore textures)
{
InternalChildren = new Drawable[]
{
@@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
- new TrianglesPiece(drawableHitObject.GetHashCode())
+ triangles = new TrianglesPiece
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Alpha = 0.5f,
}
};
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableObject);
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ if (obj.HitObject == null)
+ return;
+
+ triangles.Reset((int)obj.HitObject.StartTime);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject != null)
+ drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}
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/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
index 6381ddca69..09299a3622 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
@@ -1,14 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ExplodePiece : Container
{
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ private TrianglesPiece triangles;
+
public ExplodePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Blending = BlendingParameters.Additive;
Alpha = 0;
+ }
- Child = new TrianglesPiece
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = triangles = new TrianglesPiece
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
};
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableObject);
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ if (obj.HitObject == null)
+ return;
+
+ triangles.Reset((int)obj.HitObject.StartTime);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject != null)
+ drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}
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/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
index 6cdb0d3df3..53dc7ecea3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
@@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class TrianglesPiece : Triangles
{
- protected override bool ExpireOffScreenTriangles => false;
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
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/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index c816502d61..3bd150c4d3 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -20,7 +19,6 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
-using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
@@ -40,44 +38,21 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
- private readonly Bindable playfieldBorderStyle = new BindableBool();
-
private readonly IDictionary> poolDictionary = new Dictionary>();
+ private readonly Container judgementAboveHitObjectLayer;
+
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
- playfieldBorder = new PlayfieldBorder
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 3
- },
- spinnerProxies = new ProxyContainer
- {
- RelativeSizeAxes = Axes.Both
- },
- followPoints = new FollowPointRenderer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 2,
- },
- judgementLayer = new JudgementContainer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 1,
- },
- // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
- // Todo: Remove when hitobjects are properly pooled
- new SkinProvidingContainer(null)
- {
- Child = HitObjectContainer,
- },
- approachCircles = new ProxyContainer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = -1,
- },
+ playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
+ spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
+ followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
+ judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
+ HitObjectContainer,
+ judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
+ approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
@@ -86,13 +61,18 @@ namespace osu.Game.Rulesets.Osu.UI
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
- poolDictionary.Add(result, new DrawableJudgementPool(result));
+ poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
AddRangeInternal(poolDictionary.Values);
NewResult += onNewResult;
}
+ private void onJudgmentLoaded(DrawableOsuJudgement judgement)
+ {
+ judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
+ }
+
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
@@ -178,11 +158,13 @@ namespace osu.Game.Rulesets.Osu.UI
private class DrawableJudgementPool : DrawablePool
{
private readonly HitResult result;
+ private readonly Action onLoaded;
- public DrawableJudgementPool(HitResult result)
+ public DrawableJudgementPool(HitResult result, Action onLoaded)
: base(10)
{
this.result = result;
+ this.onLoaded = onLoaded;
}
protected override DrawableOsuJudgement CreateNewDrawable()
@@ -192,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.UI
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
+ onLoaded?.Invoke(judgement);
+
return judgement;
}
}
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.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index cbfc5a8628..b5e35f88b5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -1,12 +1,9 @@
// 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.Objects.Drawables;
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Judgements;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- switch (Result.Type)
- {
- case HitResult.Ok:
- JudgementBody.Colour = colours.GreenLight;
- break;
-
- case HitResult.Great:
- JudgementBody.Colour = colours.BlueLight;
- break;
- }
- }
-
protected override void ApplyHitAnimations()
{
this.MoveToY(-100, 500);
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/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
new file mode 100644
index 0000000000..82095cb809
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [TestFixture]
+ public class TestSceneParticleExplosion : OsuTestScene
+ {
+ private ParticleExplosion explosion;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddStep("create initial", () =>
+ {
+ Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(400)
+ };
+ });
+
+ AddWaitStep("wait for playback", 5);
+
+ AddRepeatStep(@"restart animation", () =>
+ {
+ explosion.Restart();
+ }, 10);
+ }
+ }
+}
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 d009d805f0..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()
{
@@ -210,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);
@@ -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)
{
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/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index c85ad6d651..05d6930992 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -15,103 +14,93 @@ namespace osu.Game.Database
{
public class UserLookupCache : MemoryCachingComponent
{
- private readonly HashSet nextTaskIDs = new HashSet();
-
[Resolved]
private IAPIProvider api { get; set; }
- private readonly object taskAssignmentLock = new object();
-
- private Task> pendingRequest;
-
- ///
- /// Whether has already grabbed its IDs.
- ///
- private bool pendingRequestConsumedIDs;
-
public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
- {
- var users = await getQueryTaskForUser(lookup);
- return users.FirstOrDefault(u => u.Id == lookup);
- }
+ => await queryUser(lookup);
- ///
- /// Return the task responsible for fetching the provided user.
- /// This may be part of a larger batch lookup to reduce web requests.
- ///
- /// The user to lookup.
- /// The task responsible for the lookup.
- private Task> getQueryTaskForUser(int userId)
+ private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>();
+ private Task pendingRequestTask;
+ private readonly object taskAssignmentLock = new object();
+
+ private Task queryUser(int userId)
{
lock (taskAssignmentLock)
{
- nextTaskIDs.Add(userId);
+ var tcs = new TaskCompletionSource();
- // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it.
- if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50)
- return pendingRequest;
+ // Add to the queue.
+ pendingUserTasks.Enqueue((userId, tcs));
- return queueNextTask(nextLookup);
+ // Create a request task if there's not already one.
+ if (pendingRequestTask == null)
+ createNewTask();
+
+ return tcs.Task;
}
+ }
- List nextLookup()
+ private void performLookup()
+ {
+ // contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
+ var userTasks = new Dictionary>>();
+
+ // Grab at most 50 unique user IDs from the queue.
+ lock (taskAssignmentLock)
{
- int[] lookupItems;
-
- lock (taskAssignmentLock)
+ while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
{
- pendingRequestConsumedIDs = true;
- lookupItems = nextTaskIDs.ToArray();
- nextTaskIDs.Clear();
+ (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue();
- if (lookupItems.Length == 0)
+ // Perform a secondary check for existence, in case the user was queried in a previous batch.
+ if (CheckExists(next.id, out var existing))
+ next.task.SetResult(existing);
+ else
{
- queueNextTask(null);
- return new List();
+ if (userTasks.TryGetValue(next.id, out var tasks))
+ tasks.Add(next.task);
+ else
+ userTasks[next.id] = new List> { next.task };
}
}
-
- var request = new GetUsersRequest(lookupItems);
-
- // rather than queueing, we maintain our own single-threaded request stream.
- api.Perform(request);
-
- return request.Result?.Users;
}
- }
- ///
- /// Queues new work at the end of the current work tasks.
- /// Ensures the provided work is eventually run.
- ///
- /// The work to run. Can be null to signify the end of available work.
- /// The task tracking this work.
- private Task> queueNextTask(Func> work)
- {
+ // Query the users.
+ var request = new GetUsersRequest(userTasks.Keys.ToArray());
+
+ // rather than queueing, we maintain our own single-threaded request stream.
+ api.Perform(request);
+
+ // Create a new request task if there's still more users to query.
lock (taskAssignmentLock)
{
- if (work == null)
- {
- pendingRequest = null;
- pendingRequestConsumedIDs = false;
- }
- else if (pendingRequest == null)
- {
- // special case for the first request ever.
- pendingRequest = Task.Run(work);
- pendingRequestConsumedIDs = false;
- }
- else
- {
- // append the new request on to the last to be executed.
- pendingRequest = pendingRequest.ContinueWith(_ => work());
- pendingRequestConsumedIDs = false;
- }
+ pendingRequestTask = null;
+ if (pendingUserTasks.Count > 0)
+ createNewTask();
+ }
- return pendingRequest;
+ foreach (var user in request.Result.Users)
+ {
+ if (userTasks.TryGetValue(user.Id, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(user);
+
+ userTasks.Remove(user.Id);
+ }
+ }
+
+ // if any tasks remain which were not satisfied, return null.
+ foreach (var tasks in userTasks.Values)
+ {
+ foreach (var task in tasks)
+ task.SetResult(null);
}
}
+
+ private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
}
}
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 5b0fa44444..0e9382279a 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -60,6 +60,7 @@ namespace osu.Game.Graphics.Backgrounds
///
/// Whether we want to expire triangles as they exit our draw area completely.
///
+ [Obsolete("Unused.")] // Can be removed 20210518
protected virtual bool ExpireOffScreenTriangles => true;
///
@@ -86,12 +87,9 @@ namespace osu.Game.Graphics.Backgrounds
///
public float Velocity = 1;
- private readonly Random stableRandom;
-
- private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
-
private readonly SortedList parts = new SortedList(Comparer.Default);
+ private Random stableRandom;
private IShader shader;
private readonly Texture texture;
@@ -172,7 +170,20 @@ namespace osu.Game.Graphics.Backgrounds
}
}
- protected int AimCount;
+ ///
+ /// Clears and re-initialises triangles according to a given seed.
+ ///
+ /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.
+ public void Reset(int? seed = null)
+ {
+ if (seed != null)
+ stableRandom = new Random(seed.Value);
+
+ parts.Clear();
+ addTriangles(true);
+ }
+
+ protected int AimCount { get; private set; }
private void addTriangles(bool randomY)
{
@@ -226,6 +237,8 @@ namespace osu.Game.Graphics.Backgrounds
}
}
+ private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
+
protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this);
private class TrianglesDrawNode : DrawNode
diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs
new file mode 100644
index 0000000000..e0d2b50c55
--- /dev/null
+++ b/osu.Game/Graphics/ParticleExplosion.cs
@@ -0,0 +1,144 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
+
+namespace osu.Game.Graphics
+{
+ ///
+ /// An explosion of textured particles based on how osu-stable randomises the explosion pattern.
+ ///
+ public class ParticleExplosion : Sprite
+ {
+ private readonly int particleCount;
+ private readonly double duration;
+ private double startTime;
+
+ private readonly List parts = new List();
+
+ public ParticleExplosion(Texture texture, int particleCount, double duration)
+ {
+ Texture = texture;
+ this.particleCount = particleCount;
+ this.duration = duration;
+ Blending = BlendingParameters.Additive;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Restart();
+ }
+
+ ///
+ /// Restart the animation from the current point in time.
+ /// Supports transform time offset chaining.
+ ///
+ public void Restart()
+ {
+ startTime = TransformStartTime;
+ this.FadeOutFromOne(duration);
+
+ parts.Clear();
+ for (int i = 0; i < particleCount; i++)
+ parts.Add(new ParticlePart(duration));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this);
+
+ private class ParticleExplosionDrawNode : SpriteDrawNode
+ {
+ private readonly List parts = new List();
+
+ private ParticleExplosion source => (ParticleExplosion)Source;
+
+ private double startTime;
+ private double currentTime;
+ private Vector2 sourceSize;
+
+ public ParticleExplosionDrawNode(Sprite source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ parts.Clear();
+ parts.AddRange(source.parts);
+
+ sourceSize = source.Size;
+ startTime = source.startTime;
+ currentTime = source.Time.Current;
+ }
+
+ protected override void Blit(Action vertexAction)
+ {
+ var time = currentTime - startTime;
+
+ foreach (var p in parts)
+ {
+ Vector2 pos = p.PositionAtTime(time);
+ float alpha = p.AlphaAtTime(time);
+
+ var rect = new RectangleF(
+ pos.X * sourceSize.X - Texture.DisplayWidth / 2,
+ pos.Y * sourceSize.Y - Texture.DisplayHeight / 2,
+ Texture.DisplayWidth,
+ Texture.DisplayHeight);
+
+ // convert to screen space.
+ var quad = new Quad(
+ Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
+ );
+
+ DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
+ new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
+ null, TextureCoords);
+ }
+ }
+ }
+
+ private readonly struct ParticlePart
+ {
+ private readonly double duration;
+ private readonly float direction;
+ private readonly float distance;
+
+ public ParticlePart(double availableDuration)
+ {
+ distance = RNG.NextSingle(0.5f);
+ duration = RNG.NextDouble(availableDuration / 3, availableDuration);
+ direction = RNG.NextSingle(0, MathF.PI * 2);
+ }
+
+ public float AlphaAtTime(double time) => 1 - progressAtTime(time);
+
+ public Vector2 PositionAtTime(double time)
+ {
+ var travelledDistance = distance * progressAtTime(time);
+ return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction));
+ }
+
+ private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1);
+ }
+ }
+}
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/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index 4b7de8de90..780d7ea986 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{
- public class PaginatedBeatmapContainer : PaginatedContainer
+ public class PaginatedBeatmapContainer : PaginatedProfileSubsection
{
private const float panel_padding = 10f;
private readonly BeatmapSetType type;
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs
index 4445cdce51..9413d241fa 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs
@@ -32,7 +32,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
Child = chart = new ProfileLineChart()
};
- protected override void OnUserChanged(ValueChangedEvent e)
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ User.BindValueChanged(onUserChanged, true);
+ }
+
+ private void onUserChanged(ValueChangedEvent e)
{
var values = GetValues(e.NewValue);
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index 556f3139dd..e5bb1f8008 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -13,7 +13,7 @@ using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
- public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer
+ public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection
{
public PaginatedMostPlayedBeatmapContainer(Bindable user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
index 1b8bd23eb4..008d89d881 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
- public class PaginatedKudosuHistoryContainer : PaginatedContainer
+ public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection
{
public PaginatedKudosuHistoryContainer(Bindable user)
: base(user, missingText: "This user hasn't received any kudosu!")
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
similarity index 72%
rename from osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
rename to osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
index 7b66c3f51e..75c3ee8290 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
@@ -12,27 +12,35 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
- public abstract class PaginatedContainer : ProfileSubsection
+ public abstract class PaginatedProfileSubsection : ProfileSubsection
{
[Resolved]
private IAPIProvider api { get; set; }
+ [Resolved]
+ protected RulesetStore Rulesets { get; private set; }
+
protected int VisiblePages;
protected int ItemsPerPage;
-
- protected FillFlowContainer ItemsContainer;
+ protected FillFlowContainer ItemsContainer { get; private set; }
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
private ShowMoreButton moreButton;
+ private OsuSpriteText missing;
+ private readonly string missingText;
- protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
- : base(user, headerText, missingText, counterVisibilityState)
+ protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ : base(user, headerText, counterVisibilityState)
{
+ this.missingText = missingText;
}
protected override Drawable CreateContent() => new FillFlowContainer
@@ -56,10 +64,22 @@ namespace osu.Game.Overlays.Profile.Sections
Margin = new MarginPadding { Top = 10 },
Action = showMore,
},
+ missing = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 15),
+ Text = missingText,
+ Alpha = 0,
+ }
}
};
- protected override void OnUserChanged(ValueChangedEvent e)
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ User.BindValueChanged(onUserChanged, true);
+ }
+
+ private void onUserChanged(ValueChangedEvent e)
{
loadCancellation?.Cancel();
retrievalRequest?.Cancel();
@@ -93,15 +113,15 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide();
moreButton.IsLoading = false;
- if (!string.IsNullOrEmpty(Missing.Text))
- Missing.Show();
+ if (!string.IsNullOrEmpty(missingText))
+ missing.Show();
return;
}
LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables =>
{
- Missing.Hide();
+ missing.Hide();
moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
moreButton.IsLoading = false;
@@ -121,8 +141,9 @@ namespace osu.Game.Overlays.Profile.Sections
protected override void Dispose(bool isDisposing)
{
- base.Dispose(isDisposing);
retrievalRequest?.Cancel();
+ loadCancellation?.Cancel();
+ base.Dispose(isDisposing);
}
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
index 751b35e342..3e331f85e9 100644
--- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
+++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
@@ -5,9 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets;
using osu.Game.Users;
using JetBrains.Annotations;
@@ -17,26 +14,20 @@ namespace osu.Game.Overlays.Profile.Sections
{
protected readonly Bindable User = new Bindable();
- protected RulesetStore Rulesets { get; private set; }
-
- protected OsuSpriteText Missing { get; private set; }
-
private readonly string headerText;
- private readonly string missingText;
private readonly CounterVisibilityState counterVisibilityState;
private ProfileSubsectionHeader header;
- protected ProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{
this.headerText = headerText;
- this.missingText = missingText;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
}
[BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
+ private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -48,31 +39,13 @@ namespace osu.Game.Overlays.Profile.Sections
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
},
- CreateContent(),
- Missing = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 15),
- Text = missingText,
- Alpha = 0,
- },
+ CreateContent()
};
-
- Rulesets = rulesets;
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- User.BindValueChanged(OnUserChanged, true);
}
[NotNull]
protected abstract Drawable CreateContent();
- protected virtual void OnUserChanged(ValueChangedEvent e)
- {
- }
-
protected void SetCount(int value) => header.Current.Value = value;
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 1ce3079d52..53f6d375ca 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -14,7 +14,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
- public class PaginatedScoreContainer : PaginatedContainer
+ public class PaginatedScoreContainer : PaginatedProfileSubsection
{
private readonly ScoreType type;
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
index 08f39c6272..d7101a8147 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
@@ -13,7 +13,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
- public class PaginatedRecentActivityContainer : PaginatedContainer
+ public class PaginatedRecentActivityContainer : PaginatedProfileSubsection
{
public PaginatedRecentActivityContainer(Bindable user)
: base(user, missingText: "This user hasn't done anything notable recently!")
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/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
new file mode 100644
index 0000000000..d94346cb72
--- /dev/null
+++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Judgements
+{
+ public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public DefaultJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Font = OsuFont.Numeric.With(size: 20),
+ Scale = new Vector2(0.85f, 1),
+ }
+ };
+ }
+
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+
+ break;
+
+ default:
+ this.ScaleTo(0.9f);
+ this.ScaleTo(1, 500, Easing.OutElastic);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => null;
+ }
+}
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index d24c81536e..3063656aaf 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -1,20 +1,17 @@
// 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.Diagnostics;
using JetBrains.Annotations;
-using osuTK;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Judgements
{
@@ -25,25 +22,29 @@ namespace osu.Game.Rulesets.Judgements
{
private const float judgement_size = 128;
- [Resolved]
- private OsuColour colours { get; set; }
-
public JudgementResult Result { get; private set; }
+
public DrawableHitObject JudgedObject { get; private set; }
- protected Container JudgementBody { get; private set; }
- protected SpriteText JudgementText { get; private set; }
+ public override bool RemoveCompletedTransforms => false;
- private SkinnableDrawable bodyDrawable;
+ protected SkinnableDrawable JudgementBody { get; private set; }
+
+ private readonly Container aboveHitObjectsContent;
+
+ [Resolved]
+ private ISkinSource skinSource { get; set; }
///
/// Duration of initial fade in.
///
+ [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeInDuration => 100;
///
/// Duration to wait until fade out begins. Defaults to .
///
+ [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeOutDelay => FadeInDuration;
///
@@ -61,6 +62,12 @@ namespace osu.Game.Rulesets.Judgements
{
Size = new Vector2(judgement_size);
Origin = Anchor.Centre;
+
+ AddInternal(aboveHitObjectsContent = new Container
+ {
+ Depth = float.MinValue,
+ RelativeSizeAxes = Axes.Both
+ });
}
[BackgroundDependencyLoader]
@@ -69,15 +76,61 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
}
- protected virtual void ApplyHitAnimations()
- {
- JudgementBody.ScaleTo(0.9f);
- JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
+ public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
- this.Delay(FadeOutDelay).FadeOut(400);
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ skinSource.SourceChanged += onSkinChanged;
}
- public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
+ private void onSkinChanged()
+ {
+ // on a skin change, the child component will update but not get correctly triggered to play its animation.
+ // we need to trigger a reinitialisation to make things right.
+ currentDrawableType = null;
+
+ PrepareForUse();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (skinSource != null)
+ skinSource.SourceChanged -= onSkinChanged;
+ }
+
+ ///
+ /// Apply top-level animations to the current judgement when successfully hit.
+ /// If displaying components which require lifetime extensions, manually adjusting is required.
+ ///
+ ///
+ /// For animating the actual "default skin" judgement itself, it is recommended to use .
+ /// This allows applying animations which don't affect custom skins.
+ ///
+ protected virtual void ApplyHitAnimations()
+ {
+ }
+
+ ///
+ /// Apply top-level animations to the current judgement when missed.
+ /// If displaying components which require lifetime extensions, manually adjusting is required.
+ ///
+ ///
+ /// For animating the actual "default skin" judgement itself, it is recommended to use .
+ /// This allows applying animations which don't affect custom skins.
+ ///
+ protected virtual void ApplyMissAnimations()
+ {
+ }
+
+ ///
+ /// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool.
+ ///
+ /// The applicable judgement.
+ /// The drawable object.
+ public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
{
Result = result;
JudgedObject = judgedObject;
@@ -91,34 +144,46 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
- bodyDrawable.ResetAnimation();
+ runAnimation();
+ }
- this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
- JudgementBody.ScaleTo(1);
- JudgementBody.RotateTo(0);
- JudgementBody.MoveTo(Vector2.Zero);
+ private void runAnimation()
+ {
+ ClearTransforms(true);
+ LifetimeStart = Result.TimeAbsolute;
- switch (Result.Type)
+ using (BeginAbsoluteSequence(Result.TimeAbsolute, true))
{
- case HitResult.None:
- break;
+ // not sure if this should remain going forward.
+ JudgementBody.ResetAnimation();
- case HitResult.Miss:
- JudgementBody.ScaleTo(1.6f);
- JudgementBody.ScaleTo(1, 100, Easing.In);
+ switch (Result.Type)
+ {
+ case HitResult.None:
+ break;
- JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
- JudgementBody.RotateTo(40, 800, Easing.InQuint);
+ case HitResult.Miss:
+ ApplyMissAnimations();
+ break;
- this.Delay(600).FadeOut(200);
- break;
+ default:
+ ApplyHitAnimations();
+ break;
+ }
- default:
- ApplyHitAnimations();
- break;
+ if (JudgementBody.Drawable is IAnimatableJudgement animatable)
+ {
+ var drawableAnimation = (Drawable)animatable;
+
+ animatable.PlayAnimation();
+
+ // a derived version of DrawableJudgement may be proposing a lifetime.
+ // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
+ double lastTransformTime = drawableAnimation.LatestTransformEndTime;
+ if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
+ LifetimeEnd = lastTransformTime;
+ }
}
-
- Expire(true);
}
private HitResult? currentDrawableType;
@@ -127,6 +192,7 @@ namespace osu.Game.Rulesets.Judgements
{
var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset
+ // todo: this should be removed once judgements are always pooled.
if (type == currentDrawableType)
return;
@@ -134,21 +200,24 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null)
RemoveInternal(JudgementBody);
- AddInternal(JudgementBody = new Container
+ aboveHitObjectsContent.Clear();
+ AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ =>
+ CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText
- {
- Text = type.GetDescription().ToUpperInvariant(),
- Font = OsuFont.Numeric.With(size: 20),
- Colour = colours.ForHitResult(type),
- Scale = new Vector2(0.85f, 1),
- }, confineMode: ConfineMode.NoScaling)
});
+ if (JudgementBody.Drawable is IAnimatableJudgement animatable)
+ {
+ var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
+ if (proxiedContent != null)
+ aboveHitObjectsContent.Add(proxiedContent);
+ }
+
currentDrawableType = type;
}
+
+ protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);
}
}
diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
new file mode 100644
index 0000000000..b38b83b534
--- /dev/null
+++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
@@ -0,0 +1,25 @@
+// 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.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Judgements
+{
+ ///
+ /// A skinnable judgement element which supports playing an animation from the current point in time.
+ ///
+ public interface IAnimatableJudgement : IDrawable
+ {
+ ///
+ /// Start the animation for this judgement from the current point in time.
+ ///
+ void PlayAnimation();
+
+ ///
+ /// Get proxied content which should be displayed above all hitobjects.
+ ///
+ [CanBeNull]
+ Drawable GetAboveHitObjectsProxiedContent();
+ }
+}
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 b400c532c5..ca49ed9e75 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -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())
@@ -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/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
index d8c6da86f9..ba38c7f77d 100644
--- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
+++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
@@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
- [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
- // ReSharper disable once RedundantOverload.Global
- public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
- double? legacyLastTickOffset)
- {
- return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
- }
-
// ReSharper disable once MethodOverloadWithOptionalParameter
public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default)
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/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index 1dc029506f..5fbda305c8 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI
{
Debug.Assert(!drawableMap.ContainsKey(entry));
- var drawable = pooledObjectProvider.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/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 80e33e0ec5..82ec653f31 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -218,9 +218,6 @@ namespace osu.Game.Rulesets.UI
#region Pooling support
- [Resolved(CanBeNull = true)]
- private IPooledHitObjectProvider parentPooledObjectProvider { get; set; }
-
private readonly Dictionary pools = new Dictionary();
///
@@ -320,10 +317,7 @@ namespace osu.Game.Rulesets.UI
}
}
- if (pool == null)
- return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject);
-
- return (DrawableHitObject)pool.Get(d =>
+ return (DrawableHitObject)pool?.Get(d =>
{
var dho = (DrawableHitObject)d;
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 3229719d5a..df9cadebfc 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
- protected virtual Container CreateSelectionBlueprintContainer() =>
- new Container { RelativeSizeAxes = Axes.Both };
+ protected virtual Container CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
///
/// Creates a which outlines s and handles movement of selections.
@@ -338,12 +337,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Whether a selection was performed.
private bool beginClickSelection(MouseButtonEvent e)
{
- foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
+ // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
+ foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
{
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/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
new file mode 100644
index 0000000000..9e95fe4fa1
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
@@ -0,0 +1,77 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Edit.Compose.Components
+{
+ ///
+ /// A container for ordered by their start times.
+ ///
+ public sealed class HitObjectOrderedSelectionContainer : Container
+ {
+ public override void Add(SelectionBlueprint drawable)
+ {
+ base.Add(drawable);
+ bindStartTime(drawable);
+ }
+
+ public override bool Remove(SelectionBlueprint drawable)
+ {
+ if (!base.Remove(drawable))
+ return false;
+
+ unbindStartTime(drawable);
+ return true;
+ }
+
+ public override void Clear(bool disposeChildren)
+ {
+ base.Clear(disposeChildren);
+ unbindAllStartTimes();
+ }
+
+ private readonly Dictionary startTimeMap = new Dictionary();
+
+ private void bindStartTime(SelectionBlueprint blueprint)
+ {
+ var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
+
+ bindable.BindValueChanged(_ =>
+ {
+ if (LoadState >= LoadState.Ready)
+ SortInternal();
+ });
+
+ startTimeMap[blueprint] = bindable;
+ }
+
+ private void unbindStartTime(SelectionBlueprint blueprint)
+ {
+ startTimeMap[blueprint].UnbindAll();
+ startTimeMap.Remove(blueprint);
+ }
+
+ private void unbindAllStartTimes()
+ {
+ foreach (var kvp in startTimeMap)
+ kvp.Value.UnbindAll();
+ startTimeMap.Clear();
+ }
+
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ var xObj = (SelectionBlueprint)x;
+ var yObj = (SelectionBlueprint)y;
+
+ // Put earlier blueprints towards the end of the list, so they handle input first
+ int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
+ return i == 0 ? CompareReverseChildID(x, y) : i;
+ }
+ }
+}
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..2f14c607c2 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;
@@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public TimelineSelectionBlueprintContainer()
{
- AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
+ AddInternal(new TimelinePart(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index d9948aa23c..46d5eb40b4 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Compose
composer = ruleset?.CreateHitObjectComposer();
// make the composer available to the timeline and other components in this screen.
- dependencies.CacheAs(composer);
+ if (composer != null)
+ dependencies.CacheAs(composer);
return dependencies;
}
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/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index f8bdf0140c..ce3e618889 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
+using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
@@ -149,7 +150,12 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
- ScorePanelList.AddScore(Score, true);
+ {
+ // only show flair / animation when arriving after watching a play that isn't autoplay.
+ bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay);
+
+ ScorePanelList.AddScore(Score, shouldFlair);
+ }
if (player != null && allowRetry)
{
diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs
new file mode 100644
index 0000000000..ca25efaa01
--- /dev/null
+++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs
@@ -0,0 +1,127 @@
+// 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.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement
+ {
+ private readonly HitResult result;
+
+ private readonly LegacyJudgementPieceOld temporaryOldStyle;
+
+ private readonly Drawable mainPiece;
+
+ private readonly ParticleExplosion particles;
+
+ public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Texture particleTexture)
+ {
+ this.result = result;
+
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new[]
+ {
+ mainPiece = createMainDrawable().With(d =>
+ {
+ d.Anchor = Anchor.Centre;
+ d.Origin = Anchor.Centre;
+ })
+ };
+
+ if (particleTexture != null)
+ {
+ AddInternal(particles = new ParticleExplosion(particleTexture, 150, 1600)
+ {
+ Size = new Vector2(140),
+ Depth = float.MaxValue,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+
+ if (result != HitResult.Miss)
+ {
+ //new judgement shows old as a temporary effect
+ AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f)
+ {
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+ }
+
+ public void PlayAnimation()
+ {
+ var animation = mainPiece as IFramedAnimation;
+
+ animation?.GotoFrame(0);
+
+ if (particles != null)
+ {
+ // start the particles already some way into their animation to break cluster away from centre.
+ using (particles.BeginDelayedSequence(-100, true))
+ particles.Restart();
+ }
+
+ const double fade_in_length = 120;
+ const double fade_out_delay = 500;
+ const double fade_out_length = 600;
+
+ this.FadeInFromZero(fade_in_length);
+ this.Delay(fade_out_delay).FadeOut(fade_out_length);
+
+ // new style non-miss judgements show the original style temporarily, with additive colour.
+ if (temporaryOldStyle != null)
+ {
+ temporaryOldStyle.PlayAnimation();
+
+ temporaryOldStyle.Hide();
+ temporaryOldStyle.Delay(-16)
+ .FadeTo(0.5f, 56, Easing.Out).Then()
+ .FadeOut(300);
+ }
+
+ // legacy judgements don't play any transforms if they are an animation.
+ if (animation?.FrameCount > 1)
+ return;
+
+ switch (result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ //todo: this only applies to osu! ruleset apparently.
+ this.MoveTo(new Vector2(0, -2));
+ this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In);
+
+ float rotation = RNG.NextSingle(-8.6f, 8.6f);
+
+ this.RotateTo(0);
+ this.RotateTo(rotation, fade_in_length)
+ .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
+ break;
+
+ default:
+ mainPiece.ScaleTo(0.9f);
+ mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length);
+ break;
+ }
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects.
+ }
+}
diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
new file mode 100644
index 0000000000..5d74ab9ae3
--- /dev/null
+++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
@@ -0,0 +1,75 @@
+// 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.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement
+ {
+ private readonly HitResult result;
+
+ private readonly float finalScale;
+
+ public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f)
+ {
+ this.result = result;
+ this.finalScale = finalScale;
+
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChild = createMainDrawable();
+ }
+
+ public virtual void PlayAnimation()
+ {
+ var animation = InternalChild as IFramedAnimation;
+
+ animation?.GotoFrame(0);
+
+ const double fade_in_length = 120;
+ const double fade_out_delay = 500;
+ const double fade_out_length = 600;
+
+ this.FadeInFromZero(fade_in_length);
+ this.Delay(fade_out_delay).FadeOut(fade_out_length);
+
+ // legacy judgements don't play any transforms if they are an animation.
+ if (animation?.FrameCount > 1)
+ return;
+
+ switch (result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ float rotation = RNG.NextSingle(-8.6f, 8.6f);
+
+ this.RotateTo(0);
+ this.RotateTo(rotation, fade_in_length)
+ .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
+ break;
+
+ default:
+
+ this.ScaleTo(0.6f).Then()
+ .ScaleTo(1.1f, fade_in_length * 0.8f).Then()
+ // this is actually correct to match stable; there were overlapping transforms.
+ .ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
+ .ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
+ .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
+ break;
+ }
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index fb020f4e39..63a22eba62 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -371,19 +371,15 @@ namespace osu.Game.Skinning
}
case GameplaySkinComponent resultComponent:
- switch (resultComponent.Component)
+ Func createDrawable = () => getJudgementAnimation(resultComponent.Component);
+
+ // kind of wasteful that we throw this away, but should do for now.
+ if (createDrawable() != null)
{
- case HitResult.Miss:
- return this.GetAnimation("hit0", true, false);
-
- case HitResult.Meh:
- return this.GetAnimation("hit50", true, false);
-
- case HitResult.Ok:
- return this.GetAnimation("hit100", true, false);
-
- case HitResult.Great:
- return this.GetAnimation("hit300", true, false);
+ if (Configuration.LegacyVersion > 1)
+ return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component));
+ else
+ return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
break;
@@ -392,6 +388,43 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
+ private Texture getParticleTexture(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Meh:
+ return GetTexture("particle50");
+
+ case HitResult.Ok:
+ return GetTexture("particle100");
+
+ case HitResult.Great:
+ return GetTexture("particle300");
+ }
+
+ return null;
+ }
+
+ private Drawable getJudgementAnimation(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Miss:
+ return this.GetAnimation("hit0", true, false);
+
+ case HitResult.Meh:
+ return this.GetAnimation("hit50", true, false);
+
+ case HitResult.Ok:
+ return this.GetAnimation("hit100", true, false);
+
+ case HitResult.Great:
+ return this.GetAnimation("hit300", true, false);
+ }
+
+ return null;
+ }
+
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (var name in getFallbackNames(componentName))
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/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 5352928ec6..1340d1474c 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -24,7 +24,15 @@ namespace osu.Game.Skinning
{
}
- protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) };
+ protected override Drawable CreateDefault(ISkinComponent component)
+ {
+ var texture = textures.Get(component.LookupName);
+
+ if (texture == null)
+ return null;
+
+ return new Sprite { Texture = texture };
+ }
private class SpriteComponent : ISkinComponent
{
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);
+ }
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1850ee3488..54f3fcede6 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 2ac23f1503..692dac909a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+