diff --git a/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs new file mode 100644 index 0000000000..8f7027da17 --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkCarouselFilter.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 BenchmarkDotNet.Attributes; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkCarouselFilter : BenchmarkTest + { + private BeatmapInfo getExampleBeatmap() => new BeatmapInfo + { + Ruleset = new RulesetInfo + { + ShortName = "osu", + OnlineID = 0 + }, + StarRating = 4.0d, + Difficulty = new BeatmapDifficulty + { + ApproachRate = 5.0f, + DrainRate = 3.0f, + CircleSize = 2.0f, + }, + Metadata = new BeatmapMetadata + { + Artist = "The Artist", + ArtistUnicode = "check unicode too", + Title = "Title goes here", + TitleUnicode = "Title goes here", + Author = { Username = "The Author" }, + Source = "unit tests", + Tags = "look for tags too", + }, + DifficultyName = "version as well", + Length = 2500, + BPM = 160, + BeatDivisor = 12, + Status = BeatmapOnlineStatus.Loved + }; + + private CarouselBeatmap carouselBeatmap = null!; + private FilterCriteria criteria1 = null!; + private FilterCriteria criteria2 = null!; + private FilterCriteria criteria3 = null!; + private FilterCriteria criteria4 = null!; + private FilterCriteria criteria5 = null!; + private FilterCriteria criteria6 = null!; + + public override void SetUp() + { + var beatmap = getExampleBeatmap(); + beatmap.OnlineID = 20201010; + beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 }; + carouselBeatmap = new CarouselBeatmap(beatmap); + criteria1 = new FilterCriteria(); + criteria2 = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "catch" } + }; + criteria3 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + BPM = new FilterCriteria.OptionalRange + { + IsUpperInclusive = false, + Max = 160d + } + }; + criteria4 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = "an artist" + }; + criteria5 = new FilterCriteria + { + Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" } + }; + criteria6 = new FilterCriteria { SearchText = "20201010" }; + } + + [Benchmark] + public void CarouselBeatmapFilter() + { + carouselBeatmap.Filter(criteria1); + } + + [Benchmark] + public void CriteriaMatchingSpecificRuleset() + { + carouselBeatmap.Filter(criteria2); + } + + [Benchmark] + public void CriteriaMatchingRangeMax() + { + carouselBeatmap.Filter(criteria3); + } + + [Benchmark] + public void CriteriaMatchingTerms() + { + carouselBeatmap.Filter(criteria4); + } + + [Benchmark] + public void CriteriaMatchingCreator() + { + carouselBeatmap.Filter(criteria5); + } + + [Benchmark] + public void CriteriaMatchingBeatmapIDs() + { + carouselBeatmap.Filter(criteria6); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index eab66b9857..3aab9a24e1 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -29,10 +29,21 @@ namespace osu.Game.Beatmaps return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } - public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo) { - beatmapInfo.DifficultyName - }.Concat(beatmapInfo.Metadata.GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1); + + addIfNotNull(beatmapInfo.DifficultyName); + + BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList); + return termsList; + + void addIfNotNull(string? s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index 738bdb0839..fe4e815e62 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -3,7 +3,7 @@ #nullable disable -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -13,16 +13,31 @@ namespace osu.Game.Beatmaps /// /// An array of all searchable terms provided in contained metadata. /// - public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) { - metadataInfo.Author.Username, - metadataInfo.Artist, - metadataInfo.ArtistUnicode, - metadataInfo.Title, - metadataInfo.TitleUnicode, - metadataInfo.Source, - metadataInfo.Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(MAX_SEARCHABLE_TERM_COUNT); + CollectSearchableTerms(metadataInfo, termsList); + return termsList.ToArray(); + } + + internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + + internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList) + { + addIfNotNull(metadataInfo.Author.Username); + addIfNotNull(metadataInfo.Artist); + addIfNotNull(metadataInfo.ArtistUnicode); + addIfNotNull(metadataInfo.Title); + addIfNotNull(metadataInfo.TitleUnicode); + addIfNotNull(metadataInfo.Source); + addIfNotNull(metadataInfo.Tags); + + void addIfNotNull(string s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } /// /// A user-presentable display title representing this metadata. diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 837939716b..7e48bc5cdd 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -26,6 +26,11 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); + Filtered.Value = !checkMatch(criteria); + } + + private bool checkMatch(FilterCriteria criteria) + { bool match = criteria.Ruleset == null || BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName || @@ -34,8 +39,7 @@ namespace osu.Game.Screens.Select.Carousel if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap - Filtered.Value = !match; - return; + return match; } match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); @@ -49,18 +53,38 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); + if (!match) return false; + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); - if (match && criteria.SearchTerms.Length > 0) + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) { - string[] terms = BeatmapInfo.GetSearchableTerms(); + var terms = BeatmapInfo.GetSearchableTerms(); foreach (string criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); + { + bool any = false; + + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (string term in terms) + { + if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; + + any = true; + break; + } + + if (any) continue; + + match = false; + break; + } // 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. @@ -71,13 +95,14 @@ namespace osu.Game.Screens.Select.Carousel } } - if (match) - match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; + if (!match) return false; + + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); - Filtered.Value = !match; + return match; } public override int CompareTo(FilterCriteria criteria, CarouselItem other)