1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 23:27:25 +08:00

Merge branch 'master' into hud/kc-skinnable

This commit is contained in:
Bartłomiej Dach 2023-06-27 20:35:47 +02:00
commit af66ccbfdf
No known key found for this signature in database
7 changed files with 104 additions and 25 deletions

View File

@ -159,6 +159,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(filtered, carouselItem.Filtered.Value); Assert.AreEqual(filtered, carouselItem.Filtered.Value);
} }
[Test]
[TestCase("\"artist\"", false)]
[TestCase("\"arti\"", true)]
[TestCase("\"artist title author\"", true)]
[TestCase("\"artist\" \"title\" \"author\"", false)]
[TestCase("\"an artist\"", true)]
[TestCase("\"tags too\"", false)]
[TestCase("\"tags to\"", true)]
[TestCase("\"version\"", false)]
[TestCase("\"an auteur\"", true)]
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = 6 },
AllowConvertedBeatmaps = true,
SearchText = terms
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test] [Test]
[TestCase("", false)] [TestCase("", false)]
[TestCase("The", false)] [TestCase("The", false)]

View File

@ -23,6 +23,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(4, filterCriteria.SearchTerms.Length); Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
} }
[Test]
public void TestApplyQueriesBareWordsWithExactMatch()
{
const string query = "looking for \"a beatmap\" like \"this\"";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("looking for \"a beatmap\" like \"this\"", filterCriteria.SearchText);
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap"));
Assert.That(filterCriteria.SearchTerms[0].Exact, Is.True);
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this"));
Assert.That(filterCriteria.SearchTerms[1].Exact, Is.True);
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking"));
Assert.That(filterCriteria.SearchTerms[2].Exact, Is.False);
Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for"));
Assert.That(filterCriteria.SearchTerms[3].Exact, Is.False);
Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like"));
Assert.That(filterCriteria.SearchTerms[4].Exact, Is.False);
}
/* /*
* The following tests have been written a bit strangely (they don't check exact * The following tests have been written a bit strangely (they don't check exact
* bound equality with what the filter says). * bound equality with what the filter says).
@ -235,6 +260,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
Assert.That(filterCriteria.Artist.Exact, Is.False);
} }
[Test] [Test]
@ -246,6 +272,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim()); Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm); Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
Assert.That(filterCriteria.Artist.Exact, Is.True);
} }
[Test] [Test]

View File

@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any()); AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true);
AddAssert("filter request not fired", () => !received); AddAssert("filter request not fired", () => !received);
} }

View File

@ -378,9 +378,6 @@ namespace osu.Game.Screens.Play
IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindTo(breakTracker.IsBreakTime);
IsBreakTime.BindValueChanged(onBreakTimeChanged, true); IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
loadLeaderboard(); loadLeaderboard();
} }
@ -1086,6 +1083,9 @@ namespace osu.Game.Screens.Play
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
GameplayClockContainer.Reset(startClock: true); GameplayClockContainer.Reset(startClock: true);
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
} }
public override void OnSuspending(ScreenTransitionEvent e) public override void OnSuspending(ScreenTransitionEvent e)

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -65,16 +64,16 @@ namespace osu.Game.Screens.Select.Carousel
if (criteria.SearchTerms.Length > 0) if (criteria.SearchTerms.Length > 0)
{ {
var terms = BeatmapInfo.GetSearchableTerms(); var searchableTerms = BeatmapInfo.GetSearchableTerms();
foreach (string criteriaTerm in criteria.SearchTerms) foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms)
{ {
bool any = false; bool any = false;
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (string term in terms) foreach (string searchTerm in searchableTerms)
{ {
if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; if (!criteriaTerm.Matches(searchTerm)) continue;
any = true; any = true;
break; break;
@ -98,7 +97,6 @@ namespace osu.Game.Screens.Select.Carousel
if (!match) return false; if (!match) return false;
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null) if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo); match &= criteria.RulesetCriteria.Matches(BeatmapInfo);

View File

@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using System.Text.RegularExpressions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -20,7 +18,7 @@ namespace osu.Game.Screens.Select
public GroupMode Group; public GroupMode Group;
public SortMode Sort; public SortMode Sort;
public BeatmapSetInfo SelectedBeatmapSet; public BeatmapSetInfo? SelectedBeatmapSet;
public OptionalRange<double> StarDifficulty; public OptionalRange<double> StarDifficulty;
public OptionalRange<float> ApproachRate; public OptionalRange<float> ApproachRate;
@ -40,12 +38,12 @@ namespace osu.Game.Screens.Select
IsUpperInclusive = true IsUpperInclusive = true
}; };
public string[] SearchTerms = Array.Empty<string>(); public OptionalTextFilter[] SearchTerms = Array.Empty<OptionalTextFilter>();
public RulesetInfo Ruleset; public RulesetInfo? Ruleset;
public bool AllowConvertedBeatmaps; public bool AllowConvertedBeatmaps;
private string searchText; private string searchText = string.Empty;
/// <summary> /// <summary>
/// <see cref="SearchText"/> as a number (if it can be parsed as one). /// <see cref="SearchText"/> as a number (if it can be parsed as one).
@ -58,11 +56,29 @@ namespace osu.Game.Screens.Select
set set
{ {
searchText = value; searchText = value;
SearchTerms = searchText.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
List<OptionalTextFilter> terms = new List<OptionalTextFilter>();
string remainingText = value;
// First handle quoted segments to ensure we keep inline spaces in exact matches.
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\")"))
{
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
}
// Then handle the rest splitting on any spaces.
terms.AddRange(remainingText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(s => new OptionalTextFilter
{
SearchTerm = s
}));
SearchTerms = terms.ToArray();
SearchNumber = null; SearchNumber = null;
if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed)) if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0].SearchTerm, out int parsed))
SearchNumber = parsed; SearchNumber = parsed;
} }
} }
@ -70,11 +86,9 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// Hashes from the <see cref="BeatmapCollection"/> to filter to. /// Hashes from the <see cref="BeatmapCollection"/> to filter to.
/// </summary> /// </summary>
[CanBeNull] public IEnumerable<string>? CollectionBeatmapMD5Hashes { get; set; }
public IEnumerable<string> CollectionBeatmapMD5Hashes { get; set; }
[CanBeNull] public IRulesetFilterCriteria? RulesetCriteria { get; set; }
public IRulesetFilterCriteria RulesetCriteria { get; set; }
public struct OptionalRange<T> : IEquatable<OptionalRange<T>> public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct where T : struct
@ -124,6 +138,8 @@ namespace osu.Game.Screens.Select
{ {
public bool HasFilter => !string.IsNullOrEmpty(SearchTerm); public bool HasFilter => !string.IsNullOrEmpty(SearchTerm);
public bool Exact { get; private set; }
public bool Matches(string value) public bool Matches(string value)
{ {
if (!HasFilter) if (!HasFilter)
@ -133,10 +149,23 @@ namespace osu.Game.Screens.Select
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return false; return false;
if (Exact)
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
} }
public string SearchTerm; private string searchTerm;
public string SearchTerm
{
get => searchTerm;
set
{
searchTerm = value.Trim('"');
Exact = searchTerm != value;
}
}
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
} }

View File

@ -161,7 +161,7 @@ namespace osu.Game.Screens.Select
switch (op) switch (op)
{ {
case Operator.Equal: case Operator.Equal:
textFilter.SearchTerm = value.Trim('"'); textFilter.SearchTerm = value;
return true; return true;
default: default: