mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 11:42:56 +08:00
Merge branch 'master' into tournament-setup
This commit is contained in:
commit
45015fc4ba
@ -23,10 +23,10 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="4.6.0" />
|
||||
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
|
||||
|
201
osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
Normal file
201
osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
Normal file
@ -0,0 +1,201 @@
|
||||
// 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.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
[TestFixture]
|
||||
public class FilterMatchingTest
|
||||
{
|
||||
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 5 },
|
||||
StarDifficulty = 4.0d,
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 5.0f,
|
||||
DrainRate = 3.0f,
|
||||
CircleSize = 2.0f,
|
||||
},
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "The Artist",
|
||||
ArtistUnicode = "check unicode too",
|
||||
Title = "Title goes here",
|
||||
TitleUnicode = "Title goes here",
|
||||
AuthorString = "The Author",
|
||||
Source = "unit tests",
|
||||
Tags = "look for tags too",
|
||||
},
|
||||
Version = "version as well",
|
||||
Length = 2500,
|
||||
BPM = 160,
|
||||
BeatDivisor = 12,
|
||||
Status = BeatmapSetOnlineStatus.Loved
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestCriteriaMatchingNoRuleset()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria();
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.IsFalse(carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCriteriaMatchingSpecificRuleset()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.IsTrue(carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCriteriaMatchingConvertedBeatmaps()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
AllowConvertedBeatmaps = true
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.IsFalse(carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestCriteriaMatchingRangeMin(bool inclusive)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
AllowConvertedBeatmaps = true,
|
||||
ApproachRate = new FilterCriteria.OptionalRange<float>
|
||||
{
|
||||
IsLowerInclusive = inclusive,
|
||||
Min = 5.0f
|
||||
}
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestCriteriaMatchingRangeMax(bool inclusive)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
AllowConvertedBeatmaps = true,
|
||||
BPM = new FilterCriteria.OptionalRange<double>
|
||||
{
|
||||
IsUpperInclusive = inclusive,
|
||||
Max = 160d
|
||||
}
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("artist", false)]
|
||||
[TestCase("artist title author", false)]
|
||||
[TestCase("an artist", true)]
|
||||
[TestCase("tags too", false)]
|
||||
[TestCase("version", false)]
|
||||
[TestCase("an auteur", true)]
|
||||
public void TestCriteriaMatchingTerms(string terms, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
AllowConvertedBeatmaps = true,
|
||||
SearchText = terms
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("The", false)]
|
||||
[TestCase("THE", false)]
|
||||
[TestCase("author", false)]
|
||||
[TestCase("the author", false)]
|
||||
[TestCase("the author AND then something else", true)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingCreator(string creatorName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("The", false)]
|
||||
[TestCase("THE", false)]
|
||||
[TestCase("artist", false)]
|
||||
[TestCase("the artist", false)]
|
||||
[TestCase("the artist AND then something else", true)]
|
||||
[TestCase("unicode too", false)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("artist", false)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
exampleBeatmapInfo.Metadata.ArtistUnicode = null;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
}
|
||||
}
|
184
osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Normal file
184
osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Normal file
@ -0,0 +1,184 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
[TestFixture]
|
||||
public class FilterQueryParserTest
|
||||
{
|
||||
[Test]
|
||||
public void TestApplyQueriesBareWords()
|
||||
{
|
||||
const string query = "looking for a beatmap";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("looking for a beatmap", filterCriteria.SearchText);
|
||||
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
|
||||
}
|
||||
|
||||
/*
|
||||
* The following tests have been written a bit strangely (they don't check exact
|
||||
* bound equality with what the filter says).
|
||||
* This is to account for floating-point arithmetic issues.
|
||||
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
|
||||
* of 139.99999, which would be displayed in the UI as 140.
|
||||
* Due to this the tests check the last tick inside the range and the first tick
|
||||
* outside of the range.
|
||||
*/
|
||||
|
||||
[Test]
|
||||
public void TestApplyStarQueries()
|
||||
{
|
||||
const string query = "stars<4 easy";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
|
||||
Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
|
||||
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
|
||||
Assert.IsNull(filterCriteria.StarDifficulty.Min);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyApproachRateQueries()
|
||||
{
|
||||
const string query = "ar>=9 difficult";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.IsNotNull(filterCriteria.ApproachRate.Min);
|
||||
Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
|
||||
Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
|
||||
Assert.IsNull(filterCriteria.ApproachRate.Max);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyDrainRateQueries()
|
||||
{
|
||||
const string query = "dr>2 quite specific dr<:6";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
|
||||
Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
|
||||
Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
|
||||
Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
|
||||
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyBPMQueries()
|
||||
{
|
||||
const string query = "bpm>:200 gotta go fast";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||
Assert.IsNotNull(filterCriteria.BPM.Min);
|
||||
Assert.Greater(filterCriteria.BPM.Min, 199.99d);
|
||||
Assert.Less(filterCriteria.BPM.Min, 200.00d);
|
||||
Assert.IsNull(filterCriteria.BPM.Max);
|
||||
}
|
||||
|
||||
private static object[] lengthQueryExamples =
|
||||
{
|
||||
new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
|
||||
new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
|
||||
new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
|
||||
new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
|
||||
new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(lengthQueryExamples))]
|
||||
public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
|
||||
{
|
||||
string query = $"length={lengthQuery} time";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("time", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min);
|
||||
Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyDivisorQueries()
|
||||
{
|
||||
const string query = "that's a time signature alright! divisor:12";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("that's a time signature alright!", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual(12, filterCriteria.BeatDivisor.Min);
|
||||
Assert.IsTrue(filterCriteria.BeatDivisor.IsLowerInclusive);
|
||||
Assert.AreEqual(12, filterCriteria.BeatDivisor.Max);
|
||||
Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyStatusQueries()
|
||||
{
|
||||
const string query = "I want the pp status=ranked";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
|
||||
Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
|
||||
Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
|
||||
Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyCreatorQueries()
|
||||
{
|
||||
const string query = "beatmap specifically by creator=my_fav";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueries()
|
||||
{
|
||||
const string query = "find me songs by artist=singer please";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueriesWithSpaces()
|
||||
{
|
||||
const string query = "really like artist=\"name with space\" yes";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueriesOneDoubleQuote()
|
||||
{
|
||||
const string query = "weird artist=double\"quote";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("weird", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
}
|
||||
}
|
@ -242,6 +242,21 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
|
||||
AddAssert("Selection is non-null", () => currentSelection != null);
|
||||
|
||||
setSelected(1, 3);
|
||||
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
|
||||
{
|
||||
SearchText = "#3",
|
||||
StarDifficulty = new FilterCriteria.OptionalRange<double>
|
||||
{
|
||||
Min = 2,
|
||||
Max = 5.5,
|
||||
IsLowerInclusive = true
|
||||
}
|
||||
}, false));
|
||||
checkSelected(3, 2);
|
||||
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,6 +11,6 @@
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -15,6 +15,7 @@ using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Net;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Changelog
|
||||
{
|
||||
@ -67,23 +68,35 @@ namespace osu.Game.Overlays.Changelog
|
||||
|
||||
foreach (APIChangelogEntry entry in categoryEntries)
|
||||
{
|
||||
LinkFlowContainer title = new LinkFlowContainer
|
||||
var entryColour = entry.Major ? colours.YellowLight : Color4.White;
|
||||
|
||||
LinkFlowContainer title;
|
||||
|
||||
Container titleContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Vertical = 5 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(fontSmall.Size),
|
||||
Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus,
|
||||
Colour = entryColour,
|
||||
Margin = new MarginPadding { Right = 5 },
|
||||
},
|
||||
title = new LinkFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Full,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Vertical = 5 },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var entryColour = entry.Major ? colours.YellowLight : Color4.White;
|
||||
|
||||
title.AddIcon(entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus, t =>
|
||||
{
|
||||
t.Font = fontSmall;
|
||||
t.Colour = entryColour;
|
||||
t.Padding = new MarginPadding { Left = -17, Right = 5 };
|
||||
});
|
||||
|
||||
title.AddText(entry.Title, t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
@ -139,7 +152,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
|
||||
ChangelogEntries.Add(title);
|
||||
ChangelogEntries.Add(titleContainer);
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.MessageHtml))
|
||||
{
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public event Action<ButtonState> StateChanged;
|
||||
|
||||
public readonly Key TriggerKey;
|
||||
|
||||
private readonly Container iconText;
|
||||
private readonly Container box;
|
||||
private readonly Box boxHoverLayer;
|
||||
@ -43,7 +45,6 @@ namespace osu.Game.Screens.Menu
|
||||
public ButtonSystemState VisibleState = ButtonSystemState.TopLevel;
|
||||
|
||||
private readonly Action clickAction;
|
||||
private readonly Key triggerKey;
|
||||
private SampleChannel sampleClick;
|
||||
private SampleChannel sampleHover;
|
||||
|
||||
@ -53,7 +54,7 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
this.sampleName = sampleName;
|
||||
this.clickAction = clickAction;
|
||||
this.triggerKey = triggerKey;
|
||||
TriggerKey = triggerKey;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Alpha = 0;
|
||||
@ -210,7 +211,7 @@ namespace osu.Game.Screens.Menu
|
||||
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
|
||||
return false;
|
||||
|
||||
if (triggerKey == e.Key && triggerKey != Key.Unknown)
|
||||
if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
|
||||
{
|
||||
trigger();
|
||||
return true;
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
@ -180,6 +181,20 @@ namespace osu.Game.Screens.Menu
|
||||
State = ButtonSystemState.Initial;
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (State == ButtonSystemState.Initial)
|
||||
{
|
||||
if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey))
|
||||
{
|
||||
logo?.Click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
switch (action)
|
||||
|
@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor);
|
||||
match &= criteria.OnlineStatus.IsInRange(Beatmap.Status);
|
||||
|
||||
match &= criteria.Creator.Matches(Beatmap.Metadata.AuthorString);
|
||||
match &= criteria.Artist.Matches(Beatmap.Metadata.Artist) ||
|
||||
criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode);
|
||||
|
||||
if (match)
|
||||
foreach (var criteriaTerm in criteria.SearchTerms)
|
||||
match &=
|
||||
|
@ -16,8 +16,6 @@ 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
|
||||
{
|
||||
@ -47,10 +45,7 @@ namespace osu.Game.Screens.Select
|
||||
Ruleset = ruleset.Value
|
||||
};
|
||||
|
||||
applyQueries(criteria, ref query);
|
||||
|
||||
criteria.SearchText = query;
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
return criteria;
|
||||
}
|
||||
|
||||
@ -181,129 +176,5 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Screens.Select
|
||||
public OptionalRange<double> BPM;
|
||||
public OptionalRange<int> BeatDivisor;
|
||||
public OptionalRange<BeatmapSetOnlineStatus> OnlineStatus;
|
||||
public OptionalTextFilter Creator;
|
||||
public OptionalTextFilter Artist;
|
||||
|
||||
public string[] SearchTerms = Array.Empty<string>();
|
||||
|
||||
@ -53,7 +55,7 @@ namespace osu.Game.Screens.Select
|
||||
if (comparison < 0)
|
||||
return false;
|
||||
|
||||
if (comparison == 0 && !IsInclusive)
|
||||
if (comparison == 0 && !IsLowerInclusive)
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -64,7 +66,7 @@ namespace osu.Game.Screens.Select
|
||||
if (comparison > 0)
|
||||
return false;
|
||||
|
||||
if (comparison == 0 && !IsInclusive)
|
||||
if (comparison == 0 && !IsUpperInclusive)
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -73,12 +75,33 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public T? Min;
|
||||
public T? Max;
|
||||
public bool IsInclusive;
|
||||
public bool IsLowerInclusive;
|
||||
public bool IsUpperInclusive;
|
||||
|
||||
public bool Equals(OptionalRange<T> other)
|
||||
=> Min.Equals(other.Min)
|
||||
&& Max.Equals(other.Max)
|
||||
&& IsInclusive.Equals(other.IsInclusive);
|
||||
&& IsLowerInclusive.Equals(other.IsLowerInclusive)
|
||||
&& IsUpperInclusive.Equals(other.IsUpperInclusive);
|
||||
}
|
||||
|
||||
public struct OptionalTextFilter : IEquatable<OptionalTextFilter>
|
||||
{
|
||||
public bool Matches(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchTerm))
|
||||
return true;
|
||||
|
||||
// search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
public string SearchTerm;
|
||||
|
||||
public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
211
osu.Game/Screens/Select/FilterQueryParser.cs
Normal file
211
osu.Game/Screens/Select/FilterQueryParser.cs
Normal file
@ -0,0 +1,211 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
internal static class FilterQueryParser
|
||||
{
|
||||
private static readonly Regex query_syntax_regex = new Regex(
|
||||
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, 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;
|
||||
|
||||
parseKeywordCriteria(criteria, key, value, op);
|
||||
|
||||
query = query.Replace(match.ToString(), "");
|
||||
}
|
||||
|
||||
criteria.SearchText = query;
|
||||
}
|
||||
|
||||
private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "stars" when parseFloatWithPoint(value, out var stars):
|
||||
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
|
||||
break;
|
||||
|
||||
case "ar" when parseFloatWithPoint(value, out var ar):
|
||||
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
|
||||
break;
|
||||
|
||||
case "dr" when parseFloatWithPoint(value, out var dr):
|
||||
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
|
||||
break;
|
||||
|
||||
case "cs" when parseFloatWithPoint(value, out var cs):
|
||||
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
|
||||
break;
|
||||
|
||||
case "bpm" when parseDoubleWithPoint(value, out var bpm):
|
||||
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
|
||||
break;
|
||||
|
||||
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
|
||||
var scale = getLengthScale(value);
|
||||
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
break;
|
||||
|
||||
case "divisor" when parseInt(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;
|
||||
|
||||
case "creator":
|
||||
updateCriteriaText(ref criteria.Creator, op, value);
|
||||
break;
|
||||
|
||||
case "artist":
|
||||
updateCriteriaText(ref criteria.Artist, op, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getLengthScale(string value) =>
|
||||
value.EndsWith("ms") ? 1 :
|
||||
value.EndsWith("s") ? 1000 :
|
||||
value.EndsWith("m") ? 60000 :
|
||||
value.EndsWith("h") ? 3600000 : 1000;
|
||||
|
||||
private static bool parseFloatWithPoint(string value, out float result) =>
|
||||
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseDoubleWithPoint(string value, out double result) =>
|
||||
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseInt(string value, out int result) =>
|
||||
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
textFilter.SearchTerm = value.Trim('"');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
|
||||
where T : struct, IComparable
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
range.Min = value;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
range.IsLowerInclusive = false;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
range.IsLowerInclusive = true;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
range.IsUpperInclusive = false;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
range.IsUpperInclusive = true;
|
||||
range.Max = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,6 @@
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -123,7 +123,7 @@
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.813.0" ExcludeAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user