From b4dc39412750b350a33f94fdf61c9016289892d3 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 16:01:28 +0100 Subject: [PATCH 01/26] add check for inconsistent timing --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../CheckInconsistentTimingControlPoints.cs | 162 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 868835342a..c0fc400a53 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Edit // Timing new CheckPreviewTime(), + new CheckInconsistentTimingControlPoints(), // Events new CheckBreaks(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs new file mode 100644 index 0000000000..b8694c52cc --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs @@ -0,0 +1,162 @@ +// 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.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckInconsistentTimingControlPoints : ICheck + { + // Small tolerance for floating point comparison + private const double timing_tolerance = 0.01; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Timing, "Inconsistent timing control points"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateMissingTimingPoint(this), + new IssueTemplateExtraTimingPoint(this), + new IssueTemplateMissingTimingPointMinor(this), + new IssueTemplateInconsistentMeter(this), + new IssueTemplateInconsistentBPM(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var difficulties = context.BeatmapsetDifficulties; + + if (difficulties.Count <= 1) + yield break; + + // Use the current difficulty as reference + var referenceBeatmap = context.Beatmap; + var referenceTimingPoints = referenceBeatmap.ControlPointInfo.TimingPoints; + + foreach (var beatmap in difficulties) + { + if (beatmap == referenceBeatmap) + continue; + + var timingPoints = beatmap.ControlPointInfo.TimingPoints; + + // Check each timing point in the reference against this difficulty + foreach (var referencePoint in referenceTimingPoints) + { + var matchingPoint = findMatchingTimingPoint(timingPoints, referencePoint.Time); + var exactMatchingPoint = findExactMatchingTimingPoint(timingPoints, referencePoint.Time); + + if (matchingPoint == null) + { + yield return new IssueTemplateMissingTimingPoint(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + else + { + // Check for meter signature inconsistency + if (!referencePoint.TimeSignature.Equals(matchingPoint.TimeSignature)) + { + yield return new IssueTemplateInconsistentMeter(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + + // Check for BPM inconsistency + if (Math.Abs(referencePoint.BeatLength - matchingPoint.BeatLength) > timing_tolerance) + { + yield return new IssueTemplateInconsistentBPM(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + + // Check for exact timing match (decimal precision) + if (exactMatchingPoint == null) + { + yield return new IssueTemplateMissingTimingPointMinor(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + } + } + + // Check timing points in this difficulty that aren't in the reference + foreach (var timingPoint in timingPoints) + { + var matchingReferencePoint = findMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); + var exactMatchingReferencePoint = findExactMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); + + if (matchingReferencePoint == null) + { + yield return new IssueTemplateExtraTimingPoint(this).Create(timingPoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + else if (exactMatchingReferencePoint == null) + { + yield return new IssueTemplateMissingTimingPointMinor(this).Create(timingPoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + } + } + } + + private static TimingControlPoint? findMatchingTimingPoint(IEnumerable timingPoints, double time) + { + return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, Math.Round(time), 1.0)); + } + + private static TimingControlPoint? findExactMatchingTimingPoint(IEnumerable timingPoints, double time) + { + return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, time, timing_tolerance)); + } + + public class IssueTemplateMissingTimingPoint : IssueTemplate + { + public IssueTemplateMissingTimingPoint(ICheck check) + : base(check, IssueType.Problem, "Missing timing control point in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + + public class IssueTemplateExtraTimingPoint : IssueTemplate + { + public IssueTemplateExtraTimingPoint(ICheck check) + : base(check, IssueType.Problem, "Extra timing control point in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + + public class IssueTemplateMissingTimingPointMinor : IssueTemplate + { + public IssueTemplateMissingTimingPointMinor(ICheck check) + : base(check, IssueType.Negligible, "Timing control point has decimally different offset in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + + public class IssueTemplateInconsistentMeter : IssueTemplate + { + public IssueTemplateInconsistentMeter(ICheck check) + : base(check, IssueType.Problem, "Inconsistent time signature in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + + public class IssueTemplateInconsistentBPM : IssueTemplate + { + public IssueTemplateInconsistentBPM(ICheck check) + : base(check, IssueType.Problem, "Inconsistent BPM in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + } +} From 3a44e1d5be324f70db28e12ca9c6bad4e2f9d79d Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 16:03:05 +0100 Subject: [PATCH 02/26] add tests --- ...heckInconsistentTimingControlPointsTest.cs | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckInconsistentTimingControlPointsTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckInconsistentTimingControlPointsTest.cs b/osu.Game.Tests/Editing/Checks/CheckInconsistentTimingControlPointsTest.cs new file mode 100644 index 0000000000..899a59a24f --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckInconsistentTimingControlPointsTest.cs @@ -0,0 +1,256 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; +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 CheckInconsistentTimingControlPointsTest + { + private CheckInconsistentTimingControlPoints check = null!; + + [SetUp] + public void Setup() + { + check = new CheckInconsistentTimingControlPoints(); + } + + [Test] + public void TestConsistentTiming() + { + var beatmaps = createBeatmapSetWithTiming( + new[] { 1000.0, 2000.0 }, // Timing at 1000ms and 2000ms + new[] { 1000.0, 2000.0 } // Same timing + ); + + var context = createContext(beatmaps[0], beatmaps); + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestMissingTimingPoint() + { + var beatmaps = createBeatmapSetWithTiming( + new[] { 1000.0, 2000.0 }, // Reference has timing at 1000ms and 2000ms + new[] { 1000.0 } // Second difficulty missing timing at 2000ms + ); + + var context = createContext(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckInconsistentTimingControlPoints.IssueTemplateMissingTimingPoint)); + } + + [Test] + public void TestInconsistentBPM() + { + var beatmaps = createBeatmapSetWithBPM( + new[] { (1000.0, 500.0) }, // Reference: 120 BPM (500ms beat length) + new[] { (1000.0, 600.0) } // Second: 100 BPM (600ms beat length) + ); + + var context = createContext(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckInconsistentTimingControlPoints.IssueTemplateInconsistentBPM)); + } + + [Test] + public void TestInconsistentMeter() + { + var beatmaps = createBeatmapSetWithMeter( + new[] { (1000.0, TimeSignature.SimpleQuadruple) }, // Reference: 4/4 + new[] { (1000.0, TimeSignature.SimpleTriple) } // Second: 3/4 + ); + + var context = createContext(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckInconsistentTimingControlPoints.IssueTemplateInconsistentMeter)); + } + + [Test] + public void TestDecimalOffset() + { + var beatmaps = createBeatmapSetWithTiming( + new[] { 1000.0 }, // Reference at exactly 1000ms + new[] { 1000.5 } // Second at 1000.5ms (decimal difference) + ); + + var context = createContext(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.All(issue => issue.Template is CheckInconsistentTimingControlPoints.IssueTemplateMissingTimingPointMinor)); + } + + [Test] + public void TestSingleDifficulty() + { + var beatmaps = createBeatmapSetWithTiming( + new[] { 1000.0, 2000.0 } // Only one difficulty + ); + + var context = createContext(beatmaps[0], beatmaps); + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestExtraTimingPoint() + { + var beatmaps = createBeatmapSetWithTiming( + new[] { 1000.0 }, // Reference has timing at 1000ms + new[] { 1000.0, 2000.0 } // Second has additional timing at 2000ms + ); + + var context = createContext(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.All(issue => issue.Template is CheckInconsistentTimingControlPoints.IssueTemplateExtraTimingPoint)); + } + + private IBeatmap[] createBeatmapSetWithTiming(params double[][] timingPoints) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmaps = new IBeatmap[timingPoints.Length]; + + for (int i = 0; i < timingPoints.Length; i++) + { + beatmaps[i] = createBeatmapWithTiming(timingPoints[i], $"Difficulty {i + 1}"); + beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet; + } + + foreach (var beatmap in beatmaps) + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + + return beatmaps; + } + + private IBeatmap[] createBeatmapSetWithBPM(params (double time, double beatLength)[][] timingData) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmaps = new IBeatmap[timingData.Length]; + + for (int i = 0; i < timingData.Length; i++) + { + beatmaps[i] = createBeatmapWithBPM(timingData[i], $"Difficulty {i + 1}"); + beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet; + } + + foreach (var beatmap in beatmaps) + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + + return beatmaps; + } + + private IBeatmap[] createBeatmapSetWithMeter(params (double time, TimeSignature meter)[][] timingData) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmaps = new IBeatmap[timingData.Length]; + + for (int i = 0; i < timingData.Length; i++) + { + beatmaps[i] = createBeatmapWithMeter(timingData[i], $"Difficulty {i + 1}"); + beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet; + } + + foreach (var beatmap in beatmaps) + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + + return beatmaps; + } + + private IBeatmap createBeatmapWithTiming(double[] timingPoints, string difficultyName) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = difficultyName, + Metadata = new BeatmapMetadata() + }, + ControlPointInfo = new ControlPointInfo() + }; + + foreach (double time in timingPoints) + { + beatmap.ControlPointInfo.Add(time, new TimingControlPoint + { + BeatLength = 500 // 120 BPM + }); + } + + return beatmap; + } + + private IBeatmap createBeatmapWithBPM((double time, double beatLength)[] timingData, string difficultyName) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = difficultyName, + Metadata = new BeatmapMetadata() + }, + ControlPointInfo = new ControlPointInfo() + }; + + foreach ((double time, double beatLength) in timingData) + { + beatmap.ControlPointInfo.Add(time, new TimingControlPoint + { + BeatLength = beatLength + }); + } + + return beatmap; + } + + private IBeatmap createBeatmapWithMeter((double time, TimeSignature meter)[] timingData, string difficultyName) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = difficultyName, + Metadata = new BeatmapMetadata() + }, + ControlPointInfo = new ControlPointInfo() + }; + + foreach ((double time, var meter) in timingData) + { + beatmap.ControlPointInfo.Add(time, new TimingControlPoint + { + BeatLength = 500, // 120 BPM + TimeSignature = meter + }); + } + + return beatmap; + } + + private BeatmapVerifierContext createContext(IBeatmap currentBeatmap, IBeatmap[] allDifficulties) + { + return new BeatmapVerifierContext( + currentBeatmap, + new TestWorkingBeatmap(currentBeatmap), + DifficultyRating.ExpertPlus, + beatmapInfo => allDifficulties.FirstOrDefault(b => b.BeatmapInfo.Equals(beatmapInfo)) + ); + } + } +} From 0fcef7b0ee509869c175a677a28585f277cd4965 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 18:37:54 +0100 Subject: [PATCH 03/26] move methods to `TimingCheckUtils` so they can be reused for future timing-related checks --- .../CheckInconsistentTimingControlPoints.cs | 26 +++---------- .../Checks/Components/TimingCheckUtils.cs | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs index b8694c52cc..bbed49d7ee 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs @@ -3,18 +3,12 @@ using System; using System.Collections.Generic; -using System.Linq; -using osu.Framework.Utils; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { public class CheckInconsistentTimingControlPoints : ICheck { - // Small tolerance for floating point comparison - private const double timing_tolerance = 0.01; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Timing, "Inconsistent timing control points"); public IEnumerable PossibleTemplates => new IssueTemplate[] @@ -47,8 +41,8 @@ namespace osu.Game.Rulesets.Edit.Checks // Check each timing point in the reference against this difficulty foreach (var referencePoint in referenceTimingPoints) { - var matchingPoint = findMatchingTimingPoint(timingPoints, referencePoint.Time); - var exactMatchingPoint = findExactMatchingTimingPoint(timingPoints, referencePoint.Time); + var matchingPoint = TimingCheckUtils.FindMatchingTimingPoint(timingPoints, referencePoint.Time); + var exactMatchingPoint = TimingCheckUtils.FindExactMatchingTimingPoint(timingPoints, referencePoint.Time); if (matchingPoint == null) { @@ -63,7 +57,7 @@ namespace osu.Game.Rulesets.Edit.Checks } // Check for BPM inconsistency - if (Math.Abs(referencePoint.BeatLength - matchingPoint.BeatLength) > timing_tolerance) + if (Math.Abs(referencePoint.BeatLength - matchingPoint.BeatLength) > TimingCheckUtils.TIMING_TOLERANCE) { yield return new IssueTemplateInconsistentBPM(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); } @@ -79,8 +73,8 @@ namespace osu.Game.Rulesets.Edit.Checks // Check timing points in this difficulty that aren't in the reference foreach (var timingPoint in timingPoints) { - var matchingReferencePoint = findMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); - var exactMatchingReferencePoint = findExactMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); + var matchingReferencePoint = TimingCheckUtils.FindMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); + var exactMatchingReferencePoint = TimingCheckUtils.FindExactMatchingTimingPoint(referenceTimingPoints, timingPoint.Time); if (matchingReferencePoint == null) { @@ -94,16 +88,6 @@ namespace osu.Game.Rulesets.Edit.Checks } } - private static TimingControlPoint? findMatchingTimingPoint(IEnumerable timingPoints, double time) - { - return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, Math.Round(time), 1.0)); - } - - private static TimingControlPoint? findExactMatchingTimingPoint(IEnumerable timingPoints, double time) - { - return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, time, timing_tolerance)); - } - public class IssueTemplateMissingTimingPoint : IssueTemplate { public IssueTemplateMissingTimingPoint(ICheck check) diff --git a/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs new file mode 100644 index 0000000000..1ddbeb31d6 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs @@ -0,0 +1,39 @@ +// 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.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Edit.Checks.Components +{ + public static class TimingCheckUtils + { + // Small tolerance for floating point comparison + public const double TIMING_TOLERANCE = 0.01; + + /// + /// Finds a timing control point that starts at approximately the same time (within 1ms after rounding). + /// + /// The collection of timing points to search. + /// The time to match against. + /// The matching timing control point, or null if none found. + public static TimingControlPoint? FindMatchingTimingPoint(IEnumerable timingPoints, double time) + { + return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, Math.Round(time), 1.0)); + } + + /// + /// Finds a timing control point that starts at precisely the same time (within timing tolerance). + /// + /// The collection of timing points to search. + /// The time to match against. + /// The exact matching timing control point, or null if none found. + public static TimingControlPoint? FindExactMatchingTimingPoint(IEnumerable timingPoints, double time) + { + return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, time, TIMING_TOLERANCE)); + } + } +} From 7f8b6981b25900989934a9ca33857c9e879c5d49 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 21:39:46 +0100 Subject: [PATCH 04/26] add class for general checks --- .../Rulesets/Edit/Checks/Components/IGeneralCheck.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs diff --git a/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs b/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs new file mode 100644 index 0000000000..47c9ce77e8 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Edit.Checks.Components +{ + /// + /// A general check that can be run on a beatmap to verify or find issues that apply across the beatmapset itself. + /// + public interface IGeneralCheck : ICheck + { + } +} From 8e73c57470409c75193f068ce8f76f140c23af6c Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 21:40:33 +0100 Subject: [PATCH 05/26] add `VerifyChecksScope` enum --- osu.Game/Localisation/EditorStrings.cs | 10 ++++++++ .../Checks/Components/VerifyChecksScope.cs | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index eeccdc8e8a..7d1c3074e4 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -209,6 +209,16 @@ namespace osu.Game.Localisation /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); + /// + /// "This difficulty" + /// + public static LocalisableString ThisDifficulty => new TranslatableString(getKey(@"this_difficulty"), @"This difficulty"); + + /// + /// "General" + /// + public static LocalisableString General => new TranslatableString(getKey(@"general"), @"General"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs new file mode 100644 index 0000000000..3d775c2cac --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; +using osu.Game.Localisation; + +namespace osu.Game.Rulesets.Edit.Checks.Components +{ + public enum VerifyChecksScope + { + /// + /// Run checks that apply to the current difficulty. + /// + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.ThisDifficulty))] + ThisDifficulty, + + /// + /// Run checks that apply to the beatmapset as a whole. + /// + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.General))] + General, + } +} From ca8a821dcab5fb43ed57d9658be6890e5ce6c0e3 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 21:41:21 +0100 Subject: [PATCH 06/26] add ability to filter checks via scope --- osu.Game/Screens/Edit/Verify/IssueList.cs | 29 +++++++++++++++++-- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 1 + osu.Game/Screens/Edit/Verify/ScopeSection.cs | 27 +++++++++++++++++ osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 2 ++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/Edit/Verify/ScopeSection.cs diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index e2eeff9ad5..55a711a4bd 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Edit.Verify base.LoadComplete(); verify.InterpretedDifficulty.BindValueChanged(_ => Refresh()); + verify.VerifyChecksScope.BindValueChanged(_ => Refresh()); verify.HiddenIssueTypes.BindCollectionChanged((_, _) => Refresh()); Refresh(); @@ -101,10 +102,26 @@ namespace osu.Game.Screens.Edit.Verify public void Refresh() { - var issues = generalVerifier.Run(context); + IEnumerable issues; - if (rulesetVerifier != null) - issues = issues.Concat(rulesetVerifier.Run(context)); + switch (verify.VerifyChecksScope.Value) + { + case VerifyChecksScope.General: + issues = filterByScope(generalVerifier.Run(context), true); + break; + + case VerifyChecksScope.ThisDifficulty: + var generalIssues = filterByScope(generalVerifier.Run(context), false); + var rulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); + issues = generalIssues.Concat(rulesetIssues); + break; + + default: + var allGeneralIssues = generalVerifier.Run(context); + var allRulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); + issues = allGeneralIssues.Concat(allRulesetIssues); + break; + } issues = filter(issues); @@ -118,5 +135,11 @@ namespace osu.Game.Screens.Edit.Verify { return issues.Where(issue => !verify.HiddenIssueTypes.Contains(issue.Template.Type)); } + + private IEnumerable filterByScope(IEnumerable issues, bool generalOnly) + { + return issues.Where(issue => + generalOnly ? issue.Check is IGeneralCheck : issue.Check is ICheck && issue.Check is not IGeneralCheck); + } } } diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 6d3c0520a2..01b41e622a 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -10,6 +10,7 @@ namespace osu.Game.Screens.Edit.Verify { protected override IReadOnlyList CreateSections() => new Drawable[] { + new ScopeSection(), new InterpretationSection(), new VisibilitySection() }; diff --git a/osu.Game/Screens/Edit/Verify/ScopeSection.cs b/osu.Game/Screens/Edit/Verify/ScopeSection.cs new file mode 100644 index 0000000000..a1969169e0 --- /dev/null +++ b/osu.Game/Screens/Edit/Verify/ScopeSection.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Screens.Edit.Verify +{ + internal partial class ScopeSection : EditorRoundedScreenSettingsSection + { + protected override string HeaderText => "Scope"; + + [BackgroundDependencyLoader] + private void load(VerifyScreen verify) + { + Flow.Add(new SettingsEnumDropdown + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TooltipText = "Select which type of checks to display", + Current = verify.VerifyChecksScope.GetBoundCopy() + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index fe508860e0..a365f18068 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -19,6 +19,8 @@ namespace osu.Game.Screens.Edit.Verify public readonly Bindable InterpretedDifficulty = new Bindable(); + public readonly Bindable VerifyChecksScope = new Bindable(); + public readonly BindableList HiddenIssueTypes = new BindableList { IssueType.Negligible }; public IssueList IssueList { get; private set; } From 0d2618082d5533736a697f3ba8854fc655dcfaa7 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 21:42:31 +0100 Subject: [PATCH 07/26] mark various set-level checks as general checks --- osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index 005902a8a1..6fb2406038 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -14,7 +14,7 @@ using File = TagLib.File; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAudioInVideo : ICheck + public class CheckAudioInVideo : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files"); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index 8c0c01d5da..3ddaf19419 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAudioQuality : ICheck + public class CheckAudioQuality : IGeneralCheck { // This is a requirement as stated in the Ranking Criteria. // See https://osu.ppy.sh/wiki/en/Ranking_criteria#audio diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 5008c13d9a..23a96347fa 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckBackgroundQuality : ICheck + public class CheckBackgroundQuality : IGeneralCheck { // These are the requirements as stated in the Ranking Criteria. // See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.5 diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs index ee950248db..e863dfedf9 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckDelayedHitsounds : ICheck + public class CheckDelayedHitsounds : IGeneralCheck { /// /// Threshold at which point the sample is considered silent. diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs index 9a921ba808..5730b639b1 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public abstract class CheckFilePresence : ICheck + public abstract class CheckFilePresence : IGeneralCheck { protected abstract CheckCategory Category { get; } protected abstract string TypeOfFile { get; } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index 9b6a861358..dd88a72ea4 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckHitsoundsFormat : ICheck + public class CheckHitsoundsFormat : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats."); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs index cf74ca3ea3..2a60706a17 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckInconsistentMetadata : ICheck + public class CheckInconsistentMetadata : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata"); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 1a31d19a78..e4a3397e91 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckSongFormat : ICheck + public class CheckSongFormat : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats."); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 9c702ad58a..1c29e73b26 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckTitleMarkers : ICheck + public class CheckTitleMarkers : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title."); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 3f85926e04..640377bcc8 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckTooShortAudioFiles : ICheck + public class CheckTooShortAudioFiles : IGeneralCheck { private const int ms_threshold = 25; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs index c050932aa6..6c9c44781b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -14,7 +14,7 @@ using File = TagLib.File; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckVideoResolution : ICheck + public class CheckVideoResolution : IGeneralCheck { private const int max_video_width = 1280; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs index 75cb08002f..6853706878 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckZeroByteFiles : ICheck + public class CheckZeroByteFiles : IGeneralCheck { public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files"); From 8d76ebae29134a2d47a6e069f87516e64079ec8f Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 1 Aug 2025 22:03:49 +0100 Subject: [PATCH 08/26] simplify bool check --- 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 55a711a4bd..ac4afd5677 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -139,7 +139,7 @@ namespace osu.Game.Screens.Edit.Verify private IEnumerable filterByScope(IEnumerable issues, bool generalOnly) { return issues.Where(issue => - generalOnly ? issue.Check is IGeneralCheck : issue.Check is ICheck && issue.Check is not IGeneralCheck); + generalOnly ? issue.Check is IGeneralCheck : issue.Check is not IGeneralCheck); } } } From 4171c3a2411162e34ccc463913af75a100b71ec6 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Aug 2025 12:03:29 +0300 Subject: [PATCH 09/26] Only play hold note sliding samples when beatmap is converted --- .../Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs | 1 + .../Objects/Drawables/DrawableHoldNote.cs | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs index e539baa94a..dee990f842 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs @@ -521,6 +521,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Duration = endTime - startTime, Column = column, Samples = HitObject.Samples, + PlaySlidingSamples = true, NodeSamples = nodeSamplesAt(startTime) }; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 23c062164e..210cd2a103 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -355,7 +355,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private void updateSlidingSample(ValueChangedEvent tracking) { - if (tracking.NewValue) + if (tracking.NewValue && HitObject.PlaySlidingSamples) slidingSample?.Play(); else slidingSample?.Stop(); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 98060dd226..15c570e34a 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -86,6 +86,11 @@ namespace osu.Game.Rulesets.Mania.Objects /// public HoldNoteBody Body { get; protected set; } + /// + /// Whether sliding samples should be played when held. + /// + public bool PlaySlidingSamples { get; init; } + public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From d216256fdfdb3d6a601e3e5b55c42ea938f8ec42 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Aug 2025 12:03:46 +0300 Subject: [PATCH 10/26] Fix "no release" mod not playing sliding samples in converted beatmaps --- osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index 143a5f1bdc..d664567a63 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -80,7 +80,9 @@ namespace osu.Game.Rulesets.Mania.Mods StartTime = hold.StartTime; Duration = hold.Duration; Column = hold.Column; + Samples = hold.Samples; NodeSamples = hold.NodeSamples; + PlaySlidingSamples = hold.PlaySlidingSamples; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From 6d43ae8ac122b4d8677a966c5d3ec1f050f8a2cc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Aug 2025 12:04:12 +0300 Subject: [PATCH 11/26] Clarify "invert" mod cannot possibly play sliding samples --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index d1912e3690..cc407a890f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.Mania.Mods StartTime = locations[i].startTime, Duration = duration, NodeSamples = new List> { locations[i].samples, Array.Empty() } + // intentionally don't play sliding samples here, it doesn't work in this mod. }); } From 8a2053bf615c20dfb2925d853ee427ea1a40f711 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 4 Aug 2025 14:02:40 +0100 Subject: [PATCH 12/26] rename scopes --- osu.Game/Localisation/EditorStrings.cs | 8 ++++---- .../Rulesets/Edit/Checks/Components/VerifyChecksScope.cs | 8 ++++---- osu.Game/Screens/Edit/Verify/IssueList.cs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 7d1c3074e4..2a9be8700d 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -210,14 +210,14 @@ namespace osu.Game.Localisation public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); /// - /// "This difficulty" + /// "Difficulty" /// - public static LocalisableString ThisDifficulty => new TranslatableString(getKey(@"this_difficulty"), @"This difficulty"); + public static LocalisableString Difficulty => new TranslatableString(getKey(@"this_difficulty"), @"Difficulty"); /// - /// "General" + /// "Beatmapset" /// - public static LocalisableString General => new TranslatableString(getKey(@"general"), @"General"); + public static LocalisableString Beatmapset => new TranslatableString(getKey(@"this_beatmapset"), @"Beatmapset"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs index 3d775c2cac..6542ffff37 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs @@ -11,13 +11,13 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// /// Run checks that apply to the current difficulty. /// - [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.ThisDifficulty))] - ThisDifficulty, + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.Difficulty))] + Difficulty, /// /// Run checks that apply to the beatmapset as a whole. /// - [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.General))] - General, + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.Beatmapset))] + Beatmapset, } } diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index ac4afd5677..20c3426880 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -106,11 +106,11 @@ namespace osu.Game.Screens.Edit.Verify switch (verify.VerifyChecksScope.Value) { - case VerifyChecksScope.General: + case VerifyChecksScope.Beatmapset: issues = filterByScope(generalVerifier.Run(context), true); break; - case VerifyChecksScope.ThisDifficulty: + case VerifyChecksScope.Difficulty: var generalIssues = filterByScope(generalVerifier.Run(context), false); var rulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); issues = generalIssues.Concat(rulesetIssues); From ed301ec114c0d91c50aed7c08926a99bf0e4ae2e Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 4 Aug 2025 14:12:59 +0100 Subject: [PATCH 13/26] define general checks with a bool in `CheckMetadata` instead of an interface --- osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs | 4 ++-- .../Rulesets/Edit/Checks/CheckBackgroundQuality.cs | 4 ++-- .../Rulesets/Edit/Checks/CheckDelayedHitsounds.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs | 4 ++-- .../Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 4 ++-- .../Edit/Checks/CheckInconsistentMetadata.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 4 ++-- .../Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs | 4 ++-- .../Rulesets/Edit/Checks/CheckVideoResolution.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs | 4 ++-- .../Rulesets/Edit/Checks/Components/CheckMetadata.cs | 8 +++++++- .../{VerifyChecksScope.cs => CheckScope.cs} | 2 +- .../Rulesets/Edit/Checks/Components/IGeneralCheck.cs | 12 ------------ osu.Game/Screens/Edit/Verify/IssueList.cs | 6 +++--- osu.Game/Screens/Edit/Verify/ScopeSection.cs | 2 +- osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 2 +- 18 files changed, 37 insertions(+), 43 deletions(-) rename osu.Game/Rulesets/Edit/Checks/Components/{VerifyChecksScope.cs => CheckScope.cs} (95%) delete mode 100644 osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index 6fb2406038..81f61d1bf6 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -14,9 +14,9 @@ using File = TagLib.File; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAudioInVideo : IGeneralCheck + public class CheckAudioInVideo : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files"); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index 3ddaf19419..c46fb5a56a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAudioQuality : IGeneralCheck + public class CheckAudioQuality : ICheck { // This is a requirement as stated in the Ranking Criteria. // See https://osu.ppy.sh/wiki/en/Ranking_criteria#audio @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Edit.Checks // There not existing a version with a bitrate of 128 kbps or higher is extremely rare. private const int min_bitrate = 128; - public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Too high or low audio bitrate"); + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Too high or low audio bitrate", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 23a96347fa..c843fc3248 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckBackgroundQuality : IGeneralCheck + public class CheckBackgroundQuality : ICheck { // These are the requirements as stated in the Ranking Criteria. // See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.5 @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks private const int low_width = 960; private const int low_height = 540; - public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Too high or low background resolution"); + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Too high or low background resolution", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs index e863dfedf9..f50549bda0 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckDelayedHitsounds : IGeneralCheck + public class CheckDelayedHitsounds : ICheck { /// /// Threshold at which point the sample is considered silent. @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks private const int delay_threshold = 5; private const int delay_threshold_negligible = 1; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds."); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds.", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs index 5730b639b1..a14b877586 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs @@ -7,13 +7,13 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public abstract class CheckFilePresence : IGeneralCheck + public abstract class CheckFilePresence : ICheck { protected abstract CheckCategory Category { get; } protected abstract string TypeOfFile { get; } protected abstract string? GetFilename(IBeatmap beatmap); - public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}"); + public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index dd88a72ea4..c1a4e0b1b1 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -11,9 +11,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckHitsoundsFormat : IGeneralCheck + public class CheckHitsoundsFormat : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats."); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats.", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs index 2a60706a17..fcfb6a54c2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs @@ -9,9 +9,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckInconsistentMetadata : IGeneralCheck + public class CheckInconsistentMetadata : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata"); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index e4a3397e91..5f36c2920e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -9,9 +9,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckSongFormat : IGeneralCheck + public class CheckSongFormat : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats."); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats.", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 1c29e73b26..77e1f23445 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -8,9 +8,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckTitleMarkers : IGeneralCheck + public class CheckTitleMarkers : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title."); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title.", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 640377bcc8..ff3bed956c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -10,11 +10,11 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckTooShortAudioFiles : IGeneralCheck + public class CheckTooShortAudioFiles : ICheck { private const int ms_threshold = 25; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs index 6c9c44781b..f9b56059bc 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -14,13 +14,13 @@ using File = TagLib.File; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckVideoResolution : IGeneralCheck + public class CheckVideoResolution : ICheck { private const int max_video_width = 1280; private const int max_video_height = 720; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution."); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution.", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs index 6853706878..85522157a5 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -8,9 +8,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckZeroByteFiles : IGeneralCheck + public class CheckZeroByteFiles : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files"); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files", CheckScope.Beatmapset); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs b/osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs index cebb2f5455..cbc07f1fa3 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs @@ -15,10 +15,16 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// public readonly string Description; - public CheckMetadata(CheckCategory category, string description) + /// + /// Specifies whether this check is difficulty-specific or applies to the entire beatmapset. Set to by default. + /// + public readonly CheckScope Scope; + + public CheckMetadata(CheckCategory category, string description, CheckScope scope = CheckScope.Difficulty) { Category = category; Description = description; + Scope = scope; } } } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs similarity index 95% rename from osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs rename to osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs index 6542ffff37..8ae23befeb 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/VerifyChecksScope.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs @@ -6,7 +6,7 @@ using osu.Game.Localisation; namespace osu.Game.Rulesets.Edit.Checks.Components { - public enum VerifyChecksScope + public enum CheckScope { /// /// Run checks that apply to the current difficulty. diff --git a/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs b/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs deleted file mode 100644 index 47c9ce77e8..0000000000 --- a/osu.Game/Rulesets/Edit/Checks/Components/IGeneralCheck.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Edit.Checks.Components -{ - /// - /// A general check that can be run on a beatmap to verify or find issues that apply across the beatmapset itself. - /// - public interface IGeneralCheck : ICheck - { - } -} diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 20c3426880..21a3ee39de 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -106,11 +106,11 @@ namespace osu.Game.Screens.Edit.Verify switch (verify.VerifyChecksScope.Value) { - case VerifyChecksScope.Beatmapset: + case CheckScope.Beatmapset: issues = filterByScope(generalVerifier.Run(context), true); break; - case VerifyChecksScope.Difficulty: + case CheckScope.Difficulty: var generalIssues = filterByScope(generalVerifier.Run(context), false); var rulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); issues = generalIssues.Concat(rulesetIssues); @@ -139,7 +139,7 @@ namespace osu.Game.Screens.Edit.Verify private IEnumerable filterByScope(IEnumerable issues, bool generalOnly) { return issues.Where(issue => - generalOnly ? issue.Check is IGeneralCheck : issue.Check is not IGeneralCheck); + generalOnly ? issue.Check.Metadata.Scope == CheckScope.Beatmapset : issue.Check.Metadata.Scope == CheckScope.Difficulty); } } } diff --git a/osu.Game/Screens/Edit/Verify/ScopeSection.cs b/osu.Game/Screens/Edit/Verify/ScopeSection.cs index a1969169e0..51807d5a8f 100644 --- a/osu.Game/Screens/Edit/Verify/ScopeSection.cs +++ b/osu.Game/Screens/Edit/Verify/ScopeSection.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load(VerifyScreen verify) { - Flow.Add(new SettingsEnumDropdown + Flow.Add(new SettingsEnumDropdown { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index a365f18068..208b33770f 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Verify public readonly Bindable InterpretedDifficulty = new Bindable(); - public readonly Bindable VerifyChecksScope = new Bindable(); + public readonly Bindable VerifyChecksScope = new Bindable(); public readonly BindableList HiddenIssueTypes = new BindableList { IssueType.Negligible }; From bb156d4206b6864e3fe7818c14bffc124d7ee719 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 4 Aug 2025 14:22:02 +0100 Subject: [PATCH 14/26] simplify filtering logic --- osu.Game/Screens/Edit/Verify/IssueList.cs | 30 ++++------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 21a3ee39de..415a46c583 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -102,26 +102,10 @@ namespace osu.Game.Screens.Edit.Verify public void Refresh() { - IEnumerable issues; + var issues = generalVerifier.Run(context); - switch (verify.VerifyChecksScope.Value) - { - case CheckScope.Beatmapset: - issues = filterByScope(generalVerifier.Run(context), true); - break; - - case CheckScope.Difficulty: - var generalIssues = filterByScope(generalVerifier.Run(context), false); - var rulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); - issues = generalIssues.Concat(rulesetIssues); - break; - - default: - var allGeneralIssues = generalVerifier.Run(context); - var allRulesetIssues = rulesetVerifier?.Run(context) ?? Enumerable.Empty(); - issues = allGeneralIssues.Concat(allRulesetIssues); - break; - } + if (rulesetVerifier != null) + issues = issues.Concat(rulesetVerifier.Run(context)); issues = filter(issues); @@ -132,14 +116,10 @@ namespace osu.Game.Screens.Edit.Verify } private IEnumerable filter(IEnumerable issues) - { - return issues.Where(issue => !verify.HiddenIssueTypes.Contains(issue.Template.Type)); - } - - private IEnumerable filterByScope(IEnumerable issues, bool generalOnly) { return issues.Where(issue => - generalOnly ? issue.Check.Metadata.Scope == CheckScope.Beatmapset : issue.Check.Metadata.Scope == CheckScope.Difficulty); + !verify.HiddenIssueTypes.Contains(issue.Template.Type) && + issue.Check.Metadata.Scope == verify.VerifyChecksScope.Value); } } } From a7f0ae09bbad5a2677fbfe4f631de250f7d80cea Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 4 Aug 2025 15:21:43 +0100 Subject: [PATCH 15/26] apply review - improve timing tolerance const name and documentation - simplify matching logic in `FindMatchingTimingPoint` --- .../Edit/Checks/CheckInconsistentTimingControlPoints.cs | 2 +- .../Rulesets/Edit/Checks/Components/TimingCheckUtils.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs index bbed49d7ee..8ed802e618 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentTimingControlPoints.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Edit.Checks } // Check for BPM inconsistency - if (Math.Abs(referencePoint.BeatLength - matchingPoint.BeatLength) > TimingCheckUtils.TIMING_TOLERANCE) + if (Math.Abs(referencePoint.BeatLength - matchingPoint.BeatLength) > TimingCheckUtils.TIME_OFFSET_TOLERANCE_MS) { yield return new IssueTemplateInconsistentBPM(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs index 1ddbeb31d6..f56f27813d 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/TimingCheckUtils.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Utils; @@ -11,8 +10,8 @@ namespace osu.Game.Rulesets.Edit.Checks.Components { public static class TimingCheckUtils { - // Small tolerance for floating point comparison - public const double TIMING_TOLERANCE = 0.01; + // Tolerance for exact time offset matching (in milliseconds) + public const double TIME_OFFSET_TOLERANCE_MS = 0.01; /// /// Finds a timing control point that starts at approximately the same time (within 1ms after rounding). @@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// The matching timing control point, or null if none found. public static TimingControlPoint? FindMatchingTimingPoint(IEnumerable timingPoints, double time) { - return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, Math.Round(time), 1.0)); + return timingPoints.FirstOrDefault(tp => (int)tp.Time == (int)time); } /// @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// The exact matching timing control point, or null if none found. public static TimingControlPoint? FindExactMatchingTimingPoint(IEnumerable timingPoints, double time) { - return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, time, TIMING_TOLERANCE)); + return timingPoints.FirstOrDefault(tp => Precision.AlmostEquals(tp.Time, time, TIME_OFFSET_TOLERANCE_MS)); } } } From 2c5aa73ccbe6c95fe8d916dbdcbb4a1b3cfc4f3b Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 4 Aug 2025 22:07:41 +0100 Subject: [PATCH 16/26] remove unnecessary localisations --- osu.Game/Localisation/EditorStrings.cs | 10 ---------- osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs | 5 ----- 2 files changed, 15 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 2a9be8700d..eeccdc8e8a 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -209,16 +209,6 @@ namespace osu.Game.Localisation /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); - /// - /// "Difficulty" - /// - public static LocalisableString Difficulty => new TranslatableString(getKey(@"this_difficulty"), @"Difficulty"); - - /// - /// "Beatmapset" - /// - public static LocalisableString Beatmapset => new TranslatableString(getKey(@"this_beatmapset"), @"Beatmapset"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs index 8ae23befeb..27e35bb284 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Localisation; -using osu.Game.Localisation; - namespace osu.Game.Rulesets.Edit.Checks.Components { public enum CheckScope @@ -11,13 +8,11 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// /// Run checks that apply to the current difficulty. /// - [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.Difficulty))] Difficulty, /// /// Run checks that apply to the beatmapset as a whole. /// - [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.Beatmapset))] Beatmapset, } } From ee5a5cda110687d1e02d0b33058222052306e7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Aug 2025 08:18:30 +0200 Subject: [PATCH 17/26] Use casing of "beatmap set" consistent with the rest of the code base --- osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs | 2 +- osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index 81f61d1bf6..a7e54528d2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckAudioInVideo : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index c46fb5a56a..32af1ceba4 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Edit.Checks // There not existing a version with a bitrate of 128 kbps or higher is extremely rare. private const int min_bitrate = 128; - public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Too high or low audio bitrate", CheckScope.Beatmapset); + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Too high or low audio bitrate", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index c843fc3248..c1351d053b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks private const int low_width = 960; private const int low_height = 540; - public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Too high or low background resolution", CheckScope.Beatmapset); + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Too high or low background resolution", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs index f50549bda0..a78a16953e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks private const int delay_threshold = 5; private const int delay_threshold_negligible = 1; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds.", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds.", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs index a14b877586..346b79c8af 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Edit.Checks protected abstract string TypeOfFile { get; } protected abstract string? GetFilename(IBeatmap beatmap); - public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index c1a4e0b1b1..30973cfa76 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckHitsoundsFormat : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats.", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats.", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs index fcfb6a54c2..e7f06556cc 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckInconsistentMetadata : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 5f36c2920e..5871cf51ff 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckSongFormat : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats.", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats.", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 77e1f23445..742054777e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckTitleMarkers : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title.", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title.", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index ff3bed956c..7991797ddd 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Edit.Checks { private const int ms_threshold = 25; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs index f9b56059bc..344dddec3e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Edit.Checks private const int max_video_height = 720; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution.", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution.", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs index 85522157a5..7048e944dd 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Edit.Checks { public class CheckZeroByteFiles : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files", CheckScope.Beatmapset); + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files", CheckScope.BeatmapSet); public IEnumerable PossibleTemplates => new IssueTemplate[] { diff --git a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs index 27e35bb284..46154c1231 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Edit.Checks.Components Difficulty, /// - /// Run checks that apply to the beatmapset as a whole. + /// Run checks that apply to the beatmap set as a whole. /// - Beatmapset, + BeatmapSet, } } From c1c43fab175ab6e97b8135c34c014f7c79a0d953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Aug 2025 08:23:47 +0200 Subject: [PATCH 18/26] Bring back localisations --- osu.Game/Localisation/EditorStrings.cs | 10 ++++++++++ osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index eeccdc8e8a..c8b163c678 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -209,6 +209,16 @@ namespace osu.Game.Localisation /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); + /// + /// "Current difficulty" + /// + public static LocalisableString CheckCurrentDifficulty => new TranslatableString(getKey(@"check_current_difficulty"), @"Current difficulty"); + + /// + /// "Entire beatmap set" + /// + public static LocalisableString CheckEntireBeatmapSet => new TranslatableString(getKey(@"check_entire_beatmap_set"), @"Entire beatmap set"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs index 46154c1231..7dcc4d87f2 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/CheckScope.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; +using osu.Game.Localisation; + namespace osu.Game.Rulesets.Edit.Checks.Components { public enum CheckScope @@ -8,11 +11,13 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// /// Run checks that apply to the current difficulty. /// + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.CheckCurrentDifficulty))] Difficulty, /// /// Run checks that apply to the beatmap set as a whole. /// + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.CheckEntireBeatmapSet))] BeatmapSet, } } From 03dae97ff313547e20d59dd60c388e7a8aea3422 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 5 Aug 2025 10:24:54 +0300 Subject: [PATCH 19/26] Fix special cases of mania beatmaps not playing sliding samples Apparently mania sliders are a thing. See https://github.com/ppy/osu/pull/34500#issuecomment-3150610188 --- .../Patterns/Legacy/PassThroughPatternGenerator.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs index efeb99e8b4..b3cb871f1f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Utils; @@ -30,12 +32,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (HitObject is IHasDuration endTimeData) { + // despite the beatmap originally made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played. + // this is seemingly only possible to achieve by modifying the .osu file directly, but online beatmaps that do that exist + // (see second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407) + bool playSlidingSamples = (HitObject is IHasLegacyHitObjectType hasType && hasType.LegacyType == LegacyHitObjectType.Slider) || HitObject is IHasPath; + pattern.Add(new HoldNote { StartTime = HitObject.StartTime, Duration = endTimeData.Duration, Column = column, Samples = HitObject.Samples, + PlaySlidingSamples = playSlidingSamples, NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject) }); } From 9798889ace9f102800af2b091092c94757c75d87 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 5 Aug 2025 10:25:58 +0300 Subject: [PATCH 20/26] Add test coverage --- .../ManiaBeatmapSampleConversionTest.cs | 4 +++ .../convert-samples-expected-conversion.json | 2 ++ .../mania-samples-expected-conversion.json | 2 ++ .../mania-slider-expected-conversion.json | 18 ++++++++++++ .../Testing/Beatmaps/mania-slider.osu | 29 +++++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index 99598557a6..b4f084a07c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase("convert-samples")] [TestCase("mania-samples")] + [TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407 [TestCase("slider-convert-samples")] public void Test(string name) => base.Test(name); @@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests StartTime = hitObject.StartTime, EndTime = hitObject.GetEndTime(), Column = ((ManiaHitObject)hitObject).Column, + PlaySlidingSamples = hitObject is HoldNote holdNote && holdNote.PlaySlidingSamples, Samples = getSampleNames(hitObject.Samples), NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples) }; @@ -57,12 +59,14 @@ namespace osu.Game.Rulesets.Mania.Tests public double StartTime; public double EndTime; public int Column; + public bool PlaySlidingSamples; public IList Samples; public IList> NodeSamples; public bool Equals(SampleConvertValue other) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && PlaySlidingSamples == other.PlaySlidingSamples && samplesEqual(Samples, other.Samples) && nodeSamplesEqual(NodeSamples, other.NodeSamples); diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json index 4d298bb671..273dd33452 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json @@ -5,6 +5,7 @@ "StartTime": 1000.0, "EndTime": 2750.0, "Column": 1, + "PlaySlidingSamples": true, "NodeSamples": [ ["Gameplay/normal-hitnormal"], ["Gameplay/soft-hitnormal"], @@ -15,6 +16,7 @@ "StartTime": 1875.0, "EndTime": 2750.0, "Column": 0, + "PlaySlidingSamples": true, "NodeSamples": [ ["Gameplay/soft-hitnormal"], ["Gameplay/drum-hitnormal"] diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json index fd0c0cad60..eb0c5df10b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json @@ -5,6 +5,7 @@ "StartTime": 500.0, "EndTime": 1500.0, "Column": 0, + "PlaySlidingSamples": false, "NodeSamples": [ ["Gameplay/normal-hitnormal"], [] @@ -17,6 +18,7 @@ "StartTime": 2000.0, "EndTime": 3000.0, "Column": 2, + "PlaySlidingSamples": false, "NodeSamples": [ ["Gameplay/drum-hitnormal"], [] diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider-expected-conversion.json new file mode 100644 index 0000000000..bf097f589e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider-expected-conversion.json @@ -0,0 +1,18 @@ +{ + "Mappings": [{ + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "EndTime": 2500, + "Column": 2, + "PlaySlidingSamples": true, + "NodeSamples": [ + ["Gameplay/soft-hitnormal"], + ["Gameplay/soft-hitnormal"], + ["Gameplay/soft-hitnormal"], + ["Gameplay/soft-hitnormal"] + ], + "Samples": ["Gameplay/soft-hitnormal"] + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider.osu new file mode 100644 index 0000000000..c6e44ab9bf --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-slider.osu @@ -0,0 +1,29 @@ +osu file format v5 + +[General] +StackLeniency: 0.7 +Mode: 3 + +[Difficulty] +HPDrainRate:2 +CircleSize:5 +OverallDifficulty:2 +SliderMultiplier:1 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Failing) +//Storyboard Layer 2 (Passing) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +355,476.190476190476,4,2,1,60,1,0 + +[HitObjects] +256,352,500,2,0,L|256:208,3,140 From 50daae399bb6fc5e3006fb66b7d08abab00f4173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Aug 2025 10:48:10 +0200 Subject: [PATCH 21/26] Fix grammar in comment --- .../Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs index b3cb871f1f..2c6c4d1a5c 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (HitObject is IHasDuration endTimeData) { - // despite the beatmap originally made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played. + // despite the beatmap originally being made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played. // this is seemingly only possible to achieve by modifying the .osu file directly, but online beatmaps that do that exist // (see second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407) bool playSlidingSamples = (HitObject is IHasLegacyHitObjectType hasType && hasType.LegacyType == LegacyHitObjectType.Slider) || HitObject is IHasPath; From 7c6b2dd230d9548a6ae2940aeeb6e80787a805d3 Mon Sep 17 00:00:00 2001 From: Hivie Date: Tue, 5 Aug 2025 09:59:44 +0100 Subject: [PATCH 22/26] add taiko check for inconsistent barline omission --- .../CheckTaikoInconsistentSkipBarLine.cs | 70 +++++++++++++++++++ .../Edit/TaikoBeatmapVerifier.cs | 3 + 2 files changed, 73 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs new file mode 100644 index 0000000000..95e7dc47b0 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs @@ -0,0 +1,70 @@ +// 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.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Taiko.Edit.Checks +{ + public class CheckTaikoInconsistentSkipBarLine : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Timing, "Inconsistent \"Skip Bar Line\" setting"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateInconsistentOmitFirstBarLine(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var difficulties = context.BeatmapsetDifficulties; + + if (difficulties.Count <= 1) + yield break; + + // Inconsistent bar line omission only matters for osu!taiko difficulties, so only check those + var taikoBeatmaps = difficulties.Where(b => b.BeatmapInfo.Ruleset.ShortName == "taiko").ToList(); + + if (taikoBeatmaps.Count <= 1) + yield break; + + var referenceBeatmap = context.Beatmap; + var referenceTimingPoints = referenceBeatmap.ControlPointInfo.TimingPoints; + + foreach (var beatmap in taikoBeatmaps) + { + if (beatmap == referenceBeatmap) + continue; + + var timingPoints = beatmap.ControlPointInfo.TimingPoints; + + foreach (var referencePoint in referenceTimingPoints) + { + var matchingPoint = TimingCheckUtils.FindExactMatchingTimingPoint(timingPoints, referencePoint.Time); + + if (matchingPoint == null) + // Inconsistent timing points - that's handled by the main timing check, so skip + continue; + + if (referencePoint.OmitFirstBarLine != matchingPoint.OmitFirstBarLine) + { + yield return new IssueTemplateInconsistentOmitFirstBarLine(this).Create(referencePoint.Time, beatmap.BeatmapInfo.DifficultyName); + } + } + } + } + + public class IssueTemplateInconsistentOmitFirstBarLine : IssueTemplate + { + public IssueTemplateInconsistentOmitFirstBarLine(ICheck check) + : base(check, IssueType.Problem, "Inconsistent \"Skip Bar Line\" setting in {0}.") + { + } + + public Issue Create(double time, string difficultyName) + => new Issue(time, this, difficultyName); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs index 23d0abed08..737347a64c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs @@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Taiko.Edit // Settings new CheckTaikoAbnormalDifficultySettings(), + + // Timing + new CheckTaikoInconsistentSkipBarLine(), }; public IEnumerable Run(BeatmapVerifierContext context) From 21091096864da6cd0d91bc6cce5b84fcbbb71d46 Mon Sep 17 00:00:00 2001 From: Hivie Date: Tue, 5 Aug 2025 10:00:08 +0100 Subject: [PATCH 23/26] add tests --- .../CheckTaikoInconsistentSkipBarLineTest.cs | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoInconsistentSkipBarLineTest.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoInconsistentSkipBarLineTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoInconsistentSkipBarLineTest.cs new file mode 100644 index 0000000000..45c5bf3985 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoInconsistentSkipBarLineTest.cs @@ -0,0 +1,222 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Edit.Checks; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTaikoInconsistentSkipBarLineTest + { + private CheckTaikoInconsistentSkipBarLine check = null!; + + [SetUp] + public void Setup() + { + check = new CheckTaikoInconsistentSkipBarLine(); + } + + [Test] + public void TestConsistentOmitFirstBarLine() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference + new[] { (1000.0, false), (2000.0, true) } // Same settings + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestInconsistentOmitFirstBarLine() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference + new[] { (1000.0, true), (2000.0, false) } // Different settings + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.All(issue => issue.Template is CheckTaikoInconsistentSkipBarLine.IssueTemplateInconsistentOmitFirstBarLine)); + Assert.That(issues[0].Time, Is.EqualTo(1000.0)); + Assert.That(issues[1].Time, Is.EqualTo(2000.0)); + } + + [Test] + public void TestPartiallyInconsistentOmitFirstBarLine() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true), (3000.0, false) }, // Reference + new[] { (1000.0, false), (2000.0, false), (3000.0, false) } // Only second differs + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckTaikoInconsistentSkipBarLine.IssueTemplateInconsistentOmitFirstBarLine); + Assert.That(issues[0].Time, Is.EqualTo(2000.0)); + } + + [Test] + public void TestSingleDifficulty() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) } // Only one difficulty + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestNonTaikoBeatmaps() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference + new[] { (1000.0, true), (2000.0, false) } // Different settings + ); + + // Make both beatmaps non-taiko + beatmaps[0].BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; + beatmaps[1].BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestMixedRulesets() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference + new[] { (1000.0, true), (2000.0, false) } // Different settings + ); + + // Make reference taiko, other non-taiko + beatmaps[0].BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; + beatmaps[1].BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestMissingTimingPoints() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference has 2 points + new[] { (1000.0, false) } // Other has only 1 point (missing 2000.0) + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + // Should only check the existing timing point at 1000.0 (consistent, no issue) + // The missing 2000.0 point should be ignored by this check + Assert.That(issues, Is.Empty); + } + + [Test] + public void TestExtraTimingPoints() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false) }, // Reference has 1 point + new[] { (1000.0, false), (2000.0, true) } // Other has extra point + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + // Should only check the existing timing point at 1000.0 (consistent, no issue) + // The extra 2000.0 point should be ignored by this check + Assert.That(issues, Is.Empty); + } + + [Test] + public void TestMultipleDifficultiesWithInconsistencies() + { + var beatmaps = createBeatmapSetWithTimingPoints( + new[] { (1000.0, false), (2000.0, true) }, // Reference + new[] { (1000.0, true), (2000.0, true) }, // First differs + new[] { (1000.0, false), (2000.0, false) } // Second differs + ); + + var context = createContextWithMultipleDifficulties(beatmaps[0], beatmaps); + var issues = check.Run(context).ToList(); + + // Should have issues for both other difficulties + Assert.That(issues, Has.Count.EqualTo(2)); // 1000.0 from diff2, 2000.0 from diff3 + Assert.That(issues.All(issue => issue.Template is CheckTaikoInconsistentSkipBarLine.IssueTemplateInconsistentOmitFirstBarLine)); + Assert.That(issues[0].Time, Is.EqualTo(1000.0)); + Assert.That(issues[1].Time, Is.EqualTo(2000.0)); + } + + private IBeatmap[] createBeatmapSetWithTimingPoints(params (double time, bool omitFirstBarLine)[][] timingData) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmaps = new IBeatmap[timingData.Length]; + + for (int i = 0; i < timingData.Length; i++) + { + beatmaps[i] = createBeatmapWithTimingPoints(timingData[i], $"Difficulty {i + 1}"); + beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet; + beatmaps[i].BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; + } + + // Configure the beatmapset to contain all the beatmap infos + foreach (var beatmap in beatmaps) + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + + return beatmaps; + } + + private IBeatmap createBeatmapWithTimingPoints((double time, bool omitFirstBarLine)[] timingData, string difficultyName) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = difficultyName, + Metadata = new BeatmapMetadata() + } + }; + + foreach ((double time, bool omitFirstBarLine) in timingData) + { + beatmap.ControlPointInfo.Add(time, new TimingControlPoint + { + BeatLength = 500, // Standard BPM + OmitFirstBarLine = omitFirstBarLine + }); + } + + return beatmap; + } + + private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IBeatmap[] allDifficulties) + { + return new BeatmapVerifierContext( + currentBeatmap, + new TestWorkingBeatmap(currentBeatmap), + DifficultyRating.ExpertPlus, + beatmapInfo => allDifficulties.FirstOrDefault(b => b.BeatmapInfo.Equals(beatmapInfo)) + ); + } + } +} From 85a7504ffc8ce2b05ae3aa50a324b4c0f9d445d5 Mon Sep 17 00:00:00 2001 From: Hivie Date: Tue, 5 Aug 2025 10:02:26 +0100 Subject: [PATCH 24/26] comment --- .../Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs index 95e7dc47b0..3d2e888230 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoInconsistentSkipBarLine.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks var matchingPoint = TimingCheckUtils.FindExactMatchingTimingPoint(timingPoints, referencePoint.Time); if (matchingPoint == null) - // Inconsistent timing points - that's handled by the main timing check, so skip + // Inconsistent timing points - that's handled by `CheckInconsistentTimingControlPoints`, so skip continue; if (referencePoint.OmitFirstBarLine != matchingPoint.OmitFirstBarLine) From 6b73308955ad4d4b4124de52768be7387ad3059a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Aug 2025 13:06:03 +0200 Subject: [PATCH 25/26] Fix clicking group & set headers not working (or crashing) during a filter This papers over most of https://github.com/ppy/osu/issues/34507. The full failure scenario is: - user selects a beatmap set - online lookups fire - online lookups are populated back to realm - the realm changes trigger subscription, which triggers detached store replace, which triggers refilter - refilter takes a while - during the refilter user may be unable to select some sets because the set header mapping is incomplete This only papers over the last bullet of that, in making sure that the set header mapping is not externally accessed while it's being rebuilt. It doesn't fix the insane amount of refilters caused by everything preceding the refilter being overeager to trigger said refilter. --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index aa053bb727..70edda048f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -30,8 +30,8 @@ namespace osu.Game.Screens.SelectV2 /// public IDictionary> GroupItems => groupMap; - private readonly Dictionary> setMap = new Dictionary>(); - private readonly Dictionary> groupMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); + private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; private readonly Func>? getCollections; @@ -46,8 +46,9 @@ namespace osu.Game.Screens.SelectV2 { return await Task.Run(() => { - setMap.Clear(); - groupMap.Clear(); + // preallocate space for the new mappings using last known estimates + var newSetMap = new Dictionary>(setMap.Count); + var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); var newItems = new List(); @@ -67,7 +68,7 @@ namespace osu.Game.Screens.SelectV2 if (group != null) { - groupMap[group] = currentGroupItems = new HashSet(); + newGroupMap[group] = currentGroupItems = new HashSet(); addItem(groupItem = new CarouselItem(group) { @@ -84,8 +85,8 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) - setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) + newSetMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -125,6 +126,8 @@ namespace osu.Game.Screens.SelectV2 } } + Interlocked.Exchange(ref setMap, newSetMap); + Interlocked.Exchange(ref groupMap, newGroupMap); return newItems; }, cancellationToken).ConfigureAwait(false); } From a10eaf7f2b317d15093912118a095c1765daeadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Aug 2025 13:07:53 +0200 Subject: [PATCH 26/26] Be more aggressive about cancelling grouping filter This is also papering over the larger issue of the insane refilter count and is basically a drive-by fix but rider is bugging me with a bunch of yellow highlights from memory churn which looks somewhat easily preventable. Basically the cancellation of filters could only take place between full groups. If there are few groups with lots of beatmaps, it could be a *looooong* while before the cancellation of the filter would actually take place. Hence the added extra cancellation check before every individual group item, and one extra check before performing the set mapping exchange and items return. --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 70edda048f..cba1d36ba7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -79,6 +79,8 @@ namespace osu.Game.Screens.SelectV2 foreach (var item in itemsInGroup) { + cancellationToken.ThrowIfCancellationRequested(); + var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; @@ -126,6 +128,8 @@ namespace osu.Game.Screens.SelectV2 } } + cancellationToken.ThrowIfCancellationRequested(); + Interlocked.Exchange(ref setMap, newSetMap); Interlocked.Exchange(ref groupMap, newGroupMap); return newItems;