Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recommended difficulty numerical value near filter in beatmap listing #31101

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@
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;
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
{
Expand Down Expand Up @@ -63,7 +68,7 @@ decimal getNecessaryPP(int? rulesetID)
return 336; // recommended star rating of 2

case 1:
return 928; // SR 3
return 973; // SR 3

case 2:
return 1905; // SR 4
Expand Down Expand Up @@ -170,6 +175,45 @@ public void TestCorrectStarRatingIsUsed()
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();
Expand Down
3 changes: 2 additions & 1 deletion osu.Game.Tournament/Components/SongBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;

Expand Down Expand Up @@ -207,7 +208,7 @@ private void refreshContent()
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
Expand Down
17 changes: 10 additions & 7 deletions osu.Game/Beatmaps/DifficultyRecommender.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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!;
Expand Down Expand Up @@ -83,8 +82,13 @@ private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
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>
Expand All @@ -93,15 +97,14 @@ private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
/// </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
Expand Down
4 changes: 2 additions & 2 deletions osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand All @@ -14,6 +13,7 @@
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;

Expand Down Expand Up @@ -156,7 +156,7 @@ protected override void LoadComplete()

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);

Expand Down
19 changes: 12 additions & 7 deletions osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -29,7 +27,7 @@ public partial class BeatmapListingSearchControl : CompositeDrawable
/// <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;

Expand All @@ -51,7 +49,7 @@ public partial class BeatmapListingSearchControl : CompositeDrawable

public Bindable<SearchExplicit> ExplicitContent => explicitContentFilter.Current;

public APIBeatmapSet BeatmapSet
public APIBeatmapSet? BeatmapSet
{
set
{
Expand All @@ -67,7 +65,7 @@ public APIBeatmapSet BeatmapSet
}

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;
Expand Down Expand Up @@ -151,7 +149,7 @@ public BeatmapListingSearchControl()
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}

private IBindable<bool> allowExplicitContent;
private IBindable<bool> allowExplicitContent = null!;

[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
Expand All @@ -165,14 +163,21 @@ private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
}, true);
}

protected override void LoadComplete()
{
base.LoadComplete();

generalFilter.Ruleset.BindTo(Ruleset);
}

public void TakeFocus() => textBox.TakeFocus();

private partial class BeatmapSearchTextBox : BasicSearchTextBox
{
/// <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;

Expand Down
97 changes: 86 additions & 11 deletions osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs
Original file line number Diff line number Diff line change
@@ -1,60 +1,135 @@
// 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;

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)
{
}

[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()
{
Expand Down
Loading
Loading