From fbfed167565b3a8646ad6915d9d04f71f7eae42d Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Tue, 9 Nov 2021 23:05:25 +0100 Subject: [PATCH 1/5] Started on implementing a spinner gap check for catch --- .idea/.idea.osu.Desktop/.idea/indexLayout.xml | 5 + osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 + .../Edit/CatchBeatmapVerifier.cs | 24 +++++ .../Edit/Checks/CheckTooShortSpinnerGap.cs | 91 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml index 7b08163ceb..90e8a163b9 100644 --- a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml +++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml @@ -1,5 +1,10 @@ + + + + + diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9fee6b2bc1..6b26a915dd 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -188,5 +188,7 @@ namespace osu.Game.Rulesets.Catch public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); + + public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs new file mode 100644 index 0000000000..6aa1749ff9 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs @@ -0,0 +1,24 @@ +// 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 osu.Game.Rulesets.Catch.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchBeatmapVerifier : IBeatmapVerifier + { + private readonly List checks = new List + { + new CheckTooShortSpinnerGap() + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + return checks.SelectMany(check => check.Run(context)); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs new file mode 100644 index 0000000000..7b0b5c74ce --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs @@ -0,0 +1,91 @@ +// 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.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Catch.Edit.Checks +{ + public class CheckTooShortSpinnerGap : ICheck + { + private static readonly int[] spinner_start_delta_threshold = { 250, 250, 125, 125, 62, 62 }; + private static readonly int[] spinner_end_delta_threshold = { 250, 250, 250, 125, 125, 125 }; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Too short spinner gap"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateSpinnerStartGap(this), + new IssueTemplateSpinnerEndGap(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var hitObjects = context.Beatmap.HitObjects; + int interpretedDifficulty = (int)context.InterpretedDifficulty; + int expectedStartDelta = spinner_start_delta_threshold[interpretedDifficulty]; + int expectedEndDelta = spinner_end_delta_threshold[interpretedDifficulty]; + + for (int i = 0; i < hitObjects.Count - 1; ++i) + { + if (!(hitObjects[i] is BananaShower bananaShower)) + continue; + + if (i != 0 && hitObjects[i - 1] is CatchHitObject previousHitObject && !(previousHitObject is BananaShower)) + { + double spinnerStartDelta = bananaShower.StartTime - previousHitObject.GetEndTime(); + + if (spinnerStartDelta < expectedStartDelta) + { + yield return new IssueTemplateSpinnerStartGap(this) + .Create(spinnerStartDelta, expectedStartDelta, bananaShower, previousHitObject); + } + } + + if (hitObjects[i + 1] is CatchHitObject nextHitObject && !(nextHitObject is BananaShower)) + { + double spinnerEndDelta = nextHitObject.StartTime - bananaShower.EndTime; + + if (spinnerEndDelta < expectedEndDelta) + { + yield return new IssueTemplateSpinnerEndGap(this) + .Create(spinnerEndDelta, expectedEndDelta, bananaShower, nextHitObject); + } + } + } + } + + public abstract class IssueTemplateSpinnerGap : IssueTemplate + { + protected IssueTemplateSpinnerGap(ICheck check, IssueType issueType, string unformattedMessage) + : base(check, issueType, unformattedMessage) + { + } + + public Issue Create(double deltaTime, int expectedDeltaTime, params HitObject[] hitObjects) + { + return new Issue(hitObjects, this, Math.Floor(deltaTime), expectedDeltaTime); + } + } + + public class IssueTemplateSpinnerStartGap : IssueTemplateSpinnerGap + { + public IssueTemplateSpinnerStartGap(ICheck check) + : base(check, IssueType.Problem, "There is only {0} ms apart between the start of the spinner and the last object, there should be {1} ms or more.") + { + } + } + + public class IssueTemplateSpinnerEndGap : IssueTemplateSpinnerGap + { + public IssueTemplateSpinnerEndGap(ICheck check) + : base(check, IssueType.Problem, "There is only {0} ms apart between the end of the spinner and the next object, there should be {1} ms or more.") + { + } + } + } +} From 5d8f35f3c973a941a017642cf09ba3f167ab7bd2 Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Wed, 10 Nov 2021 00:16:29 +0100 Subject: [PATCH 2/5] Code cleanup and added tests for the spinner check --- .../Editor/Checks/TestCheckBananaShowerGap.cs | 118 ++++++++++++++++++ .../Edit/CatchBeatmapVerifier.cs | 2 +- ...tSpinnerGap.cs => CheckBananaShowerGap.cs} | 31 +++-- 3 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/Checks/TestCheckBananaShowerGap.cs rename osu.Game.Rulesets.Catch/Edit/Checks/{CheckTooShortSpinnerGap.cs => CheckBananaShowerGap.cs} (71%) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/TestCheckBananaShowerGap.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/TestCheckBananaShowerGap.cs new file mode 100644 index 0000000000..055c8429d7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/TestCheckBananaShowerGap.cs @@ -0,0 +1,118 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Edit.Checks; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks +{ + [TestFixture] + public class TestCheckBananaShowerGap + { + private CheckBananaShowerGap check; + + [SetUp] + public void Setup() + { + check = new CheckBananaShowerGap(); + } + + [Test] + public void TestAllowedSpinnerGaps() + { + assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Easy); + assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Normal); + assertOk(mockBeatmap(125, 1000, 1250), DifficultyRating.Hard); + assertOk(mockBeatmap(125, 1000, 1125), DifficultyRating.Insane); + assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.Expert); + assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.ExpertPlus); + } + + [Test] + public void TestDisallowedSpinnerGapStart() + { + assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Easy); + assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Normal); + assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Hard); + assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Insane); + assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.Expert); + assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.ExpertPlus); + } + + [Test] + public void TestDisallowedSpinnerGapEnd() + { + assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Easy); + assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Normal); + assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1249), DifficultyRating.Hard); + assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1124), DifficultyRating.Insane); + assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.Expert); + assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.ExpertPlus); + } + + [Test] + public void TestConsecutiveSpinners() + { + var spinnerConsecutiveBeatmap = new Beatmap + { + HitObjects = new List + { + new BananaShower { StartTime = 0, EndTime = 100, X = 0 }, + new BananaShower { StartTime = 101, EndTime = 200, X = 0 }, + new BananaShower { StartTime = 201, EndTime = 300, X = 0 } + } + }; + + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Easy); + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Normal); + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Hard); + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Insane); + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Expert); + assertOk(spinnerConsecutiveBeatmap, DifficultyRating.ExpertPlus); + } + + private Beatmap mockBeatmap(double bananaShowerStart, double bananaShowerEnd, double nextFruitStart) + { + return new Beatmap + { + HitObjects = new List + { + new Fruit { StartTime = 0, X = 0 }, + new BananaShower { StartTime = bananaShowerStart, EndTime = bananaShowerEnd, X = 0 }, + new Fruit { StartTime = nextFruitStart, X = 0 } + } + }; + } + + private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + Assert.That(check.Run(context), Is.Empty); + } + + private void assertTooShortSpinnerStart(IBeatmap beatmap, DifficultyRating difficultyRating) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerStartGap)); + } + + private void assertTooShortSpinnerEnd(IBeatmap beatmap, DifficultyRating difficultyRating) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerEndGap)); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs index 6aa1749ff9..c7a41a4e22 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Edit { private readonly List checks = new List { - new CheckTooShortSpinnerGap() + new CheckBananaShowerGap() }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs similarity index 71% rename from osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs rename to osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs index 7b0b5c74ce..909a3110b1 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckTooShortSpinnerGap.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs @@ -10,7 +10,10 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Edit.Checks { - public class CheckTooShortSpinnerGap : ICheck + /// + /// Check the spinner/banana shower gaps specified in the osu!catch difficulty specific ranking criteria. + /// + public class CheckBananaShowerGap : ICheck { private static readonly int[] spinner_start_delta_threshold = { 250, 250, 125, 125, 62, 62 }; private static readonly int[] spinner_end_delta_threshold = { 250, 250, 250, 125, 125, 125 }; @@ -19,8 +22,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks public IEnumerable PossibleTemplates => new IssueTemplate[] { - new IssueTemplateSpinnerStartGap(this), - new IssueTemplateSpinnerEndGap(this) + new IssueTemplateBananaShowerStartGap(this), + new IssueTemplateBananaShowerEndGap(this) }; public IEnumerable Run(BeatmapVerifierContext context) @@ -35,33 +38,35 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks if (!(hitObjects[i] is BananaShower bananaShower)) continue; + // Skip if the previous hitobject is a banana shower, consecutive spinners are allowed if (i != 0 && hitObjects[i - 1] is CatchHitObject previousHitObject && !(previousHitObject is BananaShower)) { double spinnerStartDelta = bananaShower.StartTime - previousHitObject.GetEndTime(); if (spinnerStartDelta < expectedStartDelta) { - yield return new IssueTemplateSpinnerStartGap(this) + yield return new IssueTemplateBananaShowerStartGap(this) .Create(spinnerStartDelta, expectedStartDelta, bananaShower, previousHitObject); } } + // Skip if the next hitobject is a banana shower, consecutive spinners are allowed if (hitObjects[i + 1] is CatchHitObject nextHitObject && !(nextHitObject is BananaShower)) { double spinnerEndDelta = nextHitObject.StartTime - bananaShower.EndTime; if (spinnerEndDelta < expectedEndDelta) { - yield return new IssueTemplateSpinnerEndGap(this) + yield return new IssueTemplateBananaShowerEndGap(this) .Create(spinnerEndDelta, expectedEndDelta, bananaShower, nextHitObject); } } } } - public abstract class IssueTemplateSpinnerGap : IssueTemplate + public abstract class IssueTemplateBananaShowerGap : IssueTemplate { - protected IssueTemplateSpinnerGap(ICheck check, IssueType issueType, string unformattedMessage) + protected IssueTemplateBananaShowerGap(ICheck check, IssueType issueType, string unformattedMessage) : base(check, issueType, unformattedMessage) { } @@ -72,18 +77,18 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks } } - public class IssueTemplateSpinnerStartGap : IssueTemplateSpinnerGap + public class IssueTemplateBananaShowerStartGap : IssueTemplateBananaShowerGap { - public IssueTemplateSpinnerStartGap(ICheck check) - : base(check, IssueType.Problem, "There is only {0} ms apart between the start of the spinner and the last object, there should be {1} ms or more.") + public IssueTemplateBananaShowerStartGap(ICheck check) + : base(check, IssueType.Problem, "There is only {0} ms apart between the start of the spinner and the last object, it should not be less than {1} ms.") { } } - public class IssueTemplateSpinnerEndGap : IssueTemplateSpinnerGap + public class IssueTemplateBananaShowerEndGap : IssueTemplateBananaShowerGap { - public IssueTemplateSpinnerEndGap(ICheck check) - : base(check, IssueType.Problem, "There is only {0} ms apart between the end of the spinner and the next object, there should be {1} ms or more.") + public IssueTemplateBananaShowerEndGap(ICheck check) + : base(check, IssueType.Problem, "There is only {0} ms apart between the end of the spinner and the next object, it should not be less than {1} ms.") { } } From bd5caceeb1f2d95d40339fe08af3720d20749967 Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Wed, 10 Nov 2021 00:23:14 +0100 Subject: [PATCH 3/5] Fixed typo in banana shower gap check message --- osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs index 909a3110b1..d6671cb9db 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks public class IssueTemplateBananaShowerStartGap : IssueTemplateBananaShowerGap { public IssueTemplateBananaShowerStartGap(ICheck check) - : base(check, IssueType.Problem, "There is only {0} ms apart between the start of the spinner and the last object, it should not be less than {1} ms.") + : base(check, IssueType.Problem, "There is only {0} ms between the start of the spinner and the last object, it should not be less than {1} ms.") { } } @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks public class IssueTemplateBananaShowerEndGap : IssueTemplateBananaShowerGap { public IssueTemplateBananaShowerEndGap(ICheck check) - : base(check, IssueType.Problem, "There is only {0} ms apart between the end of the spinner and the next object, it should not be less than {1} ms.") + : base(check, IssueType.Problem, "There is only {0} ms between the end of the spinner and the next object, it should not be less than {1} ms.") { } } From 4e092fed0f472a0755fe650b4f05ac18e3dae65e Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Wed, 10 Nov 2021 00:35:09 +0100 Subject: [PATCH 4/5] Removed accidentally added Rider legacy UserContentModel --- .idea/.idea.osu.Desktop/.idea/indexLayout.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml index 90e8a163b9..7b08163ceb 100644 --- a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml +++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml @@ -1,10 +1,5 @@ - - - - - From d370f64ac3e83ece673948b6d9d59804814da615 Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Wed, 10 Nov 2021 19:58:36 +0100 Subject: [PATCH 5/5] Changed finding the spinner gaps via a dictionary instead of getting the thresholds via an array --- .../Edit/Checks/CheckBananaShowerGap.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs index d6671cb9db..4b2933c0e1 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckBananaShowerGap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; @@ -15,8 +16,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks /// public class CheckBananaShowerGap : ICheck { - private static readonly int[] spinner_start_delta_threshold = { 250, 250, 125, 125, 62, 62 }; - private static readonly int[] spinner_end_delta_threshold = { 250, 250, 250, 125, 125, 125 }; + private static readonly Dictionary spinner_delta_threshold = new Dictionary + { + [DifficultyRating.Easy] = (250, 250), + [DifficultyRating.Normal] = (250, 250), + [DifficultyRating.Hard] = (125, 250), + [DifficultyRating.Insane] = (125, 125), + [DifficultyRating.Expert] = (62, 125), + [DifficultyRating.ExpertPlus] = (62, 125) + }; public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Too short spinner gap"); @@ -29,9 +37,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { var hitObjects = context.Beatmap.HitObjects; - int interpretedDifficulty = (int)context.InterpretedDifficulty; - int expectedStartDelta = spinner_start_delta_threshold[interpretedDifficulty]; - int expectedEndDelta = spinner_end_delta_threshold[interpretedDifficulty]; + (int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty]; for (int i = 0; i < hitObjects.Count - 1; ++i) {