1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 11:02:57 +08:00

Merge pull request #31101 from Joehuu/recommended-diff-beatmap-listing

Add recommended difficulty numerical value near filter in beatmap listing
This commit is contained in:
Dan Balasescu 2024-12-13 18:57:16 +09:00 committed by GitHub
commit f84c67babd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 166 additions and 31 deletions

View File

@ -13,9 +13,12 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@ -23,6 +26,8 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
@ -63,7 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return 336; // recommended star rating of 2
case 1:
return 928; // SR 3
return 973; // SR 3
case 2:
return 1905; // SR 4
@ -170,6 +175,45 @@ namespace osu.Game.Tests.Visual.SongSelect
presentAndConfirm(() => maniaSet, 5);
}
[Test]
public void TestBeatmapListingFilter()
{
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
AddStep("open beatmap listing", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.B);
InputManager.ReleaseKey(Key.B);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for load", () => Game.ChildrenOfType<BeatmapListingOverlay>().SingleOrDefault()?.IsLoaded, () => Is.True);
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(1).TriggerClick());
checkRecommendedDifficulty(2);
AddStep("change mode filter to osu!taiko", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(2).TriggerClick());
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!catch", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(3).TriggerClick());
checkRecommendedDifficulty(4);
AddStep("change mode filter to osu!mania", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(4).TriggerClick());
checkRecommendedDifficulty(5);
void checkRecommendedDifficulty(double starRating)
=> AddAssert($"recommended difficulty is {starRating}",
() => Game.ChildrenOfType<BeatmapSearchGeneralFilterRow>().Single().ChildrenOfType<OsuSpriteText>().ElementAt(1).Text.ToString(),
() => Is.EqualTo($"Recommended difficulty ({starRating.FormatStarRating()})"));
}
private BeatmapSetInfo importBeatmapSet(IEnumerable<RulesetInfo> difficultyRulesets)
{
var rulesets = difficultyRulesets.ToArray();

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -207,7 +208,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[]
{
new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}"))
new DiffPiece(("Star Rating", $"{beatmap.StarRating.FormatStarRating()}{srExtra}"))
}
},
new FillFlowContainer

View File

@ -1,12 +1,9 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@ -23,10 +20,12 @@ namespace osu.Game.Beatmaps
/// </summary>
public partial class DifficultyRecommender : Component
{
public event Action? StarRatingUpdated;
private readonly LocalUserStatisticsProvider statisticsProvider;
[Resolved]
private Bindable<RulesetInfo> gameRuleset { get; set; }
private Bindable<RulesetInfo> gameRuleset { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
@ -83,8 +82,13 @@ namespace osu.Game.Beatmaps
ruleset.ShortName == @"taiko"
? Math.Pow((double)(statistics.PP ?? 0), 0.35) * 0.27
: Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
StarRatingUpdated?.Invoke();
}
public double? GetRecommendedStarRatingFor(RulesetInfo ruleset)
=> recommendedDifficultyMapping.TryGetValue(ruleset.ShortName, out double starRating) ? starRating : null;
/// <summary>
/// Find the recommended difficulty from a selection of available difficulties for the current local user.
/// </summary>
@ -93,15 +97,14 @@ namespace osu.Game.Beatmaps
/// </remarks>
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
public BeatmapInfo? GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
foreach (string r in orderedRulesets)
{
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
BeatmapInfo? beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder

View File

@ -5,7 +5,6 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -14,6 +13,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables
displayedStars.BindValueChanged(s =>
{
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.ToLocalisableString("0.00");
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.FormatStarRating();
background.Colour = colours.ForStarDifficulty(s.NewValue);

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -29,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TypingStarted;
public Action? TypingStarted;
public Bindable<string> Query => textBox.Current;
@ -51,7 +49,7 @@ namespace osu.Game.Overlays.BeatmapListing
public Bindable<SearchExplicit> ExplicitContent => explicitContentFilter.Current;
public APIBeatmapSet BeatmapSet
public APIBeatmapSet? BeatmapSet
{
set
{
@ -67,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
private readonly BeatmapSearchTextBox textBox;
private readonly BeatmapSearchMultipleSelectionFilterRow<SearchGeneral> generalFilter;
private readonly BeatmapSearchGeneralFilterRow generalFilter;
private readonly BeatmapSearchRulesetFilterRow modeFilter;
private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter;
private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter;
@ -151,7 +149,7 @@ namespace osu.Game.Overlays.BeatmapListing
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
private IBindable<bool> allowExplicitContent;
private IBindable<bool> allowExplicitContent = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
@ -165,6 +163,13 @@ namespace osu.Game.Overlays.BeatmapListing
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
generalFilter.Ruleset.BindTo(Ruleset);
}
public void TakeFocus() => textBox.TakeFocus();
private partial class BeatmapSearchTextBox : BasicSearchTextBox
@ -172,7 +177,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TextChanged;
public Action? TextChanged;
protected override Color4 SelectionColour => Color4.Gray;

View File

@ -1,18 +1,23 @@
// 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.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK.Graphics;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
@ -20,27 +25,97 @@ namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
public BeatmapSearchGeneralFilterRow()
: base(BeatmapsStrings.ListingSearchFiltersGeneral)
{
}
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter();
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter
{
Ruleset = { BindTarget = Ruleset }
};
private partial class GeneralFilter : MultipleSelectionFilter
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value)
{
if (value == SearchGeneral.FeaturedArtists)
return new FeaturedArtistsTabItem();
switch (value)
{
case SearchGeneral.Recommended:
return new RecommendedDifficultyTabItem
{
Ruleset = { BindTarget = Ruleset }
};
return new MultipleSelectionFilterTabItem(value);
case SearchGeneral.FeaturedArtists:
return new FeaturedArtistsTabItem();
default:
return new MultipleSelectionFilterTabItem(value);
}
}
}
private partial class RecommendedDifficultyTabItem : MultipleSelectionFilterTabItem
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
[Resolved]
private DifficultyRecommender? recommender { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public RecommendedDifficultyTabItem()
: base(SearchGeneral.Recommended)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (recommender != null)
recommender.StarRatingUpdated += updateText;
Ruleset.BindValueChanged(_ => updateText(), true);
}
private void updateText()
{
// fallback to profile default game mode if beatmap listing mode filter is set to Any
// TODO: find a way to update `PlayMode` when the profile default game mode has changed
RulesetInfo? ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode);
if (ruleset == null) return;
double? starRating = recommender?.GetRecommendedStarRatingFor(ruleset);
if (starRating != null)
Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({starRating.Value.FormatStarRating()})");
else
Text.Text = Value.GetLocalisableDescription();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (recommender != null)
recommender.StarRatingUpdated -= updateText;
}
}
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
private Bindable<bool> disclaimerShown;
private Bindable<bool> disclaimerShown = null!;
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
@ -48,13 +123,13 @@ namespace osu.Game.Overlays.BeatmapListing
}
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
[Resolved]
private SessionStatics sessionStatics { get; set; }
private SessionStatics sessionStatics { get; set; } = null!;
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
protected override void LoadComplete()
{

View File

@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet
@ -185,7 +186,7 @@ namespace osu.Game.Overlays.BeatmapSet
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00");
starRating.Text = beatmap.StarRating.FormatStarRating();
starRatingContainer.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },

View File

@ -226,7 +226,7 @@ namespace osu.Game.Skinning.Components
return computeDifficulty().ApproachRate.ToLocalisableString(@"0.##");
case BeatmapAttribute.StarRating:
return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2");
return (starDifficulty?.Stars ?? 0).FormatStarRating();
case BeatmapAttribute.MaxPP:
return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString();

View File

@ -32,6 +32,12 @@ namespace osu.Game.Utils
/// <param name="rank">The rank/position to be formatted.</param>
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
/// <summary>
/// Formats the supplied star rating in a consistent, simplified way.
/// </summary>
/// <param name="starRating">The star rating to be formatted.</param>
public static LocalisableString FormatStarRating(this double starRating) => starRating.ToLocalisableString("0.00");
/// <summary>
/// Finds the number of digits after the decimal.
/// </summary>