From cc46cbf7801406ff34aa58c77e61f3df1d561b08 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 5 May 2025 09:46:06 +0300 Subject: [PATCH 01/10] `WaitForSorting` -> `WaitForFiltering` --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- .../TestSceneBeatmapCarouselScrolling.cs | 4 ++-- .../TestSceneBeatmapCarouselUpdateHandling.cs | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 28a0948696..a11e4063a2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); + protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index da3fc98c19..93b0332ac4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 31aa1b6f94..4c6202d94c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 BeatmapSets.Add(baseTestBeatmap); }); - WaitForSorting(); + WaitForFiltering(); } [Test] @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap])); - WaitForSorting(); + WaitForFiltering(); AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); } @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 updateBeatmap(b => b.Metadata = metadata); - WaitForSorting(); + WaitForFiltering(); AddAssert("drawables changed", () => Carousel.ChildrenOfType(), () => Is.Not.EqualTo(originalDrawables)); } @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); From 540cfc92da1ea3f54961420c494907e5bc99c46e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 5 May 2025 09:46:41 +0300 Subject: [PATCH 02/10] Support mutating existing active filter criteria in carousel tests Allows maintaining sorting mode while modifying other filter criterias, thus simplifying some tests. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 17 ++++++++++++++++- .../SongSelectV2/TestSceneBeatmapCarousel.cs | 12 ++++++++---- .../TestSceneBeatmapCarouselArtistGrouping.cs | 5 +++-- ...estSceneBeatmapCarouselDifficultyGrouping.cs | 5 +++-- .../TestSceneBeatmapCarouselNoGrouping.cs | 3 --- .../TestSceneBeatmapCarouselScrolling.cs | 5 +++-- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index a11e4063a2..39f6c2230b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -127,9 +128,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, }; }); + + // Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable. + SortBy(SortMode.Title); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.ToString().ToLowerInvariant()}", c => c.Sort = mode); + protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.ToString().ToLowerInvariant()}", c => c.Group = mode); + + protected void ApplyToFilter(string description, Action? apply) + { + AddStep(description, () => + { + var criteria = Carousel.Criteria; + apply?.Invoke(criteria); + Carousel.Filter(criteria); + }); + } protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 5fd921645b..870225edb3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; @@ -34,9 +33,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Explicit] public void TestSorting() { - SortBy(new FilterCriteria { Sort = SortMode.Artist }); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + SortBy(SortMode.Artist); + GroupBy(GroupMode.All); + + SortBy(SortMode.Difficulty); + GroupBy(GroupMode.Difficulty); + + SortBy(SortMode.Artist); + GroupBy(GroupMode.Artist); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index f0caa796b6..84769f2cee 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -19,7 +18,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + SortBy(SortMode.Artist); + GroupBy(GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index a4cdf8abcb..37fb95ce86 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; @@ -21,7 +20,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + SortBy(SortMode.Difficulty); + GroupBy(GroupMode.Difficulty); AddBeatmaps(10, 3); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index ac02d7a3a9..cdd55f0f0c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -6,8 +6,6 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; using osuTK.Input; @@ -22,7 +20,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Sort = SortMode.Title }); } /// diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 93b0332ac4..f5574d2789 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -5,7 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; -using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -18,7 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria()); + + SortBy(SortMode.Artist); AddBeatmaps(10); WaitForDrawablePanels(); From af874ca7307db927ab69eeff340e2099649d7240 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 07:58:37 +0300 Subject: [PATCH 03/10] Fix update handling test selecting wrong beatmap Order has changed after carousel was made to sort by title. --- .../SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 4c6202d94c..b9a468d580 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestSelectionHeld() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we keep selection based on online ID where possible. public void TestSelectionHeldDifficultyNameChanged() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we fallback to keeping selection based on difficulty name. public void TestSelectionHeldDifficultyOnlineIDChanged() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); From a3aa4c7ba58fa97e2634e76496985acd88ca3297 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 12:47:49 +0300 Subject: [PATCH 04/10] Add carousel filter for matching items against criteria (i.e. actually filter) --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + .../SelectV2/BeatmapCarouselFilterMatching.cs | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4af5e759a7..3578fd46fa 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -49,6 +49,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { + new BeatmapCarouselFilterMatching(() => Criteria), new BeatmapCarouselFilterSorting(() => Criteria), grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs new file mode 100644 index 0000000000..f81f068ab7 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -0,0 +1,113 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterMatching : ICarouselFilter + { + private readonly Func getCriteria; + + public BeatmapCarouselFilterMatching(Func getCriteria) + { + this.getCriteria = getCriteria; + } + + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) + { + return await Task.Run(() => + { + var criteria = getCriteria(); + return matchItems(items, criteria); + }, cancellationToken).ConfigureAwait(false); + } + + private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) + { + foreach (var item in items) + { + var beatmap = (BeatmapInfo)item.Model; + + if (checkMatch(beatmap, criteria)) + yield return item; + } + } + + private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria) + { + bool match = criteria.Ruleset == null || + beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName || + (beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); + + if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + { + // only check ruleset equality or convertability for selected beatmap + return match; + } + + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) + { + match = beatmap.Match(criteria.SearchTerms); + + // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. + // this should be done after text matching so we can prioritise matching numbers in metadata. + if (!match && criteria.SearchNumber.HasValue) + { + match = (beatmap.OnlineID == criteria.SearchNumber.Value) || + (beatmap.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); + } + } + + if (!match) return false; + + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(beatmap.StarRating); + match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(beatmap.Difficulty.ApproachRate); + match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(beatmap.Difficulty.DrainRate); + match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(beatmap.Difficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(beatmap.Difficulty.OverallDifficulty); + match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(beatmap.Length); + match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(beatmap.LastPlayed ?? DateTimeOffset.MinValue); + match &= !criteria.DateRanked.HasFilter || (beatmap.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(beatmap.BeatmapSet.DateRanked.Value)); + match &= !criteria.DateSubmitted.HasFilter || (beatmap.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(beatmap.BeatmapSet.DateSubmitted.Value)); + match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(beatmap.BPM); + + match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(beatmap.BeatDivisor); + match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(beatmap.Status); + + if (!match) return false; + + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(beatmap.Metadata.Author.Username); + match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(beatmap.Metadata.Artist) || + criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode); + match &= !criteria.Title.HasFilter || criteria.Title.Matches(beatmap.Metadata.Title) || + criteria.Title.Matches(beatmap.Metadata.TitleUnicode); + match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(beatmap.DifficultyName); + match &= !criteria.Source.HasFilter || criteria.Source.Matches(beatmap.Metadata.Source); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(beatmap.StarRating); + + if (!match) return false; + + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(beatmap.MD5Hash) ?? true; + if (match && criteria.RulesetCriteria != null) + match &= criteria.RulesetCriteria.Matches(beatmap, criteria); + + if (match && criteria.HasOnlineID == true) + match &= beatmap.OnlineID >= 0; + + if (match && criteria.BeatmapSetId != null) + match &= criteria.BeatmapSetId == beatmap.BeatmapSet?.OnlineID; + + return match; + } + } +} From 90abd11ca5065edc831f0118ec4fe6eac809f580 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 13:00:13 +0300 Subject: [PATCH 05/10] Add test coverage for filtering --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 28 ++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 30 ++ .../TestSceneBeatmapCarouselFiltering.cs | 284 ++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 84769f2cee..8f822cbb1d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -174,5 +174,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextGroup(); WaitForGroupSelection(1, 1); } + + [Test] + public void TestBasicFiltering() + { + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("1 group + 1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 5); + + CheckNoSelection(); + SelectNextPanel(); + Select(); + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 1); + + for (int i = 0; i < 6; i++) + SelectNextPanel(); + + Select(); + + WaitForGroupSelection(0, 2); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("5 groups + 10 sets + 30 diffs displayed", () => Carousel.DisplayableItems == 45); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 37fb95ce86..bf20825bdb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -192,5 +192,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); } + + [Test] + public void TestBasicFiltering() + { + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("3 groups + 3 diffs displayed", () => Carousel.DisplayableItems == 6); + + CheckNoSelection(); + SelectNextPanel(); + Select(); + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 0); + + for (int i = 0; i < 5; i++) + SelectNextPanel(); + + Select(); + SelectNextPanel(); + Select(); + + WaitForGroupSelection(1, 0); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("3 groups + 30 diffs displayed", () => Carousel.DisplayableItems == 33); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs new file mode 100644 index 0000000000..cb1b0ec31f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -0,0 +1,284 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene + { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + } + + [Test] + public void TestBasicFiltering() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("3 diffs + 1 set displayed", () => Carousel.DisplayableItems == 4); + + SelectNextPanel(); + Select(); + + WaitForSelection(2, 0); + + for (int i = 0; i < 5; i++) + SelectNextPanel(); + + Select(); + WaitForSelection(2, 1); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("30 diffs + 10 sets displayed", () => Carousel.DisplayableItems == 40); + } + + [Test] + public void TestFilteringByUserStarDifficulty() + { + AddStep("add mixed difficulty set", () => + { + var set = TestResources.CreateTestBeatmapSetInfo(1); + set.Beatmaps.Clear(); + + for (int i = 1; i <= 15; i++) + { + set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata()) + { + BeatmapSet = set, + DifficultyName = $"Stars: {i}", + StarRating = i, + }); + } + + BeatmapSets.Add(set); + }); + + WaitForDrawablePanels(); + + ApplyToFilter("filter [5..]", c => + { + c.UserStarDifficulty.Min = 5; + c.UserStarDifficulty.Max = null; + }); + WaitForFiltering(); + AddAssert("1 set + 11 diffs displayed", () => Carousel.DisplayableItems == 12); + + ApplyToFilter("filter to [0..7]", c => + { + c.UserStarDifficulty.Min = null; + c.UserStarDifficulty.Max = 7; + }); + WaitForFiltering(); + AddAssert("1 set + 7 diffs displayed", () => Carousel.DisplayableItems == 8); + + ApplyToFilter("filter to [5..7]", c => + { + c.UserStarDifficulty.Min = 5; + c.UserStarDifficulty.Max = 7; + }); + + WaitForFiltering(); + AddAssert("1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 4); + + ApplyToFilter("filter to [2..2]", c => + { + c.UserStarDifficulty.Min = 2; + c.UserStarDifficulty.Max = 2; + }); + + WaitForFiltering(); + AddAssert("`1 set + 1 diff displayed", () => Carousel.DisplayableItems == 2); + + ApplyToFilter("filter to [0..]", c => + { + c.UserStarDifficulty.Min = 0; + c.UserStarDifficulty.Max = null; + }); + WaitForFiltering(); + AddAssert("1 set + 15 diffs displayed", () => Carousel.DisplayableItems == 16); + } + + [Test] + public void TestCarouselRemembersSelection() + { + Guid selectedID = Guid.Empty; + + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + SelectNextPanel(); + Select(); + + AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + + for (int i = 0; i < 5; i++) + { + ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + } + } + + [Test] + public void TestCarouselRemembersSelectionDifficultySort() + { + Guid selectedID = Guid.Empty; + + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + SortBy(SortMode.Difficulty); + + SelectNextGroup(); + + AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + + for (int i = 0; i < 5; i++) + { + ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + } + } + + [Test] + public void TestCarouselRetainsSelectionFromDifficultySort() + { + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + BeatmapInfo chosenBeatmap = null!; + + for (int i = 0; i < 3; i++) + { + int diff = i; + + AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]); + AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + + SortBy(SortMode.Difficulty); + AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + + SortBy(SortMode.Title); + AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + } + } + + [Test] + public void TestExternalRulesetChange() + { + ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true); + ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0)); + + WaitForFiltering(); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + + BeatmapSets.Add(testMixed); + }); + WaitForDrawablePanels(); + + SelectNextPanel(); + Select(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1; + }); + + ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1)); + + WaitForFiltering(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 2 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1; + }); + + ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2)); + + WaitForFiltering(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 2 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1; + }); + } + + [Test] + [Ignore("Difficulty sorting is broken when set headers are included.")] // todo: fix. + public void TestSortingWithDifficultyFiltered() + { + const int diffs_per_set = 3; + const int local_set_count = 2; + + AddStep("populate beatmap sets", () => + { + for (int i = 0; i < local_set_count; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set); + set.Beatmaps[0].StarRating = 3 - i; + set.Beatmaps[0].DifficultyName += $" ({3 - i}*)"; + set.Beatmaps[1].StarRating = 6 + i; + set.Beatmaps[1].DifficultyName += $" ({6 + i}*)"; + BeatmapSets.Add(set); + } + }); + + SortBy(SortMode.Difficulty); + + AddAssert($"3 sets + {local_set_count * diffs_per_set} diffs displayed", () => Carousel.DisplayableItems == 3 + local_set_count * diffs_per_set); + + ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); + + AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + + ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); + + AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + } + } +} From 725187245a4f839470603782f8955ce9403b099b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 08:30:59 +0300 Subject: [PATCH 06/10] Improve count check test assertions --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 30 +++++++++++++++++++ .../TestSceneBeatmapCarouselArtistGrouping.cs | 8 +++-- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 6 ++-- .../TestSceneBeatmapCarouselFiltering.cs | 26 +++++++++------- .../SelectV2/BeatmapCarouselFilterMatching.cs | 10 +++++++ 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 39f6c2230b..f99433983b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -160,6 +160,36 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected void CheckDisplayedBeatmapsCount(int expected) + { + AddAssert($"{expected} diffs displayed", () => + { + var matchingFilter = Carousel.Filters.OfType().Single(); + return matchingFilter.BeatmapItemsCount; + }, () => Is.EqualTo(expected)); + } + + protected void CheckDisplayedBeatmapSetsCount(int expected) + { + AddAssert($"{expected} sets displayed", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + // Using groupingFilter.SetItems.Count alone doesn't work. + // When sorting by difficulty, there can be more than one set panel for the same set displayed. + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); + }, () => Is.EqualTo(expected)); + } + + protected void CheckDisplayedGroupsCount(int expected) + { + AddAssert($"{expected} groups displayed", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + return groupingFilter.GroupItems.Count; + }, () => Is.EqualTo(expected)); + } + protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 8f822cbb1d..9cf9d07a94 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -181,7 +181,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("1 group + 1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 5); + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); CheckNoSelection(); SelectNextPanel(); @@ -200,7 +202,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("5 groups + 10 sets + 30 diffs displayed", () => Carousel.DisplayableItems == 45); + CheckDisplayedGroupsCount(5); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index bf20825bdb..2d39e40213 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -199,7 +199,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("3 groups + 3 diffs displayed", () => Carousel.DisplayableItems == 6); + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(3); CheckNoSelection(); SelectNextPanel(); @@ -220,7 +221,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("3 groups + 30 diffs displayed", () => Carousel.DisplayableItems == 33); + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(30); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index cb1b0ec31f..21c726f9ac 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("3 diffs + 1 set displayed", () => Carousel.DisplayableItems == 4); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); SelectNextPanel(); Select(); @@ -53,7 +54,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("30 diffs + 10 sets displayed", () => Carousel.DisplayableItems == 40); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); } [Test] @@ -85,7 +87,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = null; }); WaitForFiltering(); - AddAssert("1 set + 11 diffs displayed", () => Carousel.DisplayableItems == 12); + CheckDisplayedBeatmapsCount(11); ApplyToFilter("filter to [0..7]", c => { @@ -93,7 +95,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = 7; }); WaitForFiltering(); - AddAssert("1 set + 7 diffs displayed", () => Carousel.DisplayableItems == 8); + CheckDisplayedBeatmapsCount(7); ApplyToFilter("filter to [5..7]", c => { @@ -102,7 +104,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); WaitForFiltering(); - AddAssert("1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 4); + CheckDisplayedBeatmapsCount(3); ApplyToFilter("filter to [2..2]", c => { @@ -111,7 +113,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); WaitForFiltering(); - AddAssert("`1 set + 1 diff displayed", () => Carousel.DisplayableItems == 2); + CheckDisplayedBeatmapsCount(1); ApplyToFilter("filter to [0..]", c => { @@ -119,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = null; }); WaitForFiltering(); - AddAssert("1 set + 15 diffs displayed", () => Carousel.DisplayableItems == 16); + CheckDisplayedBeatmapsCount(15); } [Test] @@ -269,16 +271,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); SortBy(SortMode.Difficulty); + WaitForFiltering(); - AddAssert($"3 sets + {local_set_count * diffs_per_set} diffs displayed", () => Carousel.DisplayableItems == 3 + local_set_count * diffs_per_set); + CheckDisplayedBeatmapSetsCount(3); + CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set); ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); - AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); - AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index f81f068ab7..4da23c1fd4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -16,6 +16,11 @@ namespace osu.Game.Screens.SelectV2 { private readonly Func getCriteria; + /// + /// Counts total number of beatmap difficulties displayed post filter. + /// + public int BeatmapItemsCount { get; private set; } + public BeatmapCarouselFilterMatching(Func getCriteria) { this.getCriteria = getCriteria; @@ -32,12 +37,17 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { + BeatmapItemsCount = 0; + foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; if (checkMatch(beatmap, criteria)) + { + BeatmapItemsCount++; yield return item; + } } } From 5d04cc045d2348e31c9f6741e6214a9fb4bd5442 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 15:44:01 +0900 Subject: [PATCH 07/10] Adjust matching filter's code to conform to other filter implementations --- .../SelectV2/BeatmapCarouselFilterMatching.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 4da23c1fd4..526e76b5f1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.SelectV2 private readonly Func getCriteria; /// - /// Counts total number of beatmap difficulties displayed post filter. + /// The total number of beatmap difficulties displayed post filter. /// public int BeatmapItemsCount { get; private set; } @@ -26,19 +26,16 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - return await Task.Run(() => - { - var criteria = getCriteria(); - return matchItems(items, criteria); - }, cancellationToken).ConfigureAwait(false); - } + BeatmapItemsCount = 0; + var criteria = getCriteria(); + + return matchItems(items, criteria); + }, cancellationToken).ConfigureAwait(false); private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { - BeatmapItemsCount = 0; - foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; From 1c6e998c951de030da40e0f3389096ed9b1583df Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 10:22:20 +0300 Subject: [PATCH 08/10] Expose matched beatmaps count from `BeatmapCarousel` --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 6 +----- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f99433983b..fd27f2a438 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -162,11 +162,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void CheckDisplayedBeatmapsCount(int expected) { - AddAssert($"{expected} diffs displayed", () => - { - var matchingFilter = Carousel.Filters.OfType().Single(); - return matchingFilter.BeatmapItemsCount; - }, () => Is.EqualTo(expected)); + AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); } protected void CheckDisplayedBeatmapSetsCount(int expected) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3578fd46fa..e9107e5505 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -31,8 +31,14 @@ namespace osu.Game.Screens.SelectV2 private readonly LoadingLayer loading; + private readonly BeatmapCarouselFilterMatching matching; private readonly BeatmapCarouselFilterGrouping grouping; + /// + /// Total number of beatmap difficulties displayed with the filter. + /// + public int MatchedBeatmapsCount => matching.BeatmapItemsCount; + protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) { if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) @@ -49,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { - new BeatmapCarouselFilterMatching(() => Criteria), + matching = new BeatmapCarouselFilterMatching(() => Criteria), new BeatmapCarouselFilterSorting(() => Criteria), grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; From 8775731c2442416015148c5984c312fd173c6849 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 10:28:00 +0300 Subject: [PATCH 09/10] Add `SortAndGroupBy` method to simplify usages --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 9 +++++++++ .../Visual/SongSelectV2/TestSceneBeatmapCarousel.cs | 11 +++-------- .../TestSceneBeatmapCarouselArtistGrouping.cs | 3 +-- .../TestSceneBeatmapCarouselDifficultyGrouping.cs | 3 +-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index fd27f2a438..9d30c44a11 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -136,6 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.ToString().ToLowerInvariant()}", c => c.Sort = mode); protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.ToString().ToLowerInvariant()}", c => c.Group = mode); + protected void SortAndGroupBy(SortMode sort, GroupMode group) + { + ApplyToFilter($"sort by {sort.ToString().ToLowerInvariant()} & group by {group.ToString().ToLowerInvariant()}", c => + { + c.Sort = sort; + c.Group = group; + }); + } + protected void ApplyToFilter(string description, Action? apply) { AddStep(description, () => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 870225edb3..21030e0b88 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -33,14 +33,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Explicit] public void TestSorting() { - SortBy(SortMode.Artist); - GroupBy(GroupMode.All); - - SortBy(SortMode.Difficulty); - GroupBy(GroupMode.Difficulty); - - SortBy(SortMode.Artist); - GroupBy(GroupMode.Artist); + SortAndGroupBy(SortMode.Artist, GroupMode.All); + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 9cf9d07a94..e404317cbd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -19,8 +19,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); - SortBy(SortMode.Artist); - GroupBy(GroupMode.Artist); + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 2d39e40213..f8e818809d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -21,8 +21,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); - SortBy(SortMode.Difficulty); - GroupBy(GroupMode.Difficulty); + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); AddBeatmaps(10, 3); WaitForDrawablePanels(); From 7789f4dbb0c68ee2b2753ae9d1fa201c5ff38e54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 16:26:43 +0900 Subject: [PATCH 10/10] Ensure `BeatmapItemsCount` is stable during filter operations --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 526e76b5f1..c1ce4aaa69 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - BeatmapItemsCount = 0; var criteria = getCriteria(); return matchItems(items, criteria); @@ -36,16 +35,20 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { + int countMatching = 0; + foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; if (checkMatch(beatmap, criteria)) { - BeatmapItemsCount++; + countMatching++; yield return item; } } + + BeatmapItemsCount = countMatching; } private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria)