1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 13:22:55 +08:00

Add query-based filter modes to song select search field (#6161)

Add query-based filter modes to song select search field

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
This commit is contained in:
Dean Herbert 2019-09-19 18:28:01 +09:00 committed by GitHub
commit 62dda81711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 13 deletions

View File

@ -301,6 +301,7 @@ namespace osu.Game.Beatmaps
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
beatmap.BeatmapInfo.Ruleset = ruleset; beatmap.BeatmapInfo.Ruleset = ruleset;
// TODO: this should be done in a better place once we actually need to dynamically update it. // TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.Length = calculateLength(beatmap); beatmap.BeatmapInfo.Length = calculateLength(beatmap);

View File

@ -24,12 +24,26 @@ namespace osu.Game.Screens.Select.Carousel
{ {
base.Filter(criteria); base.Filter(criteria);
bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); bool match =
criteria.Ruleset == null ||
Beatmap.RulesetID == criteria.Ruleset.ID ||
(Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps);
foreach (var criteriaTerm in criteria.SearchTerms) match &= criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty);
match &= match &= criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) || match &= criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; match &= criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize);
match &= criteria.Length.IsInRange(Beatmap.Length);
match &= criteria.BPM.IsInRange(Beatmap.BPM);
match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor);
match &= criteria.OnlineStatus.IsInRange(Beatmap.Status);
if (match)
foreach (var criteriaTerm in criteria.SearchTerms)
match &=
Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) ||
Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
Filtered.Value = !match; Filtered.Value = !match;
} }

View File

@ -16,6 +16,8 @@ using Container = osu.Framework.Graphics.Containers.Container;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -33,14 +35,24 @@ namespace osu.Game.Screens.Select
private Bindable<GroupMode> groupMode; private Bindable<GroupMode> groupMode;
public FilterCriteria CreateCriteria() => new FilterCriteria public FilterCriteria CreateCriteria()
{ {
Group = groupMode.Value, var query = searchTextBox.Text;
Sort = sortMode.Value,
SearchText = searchTextBox.Text, var criteria = new FilterCriteria
AllowConvertedBeatmaps = showConverted.Value, {
Ruleset = ruleset.Value Group = groupMode.Value,
}; Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value
};
applyQueries(criteria, ref query);
criteria.SearchText = query;
return criteria;
}
public Action Exit; public Action Exit;
@ -169,5 +181,129 @@ namespace osu.Game.Screens.Select
} }
private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria());
private static readonly Regex query_syntax_regex = new Regex(
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status)(?<op>[=:><]+)(?<value>\S*)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private void applyQueries(FilterCriteria criteria, ref string query)
{
foreach (Match match in query_syntax_regex.Matches(query))
{
var key = match.Groups["key"].Value.ToLower();
var op = match.Groups["op"].Value;
var value = match.Groups["value"].Value;
switch (key)
{
case "stars" when float.TryParse(value, out var stars):
updateCriteriaRange(ref criteria.StarDifficulty, op, stars);
break;
case "ar" when float.TryParse(value, out var ar):
updateCriteriaRange(ref criteria.ApproachRate, op, ar);
break;
case "dr" when float.TryParse(value, out var dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr);
break;
case "cs" when float.TryParse(value, out var cs):
updateCriteriaRange(ref criteria.CircleSize, op, cs);
break;
case "bpm" when double.TryParse(value, out var bpm):
updateCriteriaRange(ref criteria.BPM, op, bpm);
break;
case "length" when double.TryParse(value.TrimEnd('m', 's', 'h'), out var length):
var scale =
value.EndsWith("ms") ? 1 :
value.EndsWith("s") ? 1000 :
value.EndsWith("m") ? 60000 :
value.EndsWith("h") ? 3600000 : 1000;
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
break;
case "divisor" when int.TryParse(value, out var divisor):
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
break;
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
break;
}
query = query.Replace(match.ToString(), "");
}
}
private void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
{
updateCriteriaRange(ref range, op, value);
switch (op)
{
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
}
}
private void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
{
updateCriteriaRange(ref range, op, value);
switch (op)
{
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
}
}
private void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
where T : struct, IComparable
{
switch (op)
{
default:
return;
case "=":
case ":":
range.IsInclusive = true;
range.Min = value;
range.Max = value;
break;
case ">":
range.IsInclusive = false;
range.Min = value;
break;
case ">=":
case ">:":
range.IsInclusive = true;
range.Min = value;
break;
case "<":
range.IsInclusive = false;
range.Max = value;
break;
case "<=":
case "<:":
range.IsInclusive = true;
range.Max = value;
break;
}
}
} }
} }

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -13,6 +15,15 @@ namespace osu.Game.Screens.Select
public GroupMode Group; public GroupMode Group;
public SortMode Sort; public SortMode Sort;
public OptionalRange<double> StarDifficulty;
public OptionalRange<float> ApproachRate;
public OptionalRange<float> DrainRate;
public OptionalRange<float> CircleSize;
public OptionalRange<double> Length;
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapSetOnlineStatus> OnlineStatus;
public string[] SearchTerms = Array.Empty<string>(); public string[] SearchTerms = Array.Empty<string>();
public RulesetInfo Ruleset; public RulesetInfo Ruleset;
@ -26,8 +37,48 @@ namespace osu.Game.Screens.Select
set set
{ {
searchText = value; searchText = value;
SearchTerms = searchText.Split(',', ' ', '!').Where(s => !string.IsNullOrEmpty(s)).ToArray(); SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
} }
} }
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct, IComparable
{
public bool IsInRange(T value)
{
if (Min != null)
{
int comparison = Comparer<T>.Default.Compare(value, Min.Value);
if (comparison < 0)
return false;
if (comparison == 0 && !IsInclusive)
return false;
}
if (Max != null)
{
int comparison = Comparer<T>.Default.Compare(value, Max.Value);
if (comparison > 0)
return false;
if (comparison == 0 && !IsInclusive)
return false;
}
return true;
}
public T? Min;
public T? Max;
public bool IsInclusive;
public bool Equals(OptionalRange<T> other)
=> Min.Equals(other.Min)
&& Max.Equals(other.Max)
&& IsInclusive.Equals(other.IsInclusive);
}
} }
} }