From 37873484485dc59e3e1f1506a013ee57db849f4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 16:29:02 +0900 Subject: [PATCH 01/25] Change `Perfect` judgement to not give extra score --- osu.Game/Rulesets/Judgements/Judgement.cs | 5 ++--- osu.Game/Rulesets/Scoring/HitResult.cs | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index f60b3a6c02..cd1e81046d 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -190,10 +190,9 @@ namespace osu.Game.Rulesets.Judgements return 200; case HitResult.Great: - return 300; - + // Perfect doesn't actually give more score / accuracy directly. case HitResult.Perfect: - return 315; + return 300; case HitResult.SmallBonus: return SMALL_BONUS_SCORE; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index ccd1f49de4..fed338b012 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -55,6 +55,13 @@ namespace osu.Game.Rulesets.Scoring [Order(1)] Great, + /// + /// This is an optional timing window tighter than . + /// + /// + /// By default, this does not give any bonus accuracy or score. + /// To have it affect scoring, consider adding a nested bonus object. + /// [Description(@"Perfect")] [EnumMember(Value = "perfect")] [Order(0)] From 94b64044e01681d3ce3da221255b26822b52c8e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 17:01:48 +0900 Subject: [PATCH 02/25] Add nested bonus judgement to `DrawableNote` for perfect bonus score --- .../Objects/Drawables/DrawableNote.cs | 39 +++++++++++++++++++ .../Drawables/DrawablePerfectBonusNote.cs | 25 ++++++++++++ osu.Game.Rulesets.Mania/Objects/Note.cs | 8 ++++ .../Objects/PerfectBonusNote.cs | 19 +++++++++ osu.Game.Rulesets.Mania/UI/Column.cs | 1 + 5 files changed, 92 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 0819e8401c..dbc9446585 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -4,6 +4,7 @@ #nullable disable using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,6 +14,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; @@ -38,6 +41,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; + [CanBeNull] + private DrawablePerfectBonusNote bonusNote; + public DrawableNote() : this(null) { @@ -89,7 +95,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) + { + bonusNote!.TriggerResult(false); ApplyResult(r => r.Type = r.Judgement.MinResult); + } + return; } @@ -97,6 +107,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; + bonusNote!.TriggerResult(result == HitResult.Perfect); ApplyResult(r => r.Type = result); } @@ -115,6 +126,34 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawablePerfectBonusNote bonus: + AddInternal(bonusNote = bonus); + break; + } + } + + protected override void ClearNestedHitObjects() + { + if (bonusNote != null) + RemoveInternal(bonusNote, false); + bonusNote = null; + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + { + switch (hitObject) + { + case PerfectBonusNote bonus: + return new DrawablePerfectBonusNote(bonus); + } + + return base.CreateNestedHitObject(hitObject); + } + private void updateSnapColour() { if (beatmap == null || HitObject == null) return; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs new file mode 100644 index 0000000000..9f51f9c5b3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.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. + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public partial class DrawablePerfectBonusNote : DrawableManiaHitObject + { + public DrawablePerfectBonusNote() + : this(null!) + { + AlwaysPresent = true; + } + + public DrawablePerfectBonusNote(PerfectBonusNote hitObject) + : base(hitObject) + { + } + + /// + /// Apply a judgement result. + /// + /// Whether this tick was reached. + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 0035960c63..955d3e9c7d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.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.Threading; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; @@ -12,5 +13,12 @@ namespace osu.Game.Rulesets.Mania.Objects public class Note : ManiaHitObject { public override Judgement CreateJudgement() => new ManiaJudgement(); + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new PerfectBonusNote { StartTime = StartTime }); + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs new file mode 100644 index 0000000000..b601a0614b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs @@ -0,0 +1,19 @@ +// 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.Mania.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class PerfectBonusNote : ManiaHitObject + { + public override Judgement CreateJudgement() => new PerfectBonusNoteJudgement(); + + public class PerfectBonusNoteJudgement : ManiaJudgement + { + public override HitResult MaxResult => HitResult.SmallBonus; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 6cd55bb099..68d1e929be 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -109,6 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); RegisterPool(10, 50); + RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); From 125f28219dd32b8039f27a721bd2926eb3bfa48a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 17:36:19 +0900 Subject: [PATCH 03/25] Fix hodl tail notes not correctly handling nested bonus judgement --- .../Objects/Drawables/DrawableHoldNoteTail.cs | 35 +++++-------------- .../Objects/Drawables/DrawableNote.cs | 4 ++- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a559e91f1b..a183231310 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -33,35 +32,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - + protected override void CheckForResult(bool userTriggered, double timeOffset) => // Factor in the release lenience - timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE; + base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); - if (!userTriggered) - { - if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = r.Judgement.MinResult); + protected override HitResult MutateResultApplication(HitResult result) + { + // If the head wasn't hit or the hold note was broken, cap the max score to Meh. + bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak; - return; - } + if (result > HitResult.Meh && hasComboBreak) + return HitResult.Meh; - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) - return; - - ApplyResult(r => - { - // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak; - - if (result > HitResult.Meh && hasComboBreak) - result = HitResult.Meh; - - r.Type = result; - }); + return result; } public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index dbc9446585..eaeac33a11 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -108,9 +108,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; bonusNote!.TriggerResult(result == HitResult.Perfect); - ApplyResult(r => r.Type = result); + ApplyResult(r => r.Type = MutateResultApplication(result)); } + protected virtual HitResult MutateResultApplication(HitResult result) => result; + public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Action != Action.Value) From 850950ba610018e87980bb2e2dfa80c371b16578 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 17:36:30 +0900 Subject: [PATCH 04/25] Add note about why `SmallBonus` is not listed in `GetValidHitResults` --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ff34b69d19..0055e10ada 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -385,6 +385,9 @@ namespace osu.Game.Rulesets.Mania HitResult.Good, HitResult.Ok, HitResult.Meh, + + // HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as + // it would be a bit redundant to show this to the user. }; } From ddbda69751c8bb0c4674e35ee6cee86cdbdbb2aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 13:14:58 +0900 Subject: [PATCH 05/25] Remove nullability of `bonusNote` --- .../Objects/Drawables/DrawableNote.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index eaeac33a11..99bd66a147 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -4,7 +4,6 @@ #nullable disable using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -41,7 +40,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; - [CanBeNull] private DrawablePerfectBonusNote bonusNote; public DrawableNote() @@ -96,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (!HitObject.HitWindows.CanBeHit(timeOffset)) { - bonusNote!.TriggerResult(false); + bonusNote.TriggerResult(false); ApplyResult(r => r.Type = r.Judgement.MinResult); } @@ -107,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; - bonusNote!.TriggerResult(result == HitResult.Perfect); + bonusNote.TriggerResult(result == HitResult.Perfect); ApplyResult(r => r.Type = MutateResultApplication(result)); } @@ -140,9 +138,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void ClearNestedHitObjects() { - if (bonusNote != null) - RemoveInternal(bonusNote, false); - bonusNote = null; + RemoveInternal(bonusNote, false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) From e85c0397223c162720004bcb66c249cfbaf0b1d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 13:31:14 +0900 Subject: [PATCH 06/25] Adjust method flow to hopefully be more legible --- .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a183231310..79002b3819 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // Factor in the release lenience base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); - protected override HitResult MutateResultApplication(HitResult result) + protected override HitResult GetCappedResult(HitResult result) { // If the head wasn't hit or the hold note was broken, cap the max score to Meh. bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 99bd66a147..c7eabaf616 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -105,11 +105,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; + result = GetCappedResult(result); + bonusNote.TriggerResult(result == HitResult.Perfect); - ApplyResult(r => r.Type = MutateResultApplication(result)); + ApplyResult(r => r.Type = result); } - protected virtual HitResult MutateResultApplication(HitResult result) => result; + /// + /// Some objects in mania may want to limit the max result. + /// + protected virtual HitResult GetCappedResult(HitResult result) => result; public virtual bool OnPressed(KeyBindingPressEvent e) { From 7c49843411c750565a3ab80f64b9dbd7233484ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 14:01:47 +0900 Subject: [PATCH 07/25] Fix various tests --- .../Mods/TestSceneManiaModDoubleTime.cs | 39 +++++++++++-------- .../TestSceneHoldNoteInput.cs | 18 ++++----- .../TestSceneMaximumScore.cs | 4 +- .../Rulesets/Scoring/ScoreProcessorTest.cs | 4 +- 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs index 00b79529a9..9f2530eb31 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { private const double offset = 18; + protected override bool AllowFail => true; + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); [Test] public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData { - PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1, + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value == 1_000_000, Autoplay = false, Beatmap = new Beatmap { @@ -40,24 +42,29 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods }); [Test] - public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData + public void TestHitWindowWithDoubleTime() { - Mod = new ManiaModDoubleTime(), - PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1, - Autoplay = false, - Beatmap = new Beatmap + var doubleTime = new ManiaModDoubleTime(); + + CreateModTest(new ModTestData { - BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, - Difficulty = { OverallDifficulty = 10 }, - HitObjects = new List + Mod = doubleTime, + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value / doubleTime.ScoreMultiplier == 100010, + Autoplay = false, + Beatmap = new Beatmap { - new Note { StartTime = 1000 } + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + Difficulty = { OverallDifficulty = 10 }, + HitObjects = new List + { + new Note { StartTime = 1000 } + }, }, - }, - ReplayFrames = new List - { - new ManiaReplayFrame(1000 + offset, ManiaAction.Key1) - } - }); + ReplayFrames = new List + { + new ManiaReplayFrame(1000 + offset, ManiaAction.Key1) + } + }); + } } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 93128c512f..ccd5e0600d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -200,10 +200,12 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertComboAtJudgement(0, 1); + // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult. + assertComboAtJudgement(1, 1); assertTailJudgement(HitResult.Meh); - assertComboAtJudgement(1, 0); - assertComboAtJudgement(2, 1); + assertComboAtJudgement(2, 0); + // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult. + assertComboAtJudgement(4, 1); } /// @@ -392,13 +394,11 @@ namespace osu.Game.Rulesets.Mania.Tests Duration = time_tail - time_head, Column = 0, }, + // Next note within tail lenience + (ManiaHitObject)(note = new Note { - // Next note within tail lenience - note = new Note - { - StartTime = time_tail + 50 - } - } + StartTime = time_tail + 50 + }) }, BeatmapInfo = { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs index 3d0abaceb5..3a74f87f1a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("all objects perfectly judged", () => judgementResults.Select(result => result.Type), () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); - AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030)); } [Test] @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("all objects perfectly judged", () => judgementResults.Select(result => result.Type), () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); - AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + AddAssert("base score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040)); } private void performTest(List hitObjects, List frames) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 92e94bd02d..cba90b2ebe 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)] [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)] - [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 317_626)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)] [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 30_398)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 31_928)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] From 14fedffcc1c0d753a395b16a5521286c826ebb37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 14:53:36 +0900 Subject: [PATCH 08/25] Fix `MissForcefully` not considering the bonus object --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index c7eabaf616..27039d2f37 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -111,6 +111,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => r.Type = result); } + public override void MissForcefully() + { + bonusNote.TriggerResult(false); + base.MissForcefully(); + } + /// /// Some objects in mania may want to limit the max result. /// From 43f619f92a7761c95d9b4632373eb2b9ed0996a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 14:57:55 +0900 Subject: [PATCH 09/25] Add `DisplayResult` flag and remove unnecessary `AlwaysPresent` --- .../Objects/Drawables/DrawablePerfectBonusNote.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs index 9f51f9c5b3..1761f675f5 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs @@ -5,10 +5,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public partial class DrawablePerfectBonusNote : DrawableManiaHitObject { + public override bool DisplayResult => false; + public DrawablePerfectBonusNote() : this(null!) { - AlwaysPresent = true; } public DrawablePerfectBonusNote(PerfectBonusNote hitObject) From 1a957364aeb9aefa9c0ce5a08e1de5961e55db8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 14:58:49 +0900 Subject: [PATCH 10/25] Add empty hit windows on `PefectBonusNote` --- osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs index b601a0614b..e98b2ff1c1 100644 --- a/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Mania.Objects public class PerfectBonusNote : ManiaHitObject { public override Judgement CreateJudgement() => new PerfectBonusNoteJudgement(); + protected override HitWindows CreateHitWindows() => HitWindows.Empty; public class PerfectBonusNoteJudgement : ManiaJudgement { From 3f09ed396f511bc45e4dfbe0eb1d205ae0e58a52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 15:11:33 +0900 Subject: [PATCH 11/25] Fix legacy skin body piece dimming when it shouldn't --- .../Skinning/Legacy/LegacyBodyPiece.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 660f72e565..5c353887c3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -123,9 +123,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy 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; + switch (hitObject) + { + // Ensure that the hold note is also faded out when the head/tail/any tick is missed. + // Importantly, we filter out unrelated objects like DrawablePerfectBonusNote. + case DrawableHoldNoteTail: + case DrawableHoldNoteHead: + case DrawableHoldNoteBody: + if (state == ArmedState.Miss) + missFadeTime.Value ??= hitObject.HitStateUpdateTime; + + break; + } } private void onIsHittingChanged(ValueChangedEvent isHitting) From 4f1546c4743fea3a9ee89dded4086d710fd50557 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 15:14:26 +0900 Subject: [PATCH 12/25] Rename `PerfectBonusNote` to `NotePerfectBonus` --- .../Objects/Drawables/DrawableNote.cs | 18 +++++++++--------- ...onusNote.cs => DrawableNotePerfectBonus.cs} | 6 +++--- osu.Game.Rulesets.Mania/Objects/Note.cs | 2 +- ...PerfectBonusNote.cs => NotePerfectBonus.cs} | 6 +++--- .../Skinning/Legacy/LegacyBodyPiece.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) rename osu.Game.Rulesets.Mania/Objects/Drawables/{DrawablePerfectBonusNote.cs => DrawableNotePerfectBonus.cs} (76%) rename osu.Game.Rulesets.Mania/Objects/{PerfectBonusNote.cs => NotePerfectBonus.cs} (70%) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 27039d2f37..c70dfcb761 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; - private DrawablePerfectBonusNote bonusNote; + private DrawableNotePerfectBonus perfectBonus; public DrawableNote() : this(null) @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (!HitObject.HitWindows.CanBeHit(timeOffset)) { - bonusNote.TriggerResult(false); + perfectBonus.TriggerResult(false); ApplyResult(r => r.Type = r.Judgement.MinResult); } @@ -107,13 +107,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables result = GetCappedResult(result); - bonusNote.TriggerResult(result == HitResult.Perfect); + perfectBonus.TriggerResult(result == HitResult.Perfect); ApplyResult(r => r.Type = result); } public override void MissForcefully() { - bonusNote.TriggerResult(false); + perfectBonus.TriggerResult(false); base.MissForcefully(); } @@ -141,23 +141,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { switch (hitObject) { - case DrawablePerfectBonusNote bonus: - AddInternal(bonusNote = bonus); + case DrawableNotePerfectBonus bonus: + AddInternal(perfectBonus = bonus); break; } } protected override void ClearNestedHitObjects() { - RemoveInternal(bonusNote, false); + RemoveInternal(perfectBonus, false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) { switch (hitObject) { - case PerfectBonusNote bonus: - return new DrawablePerfectBonusNote(bonus); + case NotePerfectBonus bonus: + return new DrawableNotePerfectBonus(bonus); } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs similarity index 76% rename from osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs rename to osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs index 1761f675f5..70ddb60296 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawablePerfectBonusNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs @@ -3,16 +3,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { - public partial class DrawablePerfectBonusNote : DrawableManiaHitObject + public partial class DrawableNotePerfectBonus : DrawableManiaHitObject { public override bool DisplayResult => false; - public DrawablePerfectBonusNote() + public DrawableNotePerfectBonus() : this(null!) { } - public DrawablePerfectBonusNote(PerfectBonusNote hitObject) + public DrawableNotePerfectBonus(NotePerfectBonus hitObject) : base(hitObject) { } diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 955d3e9c7d..5914132624 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Objects { base.CreateNestedHitObjects(cancellationToken); - AddNested(new PerfectBonusNote { StartTime = StartTime }); + AddNested(new NotePerfectBonus { StartTime = StartTime }); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs b/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs similarity index 70% rename from osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs rename to osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs index e98b2ff1c1..def4c01268 100644 --- a/osu.Game.Rulesets.Mania/Objects/PerfectBonusNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs @@ -7,12 +7,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { - public class PerfectBonusNote : ManiaHitObject + public class NotePerfectBonus : ManiaHitObject { - public override Judgement CreateJudgement() => new PerfectBonusNoteJudgement(); + public override Judgement CreateJudgement() => new NotePerfectBonusJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public class PerfectBonusNoteJudgement : ManiaJudgement + public class NotePerfectBonusJudgement : ManiaJudgement { public override HitResult MaxResult => HitResult.SmallBonus; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 5c353887c3..f27b3bdd5c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy switch (hitObject) { // Ensure that the hold note is also faded out when the head/tail/any tick is missed. - // Importantly, we filter out unrelated objects like DrawablePerfectBonusNote. + // Importantly, we filter out unrelated objects like DrawableNotePerfectBonus. case DrawableHoldNoteTail: case DrawableHoldNoteHead: case DrawableHoldNoteBody: diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 68d1e929be..9489281176 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); RegisterPool(10, 50); - RegisterPool(10, 50); + RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); From d9d062915722a7fafe2c8d987067032157c21439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 15:16:10 +0900 Subject: [PATCH 13/25] Fix code quality inspection (weird one) --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index ccd5e0600d..044ce37832 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -382,7 +382,8 @@ namespace osu.Game.Rulesets.Mania.Tests [Test] public void TestPressAndReleaseJustAfterTailWithNearbyNote() { - Note note; + // Next note within tail lenience + Note note = new Note { StartTime = time_tail + 50 }; var beatmap = new Beatmap { @@ -394,11 +395,7 @@ namespace osu.Game.Rulesets.Mania.Tests Duration = time_tail - time_head, Column = 0, }, - // Next note within tail lenience - (ManiaHitObject)(note = new Note - { - StartTime = time_tail + 50 - }) + note }, BeatmapInfo = { From db00b794a24bc21c1e637027d9c2c0ecd1fe057c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 08:52:32 +0200 Subject: [PATCH 14/25] Fix test failure due to missing zero (and FP shenanigans) --- .../Mods/TestSceneManiaModDoubleTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs index 9f2530eb31..f1a432cc06 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods CreateModTest(new ModTestData { Mod = doubleTime, - PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value / doubleTime.ScoreMultiplier == 100010, + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value == (long)(1000010 * doubleTime.ScoreMultiplier), Autoplay = false, Beatmap = new Beatmap { From b9a84127ac876c9f153f16f2cfe06da2cde3ff89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 08:57:35 +0200 Subject: [PATCH 15/25] Remove mention of "any tick" They're very dead now. --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index f27b3bdd5c..66e67136df 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { switch (hitObject) { - // Ensure that the hold note is also faded out when the head/tail/any tick is missed. + // Ensure that the hold note is also faded out when the head/tail/body is missed. // Importantly, we filter out unrelated objects like DrawableNotePerfectBonus. case DrawableHoldNoteTail: case DrawableHoldNoteHead: From 624c05e0ff3b783ceb626e8461ce3e2d3c1e1ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 09:04:53 +0200 Subject: [PATCH 16/25] Rename test step --- osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs index 3a74f87f1a..edf866952b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("all objects perfectly judged", () => judgementResults.Select(result => result.Type), () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); - AddAssert("base score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040)); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040)); } private void performTest(List hitObjects, List frames) From c48142816c63d0d2d27f920f573435b47198b1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 09:06:34 +0200 Subject: [PATCH 17/25] Reformat long lines --- .../Mods/TestSceneManiaModDoubleTime.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs index f1a432cc06..c717f03f51 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -24,7 +24,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [Test] public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData { - PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value == 1_000_000, + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 + && Player.ScoreProcessor.Accuracy.Value == 1 + && Player.ScoreProcessor.TotalScore.Value == 1_000_000, Autoplay = false, Beatmap = new Beatmap { @@ -49,7 +51,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods CreateModTest(new ModTestData { Mod = doubleTime, - PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 && Player.ScoreProcessor.TotalScore.Value == (long)(1000010 * doubleTime.ScoreMultiplier), + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 + && Player.ScoreProcessor.Accuracy.Value == 1 + && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier), Autoplay = false, Beatmap = new Beatmap { From 84be714d6bd100e69e464dafda6c471aa8e292e8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Sep 2023 02:19:11 +0900 Subject: [PATCH 18/25] Fix large instantaneous delta on first frame Happens when the first update frame comes in before any mouse input. --- .../Default/SpinnerRotationTracker.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 719cf57d98..41d6e689b1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -22,11 +22,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly DrawableSpinner drawableSpinner; - private Vector2 mousePosition; + private Vector2? mousePosition; + private float? lastAngle; - private float lastAngle; private float currentRotation; - private bool rotationTransferred; [Resolved(canBeNull: true)] @@ -63,17 +62,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override void Update() { base.Update(); - float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - float delta = thisAngle - lastAngle; + if (mousePosition is Vector2 pos) + { + float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); + float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; - if (Tracking) - AddRotation(delta); + if (Tracking) + AddRotation(delta); - lastAngle = thisAngle; + lastAngle = thisAngle; + } IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; - Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -116,8 +117,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { Tracking = false; IsSpinning.Value = false; - mousePosition = default; - lastAngle = currentRotation = Rotation = 0; + mousePosition = null; + lastAngle = null; + currentRotation = 0; + Rotation = 0; rotationTransferred = false; } From 159b24acf767b07e1dc803d7875788bd8ebf8e72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:25:03 +0900 Subject: [PATCH 19/25] Rename `RateAdjustedRotation` to `TotalRotation` --- .../TestSceneSpinnerApplication.cs | 4 ++-- .../TestSceneSpinnerRotation.cs | 12 ++++++------ .../Judgements/OsuSpinnerJudgementResult.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 6 +++--- .../Skinning/Argon/ArgonSpinnerDisc.cs | 2 +- .../Skinning/Default/DefaultSpinnerDisc.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 1ae17432be..dae81f4cff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); - AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); + AddAssert("rotation is set", () => dho.Result.TotalRotation == 180); AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests Duration = 1000, }))); - AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); + AddAssert("rotation is reset", () => dho.Result.TotalRotation == 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 116c974f32..8711aa9c09 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); - AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.TotalRotation); addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", @@ -92,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); 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. - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 941cb667cf..c5e15d63ea 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Judgements /// 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; + public float TotalRotation; /// /// Time instant at which the spin was started (the first user input which caused an increase in spin). diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 24446db92a..9fa180cf93 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.TotalRotation / 360 / HitObject.SpinsRequired, 0, 1); } } @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // don't update after end time to avoid the rate display dropping during fade out. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. if (Time.Current <= HitObject.EndTime) - spmCalculator.SetRotation(Result.RateAdjustedRotation); + spmCalculator.SetRotation(Result.TotalRotation); updateBonusScore(); } @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(Result.RateAdjustedRotation / 360); + int spins = (int)(Result.TotalRotation / 360); if (spins < completedFullSpins) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs index bdc93eb63f..079758c21e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 75f3247448..b498975a83 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 41d6e689b1..77d410887c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + drawableSpinner.Result.TotalRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); } private void resetState(DrawableHitObject obj) From cfa4adb24d2ed5282ef0e4c8616e3ab9f70c0b43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:25:40 +0900 Subject: [PATCH 20/25] Add `SpinFramesGenerator` class to simplify creating spinner tests --- .../SpinFramesGenerator.cs | 111 ++++++++++++++++++ .../TestSceneLegacyHitPolicy.cs | 15 +-- .../TestSceneSpinnerJudgement.cs | 26 +--- .../TestSceneStartTimeOrderedHitPolicy.cs | 15 +-- 4 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs new file mode 100644 index 0000000000..43adfb7f1f --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -0,0 +1,111 @@ +// 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.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class SpinFramesGenerator + { + /// + /// A small amount to spin beyond a given angle to mitigate floating-point precision errors. + /// + public const float SPIN_ERROR = MathF.PI / 8; + + /// + /// The offset from the centre of the spinner at which to spin. + /// + private const float centre_spin_offset = 50; + + private readonly double startTime; + private readonly float startAngle; + private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>(); + + /// + /// Creates a new that can be used to generate spinner spin frames. + /// + /// The time at which to start spinning. + /// The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis. + public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f) + { + this.startTime = startTime; + this.startAngle = startAngle; + } + + /// + /// Performs a single spin. + /// + /// The amount, relative to a full circle, to spin. + /// The time to spend to perform the spin. + /// This . + public SpinFramesGenerator Spin(float delta, double duration) + { + sequences.Add((delta * 2 * MathF.PI, duration)); + return this; + } + + /// + /// Constructs the replay frames. + /// + /// The replay frames. + public List Build() + { + List frames = new List(); + + double lastTime = startTime; + float lastAngle = startAngle; + int lastDirection = 0; + + for (int i = 0; i < sequences.Count; i++) + { + var seq = sequences[i]; + + int seqDirection = Math.Sign(seq.deltaAngle); + float seqError = SPIN_ERROR * seqDirection; + + if (seqDirection == lastDirection) + { + // Spinning in the same direction, but the error was already added in the last rotation. + seqError = 0; + } + else if (lastDirection != 0) + { + // Spinning in a different direction, we need to account for the error of the start angle, so double it. + seqError *= 2; + } + + double seqStartTime = lastTime; + double seqEndTime = lastTime + seq.duration; + float seqStartAngle = lastAngle; + float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError; + + // Intermediate spin frames. + for (; lastTime < seqEndTime; lastTime += 10) + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + // Final frame at the end of the current spin. + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + lastTime = seqEndTime; + lastAngle = seqEndAngle; + lastDirection = seqDirection; + } + + // Key release frame. + if (frames.Count > 0) + frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position)); + + return frames; + } + + private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle) + { + float angle = startAngle + (endAngle - startAngle) * (float)p; + return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index a2ef72fe57..e0a618b187 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -356,15 +356,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(1, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs index c969cb11b4..6a50f08508 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.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 NUnit.Framework; @@ -11,14 +10,12 @@ using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -59,26 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); } - private static List generateReplay(int spins) - { - var replayFrames = new List(); - - const int frames_per_spin = 30; - - for (int i = 0; i < spins * frames_per_spin; ++i) - { - float totalProgress = i / (float)(spins * frames_per_spin); - float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; - double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; - float posX = MathF.Cos(2 * MathF.PI * spinProgress); - float posY = MathF.Sin(2 * MathF.PI * spinProgress); - Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; - - replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); - } - - return replayFrames; - } + private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) + .Spin(spins, time_spinner_end - time_spinner_start) + .Build(); private void performTest(List frames) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index f0af3f0c39..19413a50a8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(1, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); From 28ee99f132bf48a3368bcac3f51fc2721b079225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:31:01 +0900 Subject: [PATCH 21/25] Add prospective test coverage of spinner input handling --- .../TestSceneSpinnerInput.cs | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs new file mode 100644 index 0000000000..d7151f9370 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -0,0 +1,290 @@ +// 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.Screens; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene + { + private const int centre_x = 256; + private const int centre_y = 192; + private const double time_spinner_start = 1500; + private const double time_spinner_end = 8000; + + private readonly List judgementResults = new List(); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private ManualClock? manualClock; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + { + return manualClock == null + ? base.CreateWorkingBeatmap(beatmap, storyboard) + : new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); + } + + [SetUp] + public void Setup() => Schedule(() => + { + manualClock = null; + }); + + /// + /// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOffCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + const float y_pos = centre_y - 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOnCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spins in a single direction. + /// + [TestCase(0.5f, 0)] + [TestCase(-0.5f, 0)] + [TestCase(1, 1)] + [TestCase(-1, 1)] + [TestCase(1.5f, 1)] + [TestCase(-1.5f, 1)] + [TestCase(2f, 2)] + [TestCase(-2f, 2)] + public void TestSpinSingleDirection(float amount, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(amount, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + /// + /// Spin half-way clockwise then perform one full spin counter-clockwise. + /// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW). + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinHalfBothDirections() + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(0.5f, 500) // Rotate to +0.5. + .Spin(-1f, 500) // Rotate to -0.5 + .Build()); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spin in one direction then spin in the other. + /// + [TestCase(0.5f, -1.5f, 1)] + [TestCase(-0.5f, 1.5f, 1)] + [TestCase(0.5f, -2.5f, 2)] + [TestCase(-0.5f, 2.5f, 2)] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(direction1, 500) + .Spin(direction2, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestRewind() + { + AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 }); + + List frames = new SpinFramesGenerator(time_spinner_start) + .Spin(1f, 500) // 2000ms -> 1 full CW spin + .Spin(-0.5f, 500) // 2500ms -> 0.5 CCW spins + .Spin(0.25f, 500) // 3000ms -> 0.25 CW spins + .Spin(1.25f, 500) // 3500ms -> 1 full CW spin + .Spin(0.5f, 500) // 4000ms -> 0.5 CW spins + .Build(); + + loadPlayer(frames); + + GameplayClockContainer clock = null!; + DrawableRuleset drawableRuleset = null!; + AddStep("get gameplay objects", () => + { + clock = currentPlayer.ChildrenOfType().Single(); + drawableRuleset = currentPlayer.ChildrenOfType().Single(); + }); + + addSeekStep(frames.Last().Time); + + DrawableSpinner drawableSpinner = null!; + AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType().Single()) != null); + + assertTotalRotation(4000, 900); + assertTotalRotation(3750, 810); + assertTotalRotation(3500, 720); + assertTotalRotation(3250, 530); + assertTotalRotation(3000, 540); + assertTotalRotation(2750, 540); + assertTotalRotation(2500, 540); + assertTotalRotation(2250, 360); + assertTotalRotation(2000, 180); + assertTotalRotation(1500, 0); + + void assertTotalRotation(double time, float expected) + { + addSeekStep(time); + AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation, + () => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2))); + } + + void addSeekStep(double time) + { + AddStep($"seek to {time}", () => clock.Seek(time)); + AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time)); + } + } + + private void assertTicksHit(int count) + { + AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count)); + } + + private void assertSpinnerHit(bool shouldBeHit) + { + AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit)); + } + + private void loadPlayer(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = new Vector2(centre_x, centre_y) + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private void performTest(List frames) + { + loadPlayer(frames); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} From 04af46b8c72a03030aa61b69bc4a1ef72b8d9843 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:34:56 +0900 Subject: [PATCH 22/25] Change `SpinFramesGenerator` to take degrees as input --- .../SpinFramesGenerator.cs | 4 +- .../TestSceneLegacyHitPolicy.cs | 2 +- .../TestSceneSpinnerInput.cs | 38 +++++++++---------- .../TestSceneSpinnerJudgement.cs | 2 +- .../TestSceneStartTimeOrderedHitPolicy.cs | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs index 43adfb7f1f..dbdfa1f258 100644 --- a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Performs a single spin. /// - /// The amount, relative to a full circle, to spin. + /// The amount of degrees to spin. /// The time to spend to perform the spin. /// This . public SpinFramesGenerator Spin(float delta, double duration) { - sequences.Add((delta * 2 * MathF.PI, duration)); + sequences.Add((delta / 360 * 2 * MathF.PI, duration)); return this; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index e0a618b187..fa6aa580a3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; frames.AddRange(new SpinFramesGenerator(time_spinner + 10) - .Spin(1, 500) + .Spin(360, 500) .Build()); performTest(hitObjects, frames); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index d7151f9370..c4bf0d4e2e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Spins in a single direction. /// - [TestCase(0.5f, 0)] - [TestCase(-0.5f, 0)] - [TestCase(1, 1)] - [TestCase(-1, 1)] - [TestCase(1.5f, 1)] - [TestCase(-1.5f, 1)] - [TestCase(2f, 2)] - [TestCase(-2f, 2)] + [TestCase(180, 0)] + [TestCase(-180, 0)] + [TestCase(360, 1)] + [TestCase(-360, 1)] + [TestCase(540, 1)] + [TestCase(-540, 1)] + [TestCase(720, 2)] + [TestCase(-720, 2)] public void TestSpinSingleDirection(float amount, int expectedTicks) { performTest(new SpinFramesGenerator(time_spinner_start) @@ -134,8 +134,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSpinHalfBothDirections() { performTest(new SpinFramesGenerator(time_spinner_start) - .Spin(0.5f, 500) // Rotate to +0.5. - .Spin(-1f, 500) // Rotate to -0.5 + .Spin(180, 500) // Rotate to +0.5. + .Spin(-360, 500) // Rotate to -0.5 .Build()); assertTicksHit(0); @@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Spin in one direction then spin in the other. /// - [TestCase(0.5f, -1.5f, 1)] - [TestCase(-0.5f, 1.5f, 1)] - [TestCase(0.5f, -2.5f, 2)] - [TestCase(-0.5f, 2.5f, 2)] + [TestCase(180, -540, 1)] + [TestCase(-180, 540, 1)] + [TestCase(180, -900, 2)] + [TestCase(-180, 900, 2)] [Ignore("An upcoming implementation will fix this case")] public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) { @@ -168,11 +168,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 }); List frames = new SpinFramesGenerator(time_spinner_start) - .Spin(1f, 500) // 2000ms -> 1 full CW spin - .Spin(-0.5f, 500) // 2500ms -> 0.5 CCW spins - .Spin(0.25f, 500) // 3000ms -> 0.25 CW spins - .Spin(1.25f, 500) // 3500ms -> 1 full CW spin - .Spin(0.5f, 500) // 4000ms -> 0.5 CW spins + .Spin(360, 500) // 2000ms -> 1 full CW spin + .Spin(-180, 500) // 2500ms -> 0.5 CCW spins + .Spin(90, 500) // 3000ms -> 0.25 CW spins + .Spin(450, 500) // 3500ms -> 1 full CW spin + .Spin(180, 500) // 4000ms -> 0.5 CW spins .Build(); loadPlayer(frames); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs index 6a50f08508..8d8c2e9639 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests } private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) - .Spin(spins, time_spinner_end - time_spinner_start) + .Spin(spins * 360, time_spinner_end - time_spinner_start) .Build(); private void performTest(List frames) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 19413a50a8..3475680c71 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; frames.AddRange(new SpinFramesGenerator(time_spinner + 10) - .Spin(1, 500) + .Spin(360, 500) .Build()); performTest(hitObjects, frames); From 10bab614412a17752af5ae50bfba5c65ecf6538a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 19:23:35 +0900 Subject: [PATCH 23/25] Tidy up `lastAngle` usage and add assertion of maximum delta --- .../Default/SpinnerRotationTracker.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 77d410887c..174ba1c402 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -68,6 +69,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; + // Normalise the delta to -180 .. 180 + if (delta > 180) delta -= 360; + if (delta < -180) delta += 360; + if (Tracking) AddRotation(delta); @@ -84,8 +89,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Will be a no-op if not a valid time to spin. /// - /// The delta angle. - public void AddRotation(float angle) + /// The delta angle. + public void AddRotation(float delta) { if (!isSpinnableTime) return; @@ -96,21 +101,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default rotationTransferred = true; } - if (angle > 180) - { - lastAngle += 360; - angle -= 360; - } - else if (-angle > 180) - { - lastAngle -= 360; - angle += 360; - } + currentRotation += delta; + + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; + + Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - drawableSpinner.Result.TotalRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate); } private void resetState(DrawableHitObject obj) From 0bb95cfa88fb798f07ad7a35aca0ef61f755790d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 19:34:55 +0900 Subject: [PATCH 24/25] Fix incorrect initial rotation transfer value Should have been removed as part of https://github.com/ppy/osu/pull/24360. --- .../Skinning/Default/SpinnerRotationTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 174ba1c402..69c2bf3dd0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (!rotationTransferred) { - currentRotation = Rotation * 2; + currentRotation = Rotation; rotationTransferred = true; } From 3065c9f23dfa7f9018320392e03a29ea25dee28b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Oct 2023 22:49:41 +0900 Subject: [PATCH 25/25] Fix potential frame misordering in generator --- osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs index dbdfa1f258..e6dc72033a 100644 --- a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); // Final frame at the end of the current spin. - frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); lastTime = seqEndTime; lastAngle = seqEndAngle;