Add query-based filter modes to song select search field

This commit is contained in:
Dean Herbert 2019-09-19 02:37:35 +09:00
parent 52fcd834ab
commit 92556db9cd
4 changed files with 163 additions and 13 deletions

View File

@ -301,6 +301,7 @@ private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader)
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);

View File

@ -24,12 +24,26 @@ public override void Filter(FilterCriteria 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 &=
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.HasValue || criteria.BeatDivisor == Beatmap.BeatDivisor;
match &= !criteria.OnlineStatus.HasValue || criteria.OnlineStatus == 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;
}

View File

@ -16,6 +16,8 @@
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 @@ public class FilterControl : Container
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,97 @@ private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuCo
}
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 double.TryParse(value, out var stars):
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.5);
break;
case "ar" when double.TryParse(value, out var ar):
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.3);
break;
case "dr" when double.TryParse(value, out var dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.3);
break;
case "cs" when double.TryParse(value, out var cs):
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.3);
break;
case "bpm" when double.TryParse(value, out var bpm):
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.3);
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 op == ":" && int.TryParse(value, out var divisor):
criteria.BeatDivisor = divisor;
break;
case "status" when op == ":" && Enum.TryParse<BeatmapSetOnlineStatus>(value, ignoreCase: true, out var statusValue):
criteria.OnlineStatus = statusValue;
break;
}
query = query.Remove(match.Index, match.Length);
}
}
private void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double equalityToleration = 0)
{
switch (op)
{
default:
return;
case ":":
range.IsInclusive = true;
range.Min = value - equalityToleration;
range.Max = value + equalityToleration;
break;
case ">":
range.IsInclusive = false;
range.Min = value;
break;
case ">:":
range.IsInclusive = true;
range.Min = value;
break;
case "<":
range.IsInclusive = false;
range.Max = value;
break;
case "<:":
range.IsInclusive = true;
range.Max = value;
break;
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter;
@ -13,6 +14,17 @@ public class FilterCriteria
public GroupMode Group;
public SortMode Sort;
public OptionalRange StarDifficulty;
public OptionalRange ApproachRate;
public OptionalRange DrainRate;
public OptionalRange CircleSize;
public OptionalRange Length;
public OptionalRange BPM;
public int? BeatDivisor;
public BeatmapSetOnlineStatus? OnlineStatus;
public string[] SearchTerms = Array.Empty<string>();
public RulesetInfo Ruleset;
@ -26,8 +38,27 @@ public string SearchText
set
{
searchText = value;
SearchTerms = searchText.Split(',', ' ', '!').Where(s => !string.IsNullOrEmpty(s)).ToArray();
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
}
}
public struct OptionalRange : IEquatable<OptionalRange>
{
public bool IsInRange(double value)
{
if (Min.HasValue && (IsInclusive ? value < Min.Value : value <= Min.Value))
return false;
if (Max.HasValue && (IsInclusive ? value > Max.Value : value >= Max.Value))
return false;
return true;
}
public double? Min;
public double? Max;
public bool IsInclusive;
public bool Equals(OptionalRange range) => Min == range.Min && Max == range.Max;
}
}
}