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..960469112f --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchLowestDiffDrainTime.cs @@ -0,0 +1,21 @@ +// 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; + +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, 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 new file mode 100644 index 0000000000..5e2223467d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaLowestDiffDrainTime.cs @@ -0,0 +1,21 @@ +// 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; + +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, 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.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..283f3b93af --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuLowestDiffDrainTime.cs @@ -0,0 +1,21 @@ +// 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; + +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, 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.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..8ef911c18e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs @@ -0,0 +1,21 @@ +// 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; + +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, 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.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.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs new file mode 100644 index 0000000000..6b46378c5a --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckLowestDiffDrainTimeTest.cs @@ -0,0 +1,264 @@ +// 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; +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.Rulesets.Osu; +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, + Ruleset = new OsuRuleset().RulesetInfo + }, + 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, + Ruleset = new OsuRuleset().RulesetInfo + }, + 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.Beatmap : 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, 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/Edit/BeatmapVerifierContext.cs b/osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs index 53bdf3140c..9761212b55 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 readonly IReadOnlyList BeatmapsetDifficulties; + + // 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; InterpretedDifficulty = difficultyRating; + + var beatmapSet = beatmap.BeatmapInfo.BeatmapSet; + + if (beatmapSet?.Beatmaps == null) + { + BeatmapsetDifficulties = new[] { beatmap }; + return; + } + + 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 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 new file mode 100644 index 0000000000..f4b9cc7ecb --- /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 + .Where(d => d.BeatmapInfo.Ruleset.Equals(context.Beatmap.BeatmapInfo.Ruleset)) + .ToList(); + + 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", + applicableThreshold.thresholdMs - thresholdReduction, + effectiveTime + ); + } + } + + 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 {2}, currently {3}.") + { + } + + public Issue Create(string lowestDiffLevel, string timeType, double requiredTime, double currentTime) + => new Issue(this, + lowestDiffLevel, + timeType, + TimeSpan.FromMilliseconds(requiredTime).ToString(@"m\:ss"), + TimeSpan.FromMilliseconds(currentTime).ToString(@"m\:ss")); + } + } +} diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index de7b760bcd..e2eeff9ad5 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,13 @@ 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).GetPlayableBeatmap(beatmapInfo.Ruleset) + ); + verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); RelativeSizeAxes = Axes.Both;