From f1535b74beb185c83f76d7e7e3a54e55b6c32a81 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:16:06 -0500 Subject: [PATCH 01/10] Give Spun Out mod dynamic spin rate --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 098c639949..b900fa3274 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f)); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } } From 585bd541f319ce316c8c4eef80aeb4247449b232 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:38:49 -0500 Subject: [PATCH 02/10] Add missing parentheses to RPM calculation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b900fa3274..4725a43a77 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From df9535d195700205380bc624f2a9999cfa9e228c Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 14:28:40 -0500 Subject: [PATCH 03/10] Update RPM calculation for readability Multiply the 1.01 factor to the resulting RPM, not to the duration. --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 4725a43a77..9be0dc748a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,10 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); + + // multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact, + // some spinners may not complete due to very minor decimal loss during calculation + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From c1777f20e114da75a51c6e3fc38ad5f637b4900d Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:11:44 -0500 Subject: [PATCH 04/10] Fix Spun Out tests Change 'unaffected by mods' test to use dynamic RPM value instead of a fixed value --- .../Mods/TestSceneOsuModSpunOut.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 24e69703a6..29d7e7b4d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -48,7 +48,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => { var counter = Player.ChildrenOfType().SingleOrDefault(); - return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1); + var spinner = Player.ChildrenOfType().FirstOrDefault(); + + if (counter == null || spinner == null) + return false; + + // ignore cases where the spinner hasn't started as these lead to false-positives + if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) + return false; + + double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); + + return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); } }); } From 95b1bffffeceff678762ccb32e329a0b9277af6a Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:45:02 -0500 Subject: [PATCH 05/10] Add test to ensure spinners only complete No bonus or a non-300 judgement --- .../Mods/TestSceneOsuModSpunOut.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 29d7e7b4d6..e71377a505 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -57,7 +58,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) return false; - double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); @@ -65,6 +65,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }); } + [Test] + public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + var spinner = Player.ChildrenOfType().SingleOrDefault(); + var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + + if (spinner == null || gameplayClockContainer == null) + return false; + + if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) + return false; + + return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + private Beatmap singleSpinnerBeatmap => new Beatmap { HitObjects = new List From 0d56693b7aee6221d16ac2341d86d70b5a805a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:11:22 +0900 Subject: [PATCH 06/10] Fix test not always checking the final bonus value Due to the previous logic not waiting until the spinner had completed, there could be false negatives as the check runs too early, with a potential additional bonus spin occurring afterwards. --- .../Mods/TestSceneOsuModSpunOut.cs | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e71377a505..cb8eceb213 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -8,13 +8,15 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Screens.Play; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -66,25 +68,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + public void TestSpinnerGetsNoBonusScore() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => + DrawableSpinner spinner = null; + List results = new List(); + + CreateModTest(new ModTestData { - var spinner = Player.ChildrenOfType().SingleOrDefault(); - var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); - if (spinner == null || gameplayClockContainer == null) - return false; + if (nextSpinner == null) + return false; - if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) - return false; + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => results.Add(result); - return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); - } - }); + results.Clear(); + } + + // we should only be checking the bonus/progress after the spinner has fully completed. + if (!results.OfType().Any(r => r.TimeCompleted != null)) + return false; + + return + results.Any(r => r.Type == HitResult.SmallTickHit) + && !results.Any(r => r.Type == HitResult.LargeTickHit) + && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) + && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + } private Beatmap singleSpinnerBeatmap => new Beatmap { From 91acc9eec6a8859f665f64338420efc564faf33b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:36:08 +0900 Subject: [PATCH 07/10] Remove checks which are still going to occasionally fail due to pooling --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index cb8eceb213..93c4bd96de 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -101,9 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit) - && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) - && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + && !results.Any(r => r.Type == HitResult.LargeTickHit); } }); } From 9e279c3ebc9cdc23ba5d4a1d192642b314019de8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:37:52 +0900 Subject: [PATCH 08/10] Fix completely incorrect judgement specification --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 93c4bd96de..e61720d8b9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -100,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return false; return - results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit); + results.Any(r => r.Type == HitResult.SmallBonus) + && !results.Any(r => r.Type == HitResult.LargeBonus); } }); } From a6b6644c2e441513bfba493fbe56e307996bd4ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:22:13 +0900 Subject: [PATCH 09/10] Replace LINQ queries with recommendations --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e61720d8b9..70b7d1f740 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -96,12 +96,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } // we should only be checking the bonus/progress after the spinner has fully completed. - if (!results.OfType().Any(r => r.TimeCompleted != null)) + if (results.OfType().All(r => r.TimeCompleted == null)) return false; return results.Any(r => r.Type == HitResult.SmallBonus) - && !results.Any(r => r.Type == HitResult.LargeBonus); + && results.All(r => r.Type != HitResult.LargeBonus); } }); } From 054ed546e3800e6aa0425b362b68dacf9e19fb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:56:50 +0900 Subject: [PATCH 10/10] Fix intermittent failures in remaining test method --- .../Mods/TestSceneOsuModSpunOut.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 70b7d1f740..a8953c1a6f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods protected override bool AllowFail => true; [Test] - public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData + public void TestSpinnerAutoCompleted() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1 - }); + DrawableSpinner spinner = null; + JudgementResult lastResult = null; + + CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); + + if (nextSpinner == null) + return false; + + lastResult = null; + + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => lastResult = result; + } + + return lastResult?.Type == HitResult.Great; + } + }); + } [TestCase(null)] [TestCase(typeof(OsuModDoubleTime))]