Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to search for difficulty names using square brackets #24921

Merged
merged 15 commits into from
Oct 24, 2023
Merged
53 changes: 53 additions & 0 deletions osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;

namespace osu.Game.Tests.NonVisual.Filtering
Expand Down Expand Up @@ -382,6 +384,57 @@ public void TestCustomKeywordIsParsed()
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
}

[TestCase("[1]", new[] { 0 })]
[TestCase("[1", new[] { 0 })]
[TestCase("My[Favourite", new[] { 2 })]
[TestCase("My[Favourite]", new[] { 2 })]
[TestCase("My[Favourite]Song", new[] { 2 })]
[TestCase("Favourite]", new[] { 2 })]
[TestCase("[Diff", new[] { 0, 1, 3, 4, 6 })]
[TestCase("[Diff]", new[] { 0, 1, 3, 4, 6 })]
[TestCase("[Favourite]", new[] { 3 })]
[TestCase("Title1 [Diff]", new[] { 0, 1 })]
[TestCase("Title1[Diff]", new int[] { })]
[TestCase("[diff ]with]", new[] { 4 })]
[TestCase("[diff ]with [[ brackets]]]]", new[] { 4 })]
[TestCase("[Diff in title]", new int[] { })]
[TestCase("[Diff in diff]", new[] { 6 })]
[TestCase("diff=Diff", new[] { 0, 1, 3, 4, 6 })]
[TestCase("diff=Diff1", new[] { 0 })]
[TestCase("diff=\"Diff\"", new[] { 3, 4, 6 })]
[TestCase("diff=!\"Diff\"", new int[] { })]
public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes)
{
var carouselBeatmaps = (((string title, string difficultyName)[])new[]
{
("Title1", "Diff1"),
("Title1", "Diff2"),
("My[Favourite]Song", "Expert"),
("Title", "My Favourite Diff"),
("Another One", "diff ]with [[ brackets]]]"),
("Diff in title", "a"),
("a", "Diff in diff"),
}).Select(info => new CarouselBeatmap(new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = info.title
},
DifficultyName = info.difficultyName
})).ToList();

var criteria = new FilterCriteria();

FilterQueryParser.ApplyQueries(criteria, query);
carouselBeatmaps.ForEach(b => b.Filter(criteria));

int[] visibleBeatmaps = carouselBeatmaps
.Where(b => !b.Filtered.Value)
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();

Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
}

private class CustomFilterCriteria : IRulesetFilterCriteria
{
public string? CustomValue { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private bool checkMatch(FilterCriteria criteria)
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);

match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);

if (!match) return false;
Expand Down
18 changes: 17 additions & 1 deletion osu.Game/Screens/Select/FilterCriteria.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class FilterCriteria
public OptionalTextFilter Creator;
public OptionalTextFilter Artist;
public OptionalTextFilter Title;
public OptionalTextFilter DifficultyName;

public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
{
Expand Down Expand Up @@ -68,8 +69,23 @@ public string SearchText

string remainingText = value;

// Match either an open difficulty tag to the end of string,
// or match a closed one with a whitespace after it.
//
// To keep things simple, the closing ']' may be included in the match group,
// and is trimmed post-match.
foreach (Match quotedSegment in Regex.Matches(value, "(^|\\s)\\[(.*)(\\]\\s|$)"))
{
DifficultyName = new OptionalTextFilter
{
SearchTerm = quotedSegment.Groups[2].Value.Trim(']')
};

remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
}

// First handle quoted segments to ensure we keep inline spaces in exact matches.
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)"))
{
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/Screens/Select/FilterQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key,
case "title":
return TryUpdateCriteriaText(ref criteria.Title, op, value);

case "diff":
return TryUpdateCriteriaText(ref criteria.DifficultyName, op, value);

default:
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
}
Expand Down