diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 76a8ee9914..f68ed4154b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -54,6 +54,35 @@ private void load(RulesetStore rulesets)
this.rulesets = rulesets;
}
+ [Test]
+ public void TestRecommendedSelection()
+ {
+ loadBeatmaps();
+
+ AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
+
+ // check recommended was selected
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(1, 3);
+
+ // change away from recommended
+ advanceSelection(direction: -1, diff: true);
+ waitForSelection(1, 2);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(2, 3);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(3, 3);
+
+ // go back to first set and ensure user selection was retained
+ advanceSelection(direction: -1, diff: false);
+ advanceSelection(direction: -1, diff: false);
+ waitForSelection(1, 2);
+ }
+
///
/// Test keyboard traversal
///
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 59dddc2baa..a8225ba1ec 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -48,6 +48,11 @@ public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler
public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet;
+ ///
+ /// A function to optionally decide on a recommended difficulty from a beatmap set.
+ ///
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
private CarouselBeatmapSet selectedBeatmapSet;
///
@@ -116,6 +121,7 @@ private void loadBeatmapSets(IEnumerable beatmapSets)
private readonly Stack randomSelectedBeatmaps = new Stack();
protected List Items = new List();
+
private CarouselRoot root;
public BeatmapCarousel()
@@ -579,7 +585,10 @@ private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet)
b.Metadata = beatmapSet.Metadata;
}
- var set = new CarouselBeatmapSet(beatmapSet);
+ var set = new CarouselBeatmapSet(beatmapSet)
+ {
+ GetRecommendedBeatmap = beatmaps => GetRecommendedBeatmap?.Invoke(beatmaps)
+ };
foreach (var c in set.Beatmaps)
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 8e323c66e2..92ccfde14b 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -16,6 +16,8 @@ public class CarouselBeatmapSet : CarouselGroupEagerSelect
public BeatmapSetInfo BeatmapSet;
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
{
BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet));
@@ -28,6 +30,17 @@ public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
+ protected override CarouselItem GetNextToSelect()
+ {
+ if (LastSelected == null)
+ {
+ if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended)
+ return Children.OfType().First(b => b.Beatmap == recommended);
+ }
+
+ return base.GetNextToSelect();
+ }
+
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
{
if (!(other is CarouselBeatmapSet otherSet))
diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
index 6ce12f7b89..262bea9c71 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
@@ -90,11 +90,15 @@ private void attemptSelection()
PerformSelection();
}
+ protected virtual CarouselItem GetNextToSelect()
+ {
+ return Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
+ Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ }
+
protected virtual void PerformSelection()
{
- CarouselItem nextToSelect =
- Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
- Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ CarouselItem nextToSelect = GetNextToSelect();
if (nextToSelect != null)
nextToSelect.State.Value = CarouselItemState.Selected;
diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs
new file mode 100644
index 0000000000..20cdca858a
--- /dev/null
+++ b/osu.Game/Screens/Select/DifficultyRecommender.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Screens.Select
+{
+ public class DifficultyRecommender : Component, IOnlineComponent
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [Resolved]
+ private Bindable ruleset { get; set; }
+
+ private readonly Dictionary recommendedStarDifficulty = new Dictionary();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ api.Register(this);
+ }
+
+ ///
+ /// Find the recommended difficulty from a selection of available difficulties for the current local user.
+ ///
+ ///
+ /// This requires the user to be online for now.
+ ///
+ /// A collection of beatmaps to select a difficulty from.
+ /// The recommended difficulty, or null if a recommendation could not be provided.
+ public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps)
+ {
+ if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars))
+ {
+ return beatmaps.OrderBy(b =>
+ {
+ var difference = b.StarDifficulty - stars;
+ return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
+ }).FirstOrDefault();
+ }
+
+ return null;
+ }
+
+ private void calculateRecommendedDifficulties()
+ {
+ rulesets.AvailableRulesets.ForEach(rulesetInfo =>
+ {
+ var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
+
+ req.Success += result =>
+ {
+ // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
+ recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
+ };
+
+ api.Queue(req);
+ });
+ }
+
+ public void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ switch (state)
+ {
+ case APIState.Online:
+ calculateRecommendedDifficulties();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ api.Unregister(this);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 895a8ad0c9..f164056ede 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -81,6 +81,8 @@ public abstract class SongSelect : OsuScreen, IKeyBindingHandler
protected BeatmapCarousel Carousel { get; private set; }
+ private DifficultyRecommender recommender;
+
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@@ -109,6 +111,7 @@ private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, S
AddRangeInternal(new Drawable[]
{
+ recommender = new DifficultyRecommender(),
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,
@@ -156,6 +159,7 @@ private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, S
RelativeSizeAxes = Axes.Both,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
+ GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
},
}
},