diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 28a0948696..9d30c44a11 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,12 +128,35 @@ 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 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, () => + { + 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 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)); @@ -145,6 +169,32 @@ 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", () => Carousel.MatchedBeatmapsCount, () => 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/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 5fd921645b..21030e0b88 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,9 @@ 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 }); + 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 f0caa796b6..e404317cbd 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,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); @@ -173,5 +173,37 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextGroup(); WaitForGroupSelection(1, 1); } + + [Test] + public void TestBasicFiltering() + { + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + 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(); + + 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 a4cdf8abcb..f8e818809d 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,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); AddBeatmaps(10, 3); WaitForDrawablePanels(); @@ -191,5 +191,37 @@ 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(); + + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(3); + + 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(); + + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(30); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs new file mode 100644 index 0000000000..21c726f9ac --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -0,0 +1,290 @@ +// 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(); + + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + 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(); + + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); + } + + [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(); + CheckDisplayedBeatmapsCount(11); + + ApplyToFilter("filter to [0..7]", c => + { + c.UserStarDifficulty.Min = null; + c.UserStarDifficulty.Max = 7; + }); + WaitForFiltering(); + CheckDisplayedBeatmapsCount(7); + + ApplyToFilter("filter to [5..7]", c => + { + c.UserStarDifficulty.Min = 5; + c.UserStarDifficulty.Max = 7; + }); + + WaitForFiltering(); + CheckDisplayedBeatmapsCount(3); + + ApplyToFilter("filter to [2..2]", c => + { + c.UserStarDifficulty.Min = 2; + c.UserStarDifficulty.Max = 2; + }); + + WaitForFiltering(); + CheckDisplayedBeatmapsCount(1); + + ApplyToFilter("filter to [0..]", c => + { + c.UserStarDifficulty.Min = 0; + c.UserStarDifficulty.Max = null; + }); + WaitForFiltering(); + CheckDisplayedBeatmapsCount(15); + } + + [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); + WaitForFiltering(); + + CheckDisplayedBeatmapSetsCount(3); + CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set); + + ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); + + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); + + ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); + + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); + } + } +} 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 da3fc98c19..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(); @@ -37,7 +38,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 +58,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..b9a468d580 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,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 updateBeatmap(b => b.Metadata = metadata); - WaitForSorting(); + WaitForFiltering(); AddAssert("drawables changed", () => Carousel.ChildrenOfType(), () => Is.Not.EqualTo(originalDrawables)); } [Test] public void TestSelectionHeld() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); 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])); 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])); @@ -101,14 +101,14 @@ 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])); 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])); @@ -117,14 +117,14 @@ 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])); 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])); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4af5e759a7..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,6 +55,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { + matching = 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..c1ce4aaa69 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -0,0 +1,123 @@ +// 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; + + /// + /// The total number of beatmap difficulties displayed post filter. + /// + public int BeatmapItemsCount { get; private set; } + + public BeatmapCarouselFilterMatching(Func getCriteria) + { + this.getCriteria = getCriteria; + } + + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + var criteria = getCriteria(); + + return matchItems(items, criteria); + }, cancellationToken).ConfigureAwait(false); + + private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) + { + int countMatching = 0; + + foreach (var item in items) + { + var beatmap = (BeatmapInfo)item.Model; + + if (checkMatch(beatmap, criteria)) + { + countMatching++; + yield return item; + } + } + + BeatmapItemsCount = countMatching; + } + + 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; + } + } +}