mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +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:
commit
62dda81711
@ -301,6 +301,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
|
||||
beatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
|
||||
// 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.Length = calculateLength(beatmap);
|
||||
|
@ -24,12 +24,26 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
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 &=
|
||||
Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) ||
|
||||
Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
|
||||
match &= criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty);
|
||||
match &= criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
|
||||
match &= criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
|
||||
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;
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ using Container = osu.Framework.Graphics.Containers.Container;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
@ -33,14 +35,24 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private Bindable<GroupMode> groupMode;
|
||||
|
||||
public FilterCriteria CreateCriteria() => new FilterCriteria
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
Group = groupMode.Value,
|
||||
Sort = sortMode.Value,
|
||||
SearchText = searchTextBox.Text,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value
|
||||
};
|
||||
var query = searchTextBox.Text;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Group = groupMode.Value,
|
||||
Sort = sortMode.Value,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value
|
||||
};
|
||||
|
||||
applyQueries(criteria, ref query);
|
||||
|
||||
criteria.SearchText = query;
|
||||
|
||||
return criteria;
|
||||
}
|
||||
|
||||
public Action Exit;
|
||||
|
||||
@ -169,5 +181,129 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
@ -13,6 +15,15 @@ namespace osu.Game.Screens.Select
|
||||
public GroupMode Group;
|
||||
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 RulesetInfo Ruleset;
|
||||
@ -26,8 +37,48 @@ namespace osu.Game.Screens.Select
|
||||
set
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user