From f1eb7d367b562c22c208eed133a495ef2352af19 Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 01:53:07 +0100 Subject: [PATCH 01/10] include all of a beatmapset's diffs in the verifier context --- .../Rulesets/Edit/BeatmapVerifierContext.cs | 37 ++++++++++++++++++- osu.Game/Screens/Edit/Verify/IssueList.cs | 5 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs index 53bdf3140c..647c43a3f2 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs @@ -1,6 +1,8 @@ // 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.Beatmaps; namespace osu.Game.Rulesets.Edit @@ -26,11 +28,44 @@ namespace osu.Game.Rulesets.Edit /// public DifficultyRating InterpretedDifficulty; - public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus) + /// + /// All beatmap difficulties in the same beatmapset, including the current beatmap. + /// + public IReadOnlyList BeatmapsetDifficulties => beatmapsetDifficulties.Value; + + private readonly Lazy> beatmapsetDifficulties; + + public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus, Func? beatmapResolver = null) { Beatmap = beatmap; WorkingBeatmap = workingBeatmap; InterpretedDifficulty = difficultyRating; + + beatmapsetDifficulties = new Lazy>(() => + { + var beatmapSet = beatmap.BeatmapInfo.BeatmapSet; + if (beatmapSet?.Beatmaps == null) + return new[] { beatmap }; + + var difficulties = new List(); + + foreach (var beatmapInfo in beatmapSet.Beatmaps) + { + // Use the current beatmap if it matches this BeatmapInfo + if (beatmapInfo.Equals(beatmap.BeatmapInfo)) + { + difficulties.Add(beatmap); + continue; + } + + // Try to resolve other difficulties using the provided resolver + var working = beatmapResolver?.Invoke(beatmapInfo); + if (working?.Beatmap != null) + difficulties.Add(working.Beatmap); + } + + return difficulties; + }); } } } diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index de7b760bcd..62056e2ae1 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -33,6 +33,9 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private VerifyScreen verify { get; set; } + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private IBeatmapVerifier rulesetVerifier; private BeatmapVerifier generalVerifier; private BeatmapVerifierContext context; @@ -43,7 +46,7 @@ namespace osu.Game.Screens.Edit.Verify generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapVerifier(); - context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value); + context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value, beatmapInfo => beatmapManager.GetWorkingBeatmap(beatmapInfo)); verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); RelativeSizeAxes = Axes.Both; From ccf6d9c1733d874ad4a4fd49e199f78f6a176a18 Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 02:00:50 +0100 Subject: [PATCH 02/10] Add verify check for lowest diff drain time requirement --- .../Edit/CatchBeatmapVerifier.cs | 3 + .../Checks/CheckCatchLowestDiffDrainTime.cs | 20 +++++ .../Checks/CheckManiaLowestDiffDrainTime.cs | 20 +++++ .../Edit/ManiaBeatmapVerifier.cs | 3 + .../Checks/CheckOsuLowestDiffDrainTime.cs | 20 +++++ .../Edit/OsuBeatmapVerifier.cs | 1 + .../Checks/CheckTaikoLowestDiffDrainTime.cs | 20 +++++ .../Edit/TaikoBeatmapVerifier.cs | 3 + .../Edit/Checks/CheckLowestDiffDrainTime.cs | 88 +++++++++++++++++++ 9 files changed, 178 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs index 374ab16633..0783ec72e9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs @@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit new CheckBananaShowerGap(), new CheckConcurrentObjects(), + // Spread + new CheckCatchLowestDiffDrainTime(), + // Settings new CheckCatchAbnormalDifficultySettings(), }; diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs new file mode 100644 index 0000000000..70d806100f --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs @@ -0,0 +1,20 @@ +// 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.Checks; + +namespace osu.Game.Rulesets.Catch.Edit.Checks +{ + public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime + { + protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() + { + // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general + yield return (DifficultyRating.Hard, (2 * 60 + 30) * 1000, "Platter"); // 2:30 + yield return (DifficultyRating.Insane, (3 * 60 + 15) * 1000, "Rain"); // 3:15 + yield return (DifficultyRating.Expert, 4 * 60 * 1000, "Overdose"); // 4:00 + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs new file mode 100644 index 0000000000..4d8cf458b8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs @@ -0,0 +1,20 @@ +// 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.Checks; + +namespace osu.Game.Rulesets.Mania.Edit.Checks +{ + public class CheckManiaLowestDiffDrainTime : CheckLowestDiffDrainTime + { + protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() + { + // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21mania#rules + yield return (DifficultyRating.Hard, (2 * 60 + 30) * 1000, "Hard"); // 2:30 + yield return (DifficultyRating.Insane, (2 * 60 + 45) * 1000, "Insane"); // 2:45 + yield return (DifficultyRating.Expert, (3 * 60 + 30) * 1000, "Expert"); // 3:30 + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs index efb1d354af..17997ed463 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs @@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Mania.Edit // Compose new CheckManiaConcurrentObjects(), + // Spread + new CheckManiaLowestDiffDrainTime(), + // Settings new CheckKeyCount(), new CheckManiaAbnormalDifficultySettings(), diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs new file mode 100644 index 0000000000..400fe7d0fa --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs @@ -0,0 +1,20 @@ +// 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.Checks; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckOsuLowestDiffDrainTime : CheckLowestDiffDrainTime + { + protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() + { + // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21#general + yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Hard"); // 3:30 + yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Insane"); // 4:15 + yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Expert"); // 5:00 + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index c3796124b8..67fddfb8a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit new CheckTimeDistanceEquality(), new CheckLowDiffOverlaps(), new CheckTooShortSliders(), + new CheckOsuLowestDiffDrainTime(), // Settings new CheckOsuAbnormalDifficultySettings(), diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs new file mode 100644 index 0000000000..60a7cd2a5e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs @@ -0,0 +1,20 @@ +// 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.Checks; + +namespace osu.Game.Rulesets.Taiko.Edit.Checks +{ + public class CheckTaikoLowestDiffDrainTime : CheckLowestDiffDrainTime + { + protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() + { + // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21taiko#general + yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Muzukashii"); // 3:30 + yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Oni"); // 4:15 + yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Inner Oni"); // 5:00 + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs index 8f695c4834..23d0abed08 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit // Compose new CheckConcurrentObjects(), + // Spread + new CheckTaikoLowestDiffDrainTime(), + // Settings new CheckTaikoAbnormalDifficultySettings(), }; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs new file mode 100644 index 0000000000..47db1fc54b --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public abstract class CheckLowestDiffDrainTime : ICheck + { + /// + /// Defines the minimum drain time thresholds for different difficulty ratings. + /// + protected abstract IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds(); + + private const double break_time_leniency = 30 * 1000; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Spread, "Lowest difficulty too difficult for the given drain/play time(s)"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + IReadOnlyList difficulties = context.BeatmapsetDifficulties; + + if (difficulties.Count == 0) + yield break; + + var lowestDifficulty = difficulties.OrderBy(b => b.BeatmapInfo.StarRating).First(); + + // Get difficulty rating for the lowest difficulty + DifficultyRating lowestDifficultyRating = lowestDifficulty == context.Beatmap + ? context.InterpretedDifficulty + : StarDifficulty.GetDifficultyRating(lowestDifficulty.BeatmapInfo.StarRating); + + double drainTime = context.Beatmap.CalculateDrainLength(); + double playTime = context.Beatmap.CalculatePlayableLength(); + + bool isHighestDifficulty = difficulties.OrderByDescending(b => b.BeatmapInfo.StarRating).First() == context.Beatmap; + + // Use play time unless it's the highest difficulty and has significant breaks + bool canUsePlayTime = !isHighestDifficulty || context.Beatmap.TotalBreakTime < break_time_leniency; + + double effectiveTime = canUsePlayTime ? playTime : drainTime; + double thresholdReduction = canUsePlayTime ? 0 : break_time_leniency; + + // Check against thresholds based on the lowest difficulty's rating in the beatmapset + // Find the most appropriate threshold (highest rating that applies) + var applicableThreshold = GetThresholds() + .Where(t => lowestDifficultyRating >= t.rating) + .OrderByDescending(t => t.rating) + .FirstOrDefault(); + + if (applicableThreshold != default && effectiveTime < applicableThreshold.thresholdMs - thresholdReduction) + { + yield return new IssueTemplateTooShort(this).Create( + applicableThreshold.name, + canUsePlayTime ? "play" : "drain", + context.Beatmap.BeatmapInfo.DifficultyName, + applicableThreshold.thresholdMs - thresholdReduction, + effectiveTime + ); + } + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "With a lowest difficulty {0}, the {1} time of {2} must be at least {3}, currently {4}.") + { + } + + public Issue Create(string lowestDiffLevel, string timeType, string beatmapName, double requiredTime, double currentTime) + => new Issue(this, + lowestDiffLevel, + timeType, + beatmapName, + TimeSpan.FromMilliseconds(requiredTime).ToString(@"m\:ss"), + TimeSpan.FromMilliseconds(currentTime).ToString(@"m\:ss")); + } + } +} From 47bb254497f986e75e21a2850e79d9b938658f5d Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 02:01:00 +0100 Subject: [PATCH 03/10] add test coverage --- .../Checks/CheckLowestDiffDrainTimeTest.cs | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs new file mode 100644 index 0000000000..96f942fd8e --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs @@ -0,0 +1,260 @@ +// 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.Timing; +using osu.Game.Extensions; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckLowestDiffDrainTimeTest + { + private TestCheckLowestDiffDrainTime check = null!; + + [SetUp] + public void Setup() + { + check = new TestCheckLowestDiffDrainTime(); + } + + [Test] + public void TestSingleDifficultyMeetsRequirement() + { + var beatmap = createBeatmapWithDrainTime(4 * 60 * 1000, 3.5, "Hard"); // 4 minutes + assertOk(beatmap); + } + + [Test] + public void TestSingleDifficultyTooShort() + { + var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 3.5, "Hard"); // 2 minutes - too short for Hard + assertTooShort(beatmap); + } + + [Test] + public void TestHardDifficultyAtThreshold() + { + var beatmap = createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 3.5, "Hard"); // Exactly 3:30 + assertOk(beatmap); + } + + [Test] + public void TestHardDifficultyJustUnderThreshold() + { + var beatmap = createBeatmapWithDrainTime((3 * 60 + 29) * 1000, 3.5, "Hard"); // 3:29 - just under threshold + assertTooShort(beatmap); + } + + [Test] + public void TestInsaneDifficultyAtThreshold() + { + var beatmap = createBeatmapWithDrainTime((4 * 60 + 15) * 1000, 4.5, "Insane"); // Exactly 4:15 + assertOk(beatmap); + } + + [Test] + public void TestInsaneDifficultyTooShort() + { + var beatmap = createBeatmapWithDrainTime(4 * 60 * 1000, 4.5, "Insane"); // 4:00 - too short for Insane + assertTooShort(beatmap); + } + + [Test] + public void TestExpertDifficultyAtThreshold() + { + var beatmap = createBeatmapWithDrainTime(5 * 60 * 1000, 5.5, "Expert"); // Exactly 5:00 + assertOk(beatmap); + } + + [Test] + public void TestExpertDifficultyTooShort() + { + var beatmap = createBeatmapWithDrainTime((4 * 60 + 30) * 1000, 5.5, "Expert"); // 4:30 - too short for Expert + assertTooShort(beatmap); + } + + [Test] + public void TestEasyDifficultyMeetsRequirement() + { + var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 1.5, "Easy"); // 2 minutes - should be ok for Easy + assertOk(beatmap); + } + + [Test] + public void TestNormalDifficultyMeetsRequirement() + { + var beatmap = createBeatmapWithDrainTime(2 * 60 * 1000, 2.5, "Normal"); // 2 minutes - should be ok for Normal + assertOk(beatmap); + } + + [Test] + public void TestMultipleDifficultiesMeetsRequirement() + { + var difficulties = new List + { + createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 3.5, "Hard"), // Hard - lowest difficulty, 3:30 + createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 4.5, "Insane"), + createBeatmapWithDrainTime((3 * 60 + 30) * 1000, 5.5, "Expert") + }; + + // All should be ok because lowest difficulty is Hard and drain time meets Hard requirement + assertOkWithMultipleDifficulties(difficulties[0], difficulties); + assertOkWithMultipleDifficulties(difficulties[1], difficulties); + assertOkWithMultipleDifficulties(difficulties[2], difficulties); + } + + [Test] + public void TestMultipleDifficultiesTooShort() + { + var difficulties = new List + { + createBeatmapWithDrainTime(4 * 60 * 1000, 4.5, "Insane"), // Insane - lowest difficulty, 4:00 + createBeatmapWithDrainTime(4 * 60 * 1000, 5.5, "Expert") // Same drain time + }; + + // Should be too short because lowest difficulty is Insane and requires 4:15 + assertTooShortWithMultipleDifficulties(difficulties[0], difficulties); + assertTooShortWithMultipleDifficulties(difficulties[1], difficulties); + } + + [Test] + public void TestPlayTimeVsDrainTimeNotHighestDifficulty() + { + var expertBeatmap = createBeatmapWithPlayTime(5 * 60 * 1000, 5.5, "Expert"); // 5:00 play time + expertBeatmap.Breaks.Add(new BreakPeriod(60000, 100000)); // 40-second break + + var difficulties = new List + { + expertBeatmap, // Expert - 5:00 play, 4:20 drain + createBeatmapWithPlayTime(5 * 60 * 1000, 6.5, "ExpertPlus") // ExpertPlus - highest difficulty + }; + + // The Expert difficulty (not highest) should use play time (5:00) and pass the Expert requirement + assertOkWithMultipleDifficulties(difficulties[0], difficulties); + } + + [Test] + public void TestPlayTimeVsDrainTimeHighestDifficulty() + { + var expertBeatmap = createBeatmapWithPlayTime(5 * 60 * 1000, 5.5, "Expert"); // 5:00 play time + expertBeatmap.Breaks.Add(new BreakPeriod(60000, 100000)); // 40-second break + + // As the highest difficulty with breaks > 30s, it should use drain time and fail + assertTooShort(expertBeatmap); + } + + private IBeatmap createBeatmapWithDrainTime(double drainTimeMs, double starRating = 3.5, string difficultyName = "Default") + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + StarRating = starRating, + DifficultyName = difficultyName + }, + HitObjects = new List + { + new HitObject { StartTime = 0 }, + new HitObject { StartTime = drainTimeMs } // Last object at drain time + } + }; + + return beatmap; + } + + private IBeatmap createBeatmapWithPlayTime(double playTimeMs, double starRating = 3.5, string difficultyName = "Default") + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + StarRating = starRating, + DifficultyName = difficultyName + }, + HitObjects = new List + { + new HitObject { StartTime = 0 }, + new HitObject { StartTime = playTimeMs } // Last object at play time + } + }; + + return beatmap; + } + + private void assertOk(IBeatmap beatmap) + { + var difficultyRating = StarDifficulty.GetDifficultyRating(beatmap.BeatmapInfo.StarRating); + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + + Assert.That(check.Run(context), Is.Empty); + } + + private void assertTooShort(IBeatmap beatmap) + { + var difficultyRating = StarDifficulty.GetDifficultyRating(beatmap.BeatmapInfo.StarRating); + 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.First().Template is CheckLowestDiffDrainTime.IssueTemplateTooShort); + } + + private void assertOkWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable allDifficulties) + { + var context = createContextWithMultipleDifficulties(currentBeatmap, allDifficulties); + + Assert.That(check.Run(context), Is.Empty); + } + + private void assertTooShortWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable allDifficulties) + { + var context = createContextWithMultipleDifficulties(currentBeatmap, allDifficulties); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.First().Template is CheckLowestDiffDrainTime.IssueTemplateTooShort); + } + + private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IEnumerable allDifficulties) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmapInfos = allDifficulties.Select(d => d.BeatmapInfo).ToList(); + + // Set up the beatmapset with all difficulties + beatmapSet.Beatmaps.AddRange(beatmapInfos); + currentBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + + // Create a resolver that returns the appropriate working beatmap for each difficulty + var difficultyDict = allDifficulties.ToDictionary(d => d.BeatmapInfo, d => new TestWorkingBeatmap(d)); + + // Use the current beatmap's star rating to determine its difficulty rating + var currentDifficultyRating = StarDifficulty.GetDifficultyRating(currentBeatmap.BeatmapInfo.StarRating); + + return new BeatmapVerifierContext( + currentBeatmap, + new TestWorkingBeatmap(currentBeatmap), + currentDifficultyRating, + beatmapInfo => difficultyDict.TryGetValue(beatmapInfo, out var workingBeatmap) ? workingBeatmap : null + ); + } + + private class TestCheckLowestDiffDrainTime : CheckLowestDiffDrainTime + { + protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() + { + // Same thresholds as `CheckOsuLowestDiffDrainTime` for testing + yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Hard"); + yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Insane"); + yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Expert"); + } + } + } +} From 806995e951544edaa738063bb5e7681b13717a2b Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 14 Jul 2025 13:53:01 +0100 Subject: [PATCH 04/10] unlazy BeatmapsetDifficulties --- .../Rulesets/Edit/BeatmapVerifierContext.cs | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs index 647c43a3f2..9b4448a6f9 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs @@ -31,9 +31,7 @@ namespace osu.Game.Rulesets.Edit /// /// All beatmap difficulties in the same beatmapset, including the current beatmap. /// - public IReadOnlyList BeatmapsetDifficulties => beatmapsetDifficulties.Value; - - private readonly Lazy> beatmapsetDifficulties; + public readonly IReadOnlyList BeatmapsetDifficulties; public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus, Func? beatmapResolver = null) { @@ -41,31 +39,32 @@ namespace osu.Game.Rulesets.Edit WorkingBeatmap = workingBeatmap; InterpretedDifficulty = difficultyRating; - beatmapsetDifficulties = new Lazy>(() => + var beatmapSet = beatmap.BeatmapInfo.BeatmapSet; + + if (beatmapSet?.Beatmaps == null) { - var beatmapSet = beatmap.BeatmapInfo.BeatmapSet; - if (beatmapSet?.Beatmaps == null) - return new[] { beatmap }; + BeatmapsetDifficulties = new[] { beatmap }; + return; + } - var difficulties = new List(); + var difficulties = new List(); - foreach (var beatmapInfo in beatmapSet.Beatmaps) + foreach (var beatmapInfo in beatmapSet.Beatmaps) + { + // Use the current beatmap if it matches this BeatmapInfo + if (beatmapInfo.Equals(beatmap.BeatmapInfo)) { - // Use the current beatmap if it matches this BeatmapInfo - if (beatmapInfo.Equals(beatmap.BeatmapInfo)) - { - difficulties.Add(beatmap); - continue; - } - - // Try to resolve other difficulties using the provided resolver - var working = beatmapResolver?.Invoke(beatmapInfo); - if (working?.Beatmap != null) - difficulties.Add(working.Beatmap); + difficulties.Add(beatmap); + continue; } - return difficulties; - }); + // Try to resolve other difficulties using the provided resolver + var working = beatmapResolver?.Invoke(beatmapInfo); + if (working?.Beatmap != null) + difficulties.Add(working.Beatmap); + } + + BeatmapsetDifficulties = difficulties; } } } From 747bff1df9e671eff2a773dfd35c80eac5d0f8e5 Mon Sep 17 00:00:00 2001 From: Hivie Date: Wed, 16 Jul 2025 01:53:22 +0100 Subject: [PATCH 05/10] address review - add TODO for refactoring verifier context ctor - call `GetPlayableBeatmap()` in verifier context ctor - filter diffs with relevant ruleset in check logic - fix tests --- .../Editing/Checks/CheckLowestDiffDrainTimeTest.cs | 9 ++++++--- osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs | 9 +++++---- .../Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs | 4 +++- osu.Game/Screens/Edit/Verify/IssueList.cs | 11 ++++++++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs index 96f942fd8e..20213b13a4 100644 --- a/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs @@ -10,6 +10,7 @@ using osu.Game.Extensions; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Editing.Checks @@ -158,7 +159,8 @@ namespace osu.Game.Tests.Editing.Checks BeatmapInfo = new BeatmapInfo { StarRating = starRating, - DifficultyName = difficultyName + DifficultyName = difficultyName, + Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = new List { @@ -177,7 +179,8 @@ namespace osu.Game.Tests.Editing.Checks BeatmapInfo = new BeatmapInfo { StarRating = starRating, - DifficultyName = difficultyName + DifficultyName = difficultyName, + Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = new List { @@ -242,7 +245,7 @@ namespace osu.Game.Tests.Editing.Checks currentBeatmap, new TestWorkingBeatmap(currentBeatmap), currentDifficultyRating, - beatmapInfo => difficultyDict.TryGetValue(beatmapInfo, out var workingBeatmap) ? workingBeatmap : null + beatmapInfo => difficultyDict.TryGetValue(beatmapInfo, out var workingBeatmap) ? workingBeatmap.Beatmap : null ); } diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs index 9b4448a6f9..9761212b55 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs @@ -33,7 +33,8 @@ namespace osu.Game.Rulesets.Edit /// public readonly IReadOnlyList BeatmapsetDifficulties; - public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus, Func? beatmapResolver = null) + // TODO: Refactor this to have a simple constructor that only stores data and move the beatmap resolution logic to a static factory method. + public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus, Func? beatmapResolver = null) { Beatmap = beatmap; WorkingBeatmap = workingBeatmap; @@ -59,9 +60,9 @@ namespace osu.Game.Rulesets.Edit } // Try to resolve other difficulties using the provided resolver - var working = beatmapResolver?.Invoke(beatmapInfo); - if (working?.Beatmap != null) - difficulties.Add(working.Beatmap); + var resolvedBeatmap = beatmapResolver?.Invoke(beatmapInfo); + if (resolvedBeatmap != null) + difficulties.Add(resolvedBeatmap); } BeatmapsetDifficulties = difficulties; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs index 47db1fc54b..58346f7e3e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs @@ -27,7 +27,9 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - IReadOnlyList difficulties = context.BeatmapsetDifficulties; + IReadOnlyList difficulties = context.BeatmapsetDifficulties + .Where(d => d.BeatmapInfo.Ruleset.Equals(context.Beatmap.BeatmapInfo.Ruleset)) + .ToList(); if (difficulties.Count == 0) yield break; diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 62056e2ae1..6ef193fd79 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -46,7 +46,16 @@ namespace osu.Game.Screens.Edit.Verify generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapVerifier(); - context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value, beatmapInfo => beatmapManager.GetWorkingBeatmap(beatmapInfo)); + context = new BeatmapVerifierContext( + beatmap, + workingBeatmap.Value, + verify.InterpretedDifficulty.Value, + beatmapInfo => + beatmapManager + .GetWorkingBeatmap(beatmapInfo) + ?.GetPlayableBeatmap(beatmapInfo.Ruleset) + ); + verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); RelativeSizeAxes = Axes.Both; From 83ad34b718442a409d36e73042dabe1cba9343c9 Mon Sep 17 00:00:00 2001 From: Hivie Date: Wed, 16 Jul 2025 02:08:07 +0100 Subject: [PATCH 06/10] fix ci --- osu.Game/Screens/Edit/Verify/IssueList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 6ef193fd79..2c7d3932ad 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Verify beatmapInfo => beatmapManager .GetWorkingBeatmap(beatmapInfo) - ?.GetPlayableBeatmap(beatmapInfo.Ruleset) + .GetPlayableBeatmap(beatmapInfo.Ruleset) ); verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); From 72254226cf347b2fbecd056752ed3af8f97ba30b Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 21 Jul 2025 12:22:43 +0100 Subject: [PATCH 07/10] reduce whitespace --- osu.Game/Screens/Edit/Verify/IssueList.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 2c7d3932ad..e2eeff9ad5 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -50,10 +50,7 @@ namespace osu.Game.Screens.Edit.Verify beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value, - beatmapInfo => - beatmapManager - .GetWorkingBeatmap(beatmapInfo) - .GetPlayableBeatmap(beatmapInfo.Ruleset) + beatmapInfo => beatmapManager.GetWorkingBeatmap(beatmapInfo).GetPlayableBeatmap(beatmapInfo.Ruleset) ); verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); From 86369ab40344ef513b5066ef384eb657c48ff668 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 21 Jul 2025 12:32:45 +0100 Subject: [PATCH 08/10] use `TimeSpan` for defining thresholds --- .../Edit/Checks/CheckCatchLowestDiffDrainTime.cs | 7 ++++--- .../Edit/Checks/CheckManiaLowestDiffDrainTime.cs | 7 ++++--- .../Edit/Checks/CheckOsuLowestDiffDrainTime.cs | 7 ++++--- .../Edit/Checks/CheckTaikoLowestDiffDrainTime.cs | 7 ++++--- .../Editing/Checks/CheckLowestDiffDrainTimeTest.cs | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs index 70d806100f..960469112f 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.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; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks; @@ -12,9 +13,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general - yield return (DifficultyRating.Hard, (2 * 60 + 30) * 1000, "Platter"); // 2:30 - yield return (DifficultyRating.Insane, (3 * 60 + 15) * 1000, "Rain"); // 3:15 - yield return (DifficultyRating.Expert, 4 * 60 * 1000, "Overdose"); // 4:00 + yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose"); } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs index 4d8cf458b8..5e2223467d 100644 --- a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.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; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks; @@ -12,9 +13,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21mania#rules - yield return (DifficultyRating.Hard, (2 * 60 + 30) * 1000, "Hard"); // 2:30 - yield return (DifficultyRating.Insane, (2 * 60 + 45) * 1000, "Insane"); // 2:45 - yield return (DifficultyRating.Expert, (3 * 60 + 30) * 1000, "Expert"); // 3:30 + yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Hard"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 2, 45).TotalMilliseconds, "Insane"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 3, 30).TotalMilliseconds, "Expert"); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs index 400fe7d0fa..283f3b93af 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.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; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks; @@ -12,9 +13,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21#general - yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Hard"); // 3:30 - yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Insane"); // 4:15 - yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Expert"); // 5:00 + yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Hard"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Insane"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Expert"); } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs index 60a7cd2a5e..8ef911c18e 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.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; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks; @@ -12,9 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21taiko#general - yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Muzukashii"); // 3:30 - yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Oni"); // 4:15 - yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Inner Oni"); // 5:00 + yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Muzukashii"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Oni"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Inner Oni"); } } } diff --git a/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs index 20213b13a4..6b46378c5a 100644 --- a/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.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; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -254,9 +255,9 @@ namespace osu.Game.Tests.Editing.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // Same thresholds as `CheckOsuLowestDiffDrainTime` for testing - yield return (DifficultyRating.Hard, (3 * 60 + 30) * 1000, "Hard"); - yield return (DifficultyRating.Insane, (4 * 60 + 15) * 1000, "Insane"); - yield return (DifficultyRating.Expert, 5 * 60 * 1000, "Expert"); + yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Hard"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Insane"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Expert"); } } } From 9bab2444ea26d912cf0e3ac79bfdcd10959d514b Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 21 Jul 2025 13:18:25 +0100 Subject: [PATCH 09/10] adjust wording --- osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs index 58346f7e3e..641bd66f14 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs @@ -64,7 +64,6 @@ namespace osu.Game.Rulesets.Edit.Checks yield return new IssueTemplateTooShort(this).Create( applicableThreshold.name, canUsePlayTime ? "play" : "drain", - context.Beatmap.BeatmapInfo.DifficultyName, applicableThreshold.thresholdMs - thresholdReduction, effectiveTime ); @@ -74,15 +73,14 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateTooShort : IssueTemplate { public IssueTemplateTooShort(ICheck check) - : base(check, IssueType.Problem, "With a lowest difficulty {0}, the {1} time of {2} must be at least {3}, currently {4}.") + : base(check, IssueType.Problem, "With the lowest difficulty being \"{0}\", the {1} time of this difficulty must be at least {3}, currently {4}.") { } - public Issue Create(string lowestDiffLevel, string timeType, string beatmapName, double requiredTime, double currentTime) + public Issue Create(string lowestDiffLevel, string timeType, double requiredTime, double currentTime) => new Issue(this, lowestDiffLevel, timeType, - beatmapName, TimeSpan.FromMilliseconds(requiredTime).ToString(@"m\:ss"), TimeSpan.FromMilliseconds(currentTime).ToString(@"m\:ss")); } From 209a75c4df38bfcfbcb150516f483fbd37bc4835 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 21 Jul 2025 13:21:30 +0100 Subject: [PATCH 10/10] oops --- osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs index 641bd66f14..f4b9cc7ecb 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckLowestDiffDrainTime.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateTooShort : IssueTemplate { public IssueTemplateTooShort(ICheck check) - : base(check, IssueType.Problem, "With the lowest difficulty being \"{0}\", the {1} time of this difficulty must be at least {3}, currently {4}.") + : base(check, IssueType.Problem, "With the lowest difficulty being \"{0}\", the {1} time of this difficulty must be at least {2}, currently {3}.") { }