From 6da2a3d51fe441280af9536bca3fcf02662826ce Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:50:11 +0200 Subject: [PATCH 1/4] Add zero-length objects check and tests --- .../Checks/CheckZeroLengthObjectsTest.cs | 94 +++++++++++++++++++ .../Edit/Checks/CheckZeroLengthObjects.cs | 47 ++++++++++ 2 files changed, 141 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs new file mode 100644 index 0000000000..93b20cd166 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs @@ -0,0 +1,94 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckZeroLengthObjectsTest + { + private CheckZeroLengthObjects check; + + [SetUp] + public void Setup() + { + check = new CheckZeroLengthObjects(); + } + + [Test] + public void TestCircle() + { + assertOk(new List + { + new HitCircle { StartTime = 1000, Position = new Vector2(0, 0) } + }); + } + + [Test] + public void TestRegularSlider() + { + assertOk(new List + { + getSliderMock(1000).Object + }); + } + + [Test] + public void TestZeroLengthSlider() + { + assertZeroLength(new List + { + getSliderMock(0).Object + }); + } + + [Test] + public void TestNegativeLengthSlider() + { + assertZeroLength(new List + { + getSliderMock(-1000).Object + }); + } + + private Mock getSliderMock(double duration) + { + var mockSlider = new Mock(); + mockSlider.As().Setup(d => d.Duration).Returns(duration); + + return mockSlider; + } + + private void assertOk(List hitObjects) + { + Assert.That(check.Run(getContext(hitObjects)), Is.Empty); + } + + private void assertZeroLength(List hitObjects) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.First().Template is CheckZeroLengthObjects.IssueTemplateZeroLength); + } + + private BeatmapVerifierContext getContext(List hitObjects) + { + var beatmap = new Beatmap { HitObjects = hitObjects }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs new file mode 100644 index 0000000000..b9be94736b --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckZeroLengthObjects : ICheck + { + /// + /// The duration can be this low before being treated as having no length, in case of precision errors. Unit is milliseconds. + /// + private const double leniency = 0.5d; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Zero-length hitobjects"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateZeroLength(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + foreach (var hitObject in context.Beatmap.HitObjects) + { + if (!(hitObject is IHasDuration hasDuration)) + continue; + + if (hasDuration.Duration < leniency) + yield return new IssueTemplateZeroLength(this).Create(hitObject, hasDuration.Duration); + } + } + + public class IssueTemplateZeroLength : IssueTemplate + { + public IssueTemplateZeroLength(ICheck check) + : base(check, IssueType.Problem, "{0} has a duration of {1:0}.") + { + } + + public Issue Create(HitObject hitobject, double duration) => new Issue(hitobject, this, hitobject.GetType(), duration); + } + } +} From fec944830144eefcd2913a0d435ca507959a719f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:50:41 +0200 Subject: [PATCH 2/4] Add too short sliders check and tests --- .../Editor/Checks/CheckTooShortSlidersTest.cs | 145 ++++++++++++++++++ .../Edit/Checks/CheckTooShortSliders.cs | 48 ++++++ 2 files changed, 193 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs new file mode 100644 index 0000000000..2eab5a4ce6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs @@ -0,0 +1,145 @@ +// 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.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTooShortSlidersTest + { + private CheckTooShortSliders check; + + [SetUp] + public void Setup() + { + check = new CheckTooShortSliders(); + } + + [Test] + public void TestLongSlider() + { + Slider slider = new Slider + { + StartTime = 0, + RepeatCount = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0)), + new PathControlPoint(new Vector2(100, 0)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertOk(new List { slider }); + } + + [Test] + public void TestShortSlider() + { + Slider slider = new Slider + { + StartTime = 0, + RepeatCount = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0)), + new PathControlPoint(new Vector2(25, 0)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertOk(new List { slider }); + } + + [Test] + public void TestTooShortSliderExpert() + { + Slider slider = new Slider + { + StartTime = 0, + RepeatCount = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0)), + new PathControlPoint(new Vector2(10, 0)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertOk(new List { slider }, DifficultyRating.Expert); + } + + [Test] + public void TestTooShortSlider() + { + Slider slider = new Slider + { + StartTime = 0, + RepeatCount = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0)), + new PathControlPoint(new Vector2(10, 0)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertTooShort(new List { slider }); + } + + [Test] + public void TestTooShortSliderWithRepeats() + { + // Would be ok if we looked at the duration, but not if we look at the span duration. + Slider slider = new Slider + { + StartTime = 0, + RepeatCount = 2, + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0)), + new PathControlPoint(new Vector2(10, 0)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertTooShort(new List { slider }); + } + + private void assertOk(List hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + Assert.That(check.Run(getContext(hitObjects, difficultyRating)), Is.Empty); + } + + private void assertTooShort(List hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + var issues = check.Run(getContext(hitObjects, difficultyRating)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.First().Template is CheckTooShortSliders.IssueTemplateTooShort); + } + + private BeatmapVerifierContext getContext(List hitObjects, DifficultyRating difficultyRating) + { + var beatmap = new Beatmap { HitObjects = hitObjects }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs new file mode 100644 index 0000000000..159498c479 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckTooShortSliders : ICheck + { + /// + /// The shortest acceptable duration between the head and tail of the slider (so ignoring repeats). + /// + private const double span_duration_threshold = 125; // 240 BPM 1/2 + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short sliders"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + if (context.InterpretedDifficulty > DifficultyRating.Easy) + yield break; + + foreach (var hitObject in context.Beatmap.HitObjects) + { + if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold) + yield return new IssueTemplateTooShort(this).Create(slider); + } + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "This slider is too short ({0:0} ms), expected at least {1:0} ms.") + { + } + + public Issue Create(Slider slider) => new Issue(slider, this, slider.SpanDuration, span_duration_threshold); + } + } +} From 53c0298b5ea9689eba4eb33dfc29f43224e63e50 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:51:40 +0200 Subject: [PATCH 3/4] Add too short spinners check and tests --- .../Checks/CheckTooShortSpinnersTest.cs | 116 ++++++++++++++++++ .../Edit/Checks/CheckTooShortSpinners.cs | 61 +++++++++ 2 files changed, 177 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs new file mode 100644 index 0000000000..6a3f168ee1 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs @@ -0,0 +1,116 @@ +// 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.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTooShortSpinnersTest + { + private CheckTooShortSpinners check; + private BeatmapDifficulty difficulty; + + [SetUp] + public void Setup() + { + check = new CheckTooShortSpinners(); + difficulty = new BeatmapDifficulty(); + } + + [Test] + public void TestLongSpinner() + { + Spinner spinner = new Spinner { StartTime = 0, Duration = 4000 }; + spinner.ApplyDefaults(new ControlPointInfo(), difficulty); + + assertOk(new List { spinner }, difficulty); + } + + [Test] + public void TestShortSpinner() + { + Spinner spinner = new Spinner { StartTime = 0, Duration = 750 }; + spinner.ApplyDefaults(new ControlPointInfo(), difficulty); + + assertOk(new List { spinner }, difficulty); + } + + [Test] + public void TestVeryShortSpinner() + { + // Spinners at a certain duration only get 1000 points if approached by auto at a certain angle, making it difficult to determine. + Spinner spinner = new Spinner { StartTime = 0, Duration = 475 }; + spinner.ApplyDefaults(new ControlPointInfo(), difficulty); + + assertVeryShort(new List { spinner }, difficulty); + } + + [Test] + public void TestTooShortSpinner() + { + Spinner spinner = new Spinner { StartTime = 0, Duration = 400 }; + spinner.ApplyDefaults(new ControlPointInfo(), difficulty); + + assertTooShort(new List { spinner }, difficulty); + } + + [Test] + public void TestTooShortSpinnerVaryingOd() + { + const double duration = 450; + + var difficultyLowOd = new BeatmapDifficulty { OverallDifficulty = 1 }; + Spinner spinnerLowOd = new Spinner { StartTime = 0, Duration = duration }; + spinnerLowOd.ApplyDefaults(new ControlPointInfo(), difficultyLowOd); + + var difficultyHighOd = new BeatmapDifficulty { OverallDifficulty = 10 }; + Spinner spinnerHighOd = new Spinner { StartTime = 0, Duration = duration }; + spinnerHighOd.ApplyDefaults(new ControlPointInfo(), difficultyHighOd); + + assertOk(new List { spinnerLowOd }, difficultyLowOd); + assertTooShort(new List { spinnerHighOd }, difficultyHighOd); + } + + private void assertOk(List hitObjects, BeatmapDifficulty beatmapDifficulty) + { + Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty); + } + + private void assertVeryShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + { + var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort); + } + + private void assertTooShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + { + var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort); + } + + private BeatmapVerifierContext getContext(List hitObjects, BeatmapDifficulty beatmapDifficulty) + { + var beatmap = new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty } + }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs new file mode 100644 index 0000000000..0d0c3d9e69 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckTooShortSpinners : ICheck + { + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short spinners"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty; + + // These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner. + // It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners. + double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok. + double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok. + + foreach (var hitObject in context.Beatmap.HitObjects) + { + if (!(hitObject is Spinner spinner)) + continue; + + if (spinner.Duration < problemThreshold) + yield return new IssueTemplateTooShort(this).Create(spinner); + else if (spinner.Duration < warningThreshold) + yield return new IssueTemplateVeryShort(this).Create(spinner); + } + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "This spinner is too short. Auto cannot achieve 1000 points on this.") + { + } + + public Issue Create(Spinner spinner) => new Issue(spinner, this); + } + + public class IssueTemplateVeryShort : IssueTemplate + { + public IssueTemplateVeryShort(ICheck check) + : base(check, IssueType.Warning, "This spinner may be too short. Ensure auto can achieve 1000 points on this.") + { + } + + public Issue Create(Spinner spinner) => new Issue(spinner, this); + } + } +} From 3a5912e35eb03736be9cc6ca6883fa4617583e5c Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:53:25 +0200 Subject: [PATCH 4/4] Add new checks to verifiers --- osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs | 4 +++- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 896e904f3f..221723e4cd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -15,10 +15,12 @@ namespace osu.Game.Rulesets.Osu.Edit { // Compose new CheckOffscreenObjects(), + new CheckTooShortSpinners(), // Spread new CheckTimeDistanceEquality(), - new CheckLowDiffOverlaps() + new CheckLowDiffOverlaps(), + new CheckTooShortSliders(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 706eec226c..81f4808789 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Edit // Compose new CheckUnsnappedObjects(), - new CheckConcurrentObjects() + new CheckConcurrentObjects(), + new CheckZeroLengthObjects(), }; public IEnumerable Run(BeatmapVerifierContext context)