1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-23 22:44:54 +08:00

Merge pull request #33046 from frenzibyte/carousel-filtering

Add filtering support in carousel v2
This commit is contained in:
Dean Herbert
2025-05-07 17:42:33 +09:00
committed by GitHub
Unverified
10 changed files with 557 additions and 26 deletions
@@ -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<FilterCriteria>? apply)
{
AddStep(description, () =>
{
var criteria = Carousel.Criteria;
apply?.Invoke(criteria);
Carousel.Filter(criteria);
});
}
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().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<BeatmapCarouselFilterGrouping>().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<BeatmapCarouselFilterGrouping>().Single();
return groupingFilter.GroupItems.Count;
}, () => Is.EqualTo(expected));
}
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
@@ -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]
@@ -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);
}
}
}
@@ -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<PanelBeatmap>(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);
}
}
}
@@ -0,0 +1,290 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<PanelBeatmap>();
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<PanelBeatmap>();
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<PanelBeatmap>();
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);
}
}
}
@@ -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 });
}
/// <summary>
@@ -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<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
WaitForFiltering();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().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<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
WaitForFiltering();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
@@ -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<Panel>(), () => 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<Panel>(), () => 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]));
@@ -31,8 +31,14 @@ namespace osu.Game.Screens.SelectV2
private readonly LoadingLayer loading;
private readonly BeatmapCarouselFilterMatching matching;
private readonly BeatmapCarouselFilterGrouping grouping;
/// <summary>
/// Total number of beatmap difficulties displayed with the filter.
/// </summary>
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),
};
@@ -0,0 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<FilterCriteria> getCriteria;
/// <summary>
/// The total number of beatmap difficulties displayed post filter.
/// </summary>
public int BeatmapItemsCount { get; private set; }
public BeatmapCarouselFilterMatching(Func<FilterCriteria> getCriteria)
{
this.getCriteria = getCriteria;
}
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
{
var criteria = getCriteria();
return matchItems(items, criteria);
}, cancellationToken).ConfigureAwait(false);
private IEnumerable<CarouselItem> matchItems(IEnumerable<CarouselItem> 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;
}
}
}