From ad35dad46dc0b874ec049b6f195c25a0a2b9fcd3 Mon Sep 17 00:00:00 2001 From: Dani211e Date: Fri, 16 May 2025 20:41:42 +0200 Subject: [PATCH 01/12] Add sorting dropdown Only use sorting when on local scope otherwise hide --- osu.Game/Configuration/OsuConfigManager.cs | 3 + .../Online/Leaderboards/LeaderboardManager.cs | 5 +- osu.Game/Scoring/ScoreInfoExtensions.cs | 30 +++++++ .../Select/Leaderboards/RankingsSort.cs | 14 ++++ .../Screens/SelectV2/BeatmapDetailsArea.cs | 1 + .../SelectV2/BeatmapDetailsArea_Header.cs | 81 +++++++++---------- .../SelectV2/BeatmapLeaderboardWedge.cs | 6 +- 7 files changed, 95 insertions(+), 45 deletions(-) create mode 100644 osu.Game/Screens/Select/Leaderboards/RankingsSort.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index df3e7d88af..af079003a0 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Skinning; using osu.Game.Users; @@ -41,6 +42,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString()); SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local); + SetDefault(OsuSetting.BeatmapRankingsSort, RankingsSort.Score); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); SetDefault(OsuSetting.ShowConvertedBeatmaps, true); @@ -382,6 +384,7 @@ namespace osu.Game.Configuration MenuParallax, Prefer24HourTime, BeatmapDetailTab, + BeatmapRankingsSort, BeatmapDetailModsFilter, Username, ReleaseStream, diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index d5d1672e1b..e984b610b8 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -180,7 +180,7 @@ namespace osu.Game.Online.Leaderboards } } - newScores = newScores.Detach().OrderByTotalScore(); + newScores = newScores.Detach().OrderByCriteria(CurrentCriteria.Sorting); var newScoresArray = newScores.ToArray(); scores.Value = LeaderboardScores.Success(newScoresArray, newScoresArray.Length, null); @@ -191,7 +191,8 @@ namespace osu.Game.Online.Leaderboards BeatmapInfo? Beatmap, RulesetInfo? Ruleset, BeatmapLeaderboardScope Scope, - Mod[]? ExactMods + Mod[]? ExactMods, + RankingsSort Sorting = RankingsSort.Score ); public record LeaderboardScores diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 6e57a9fd0b..6cfd139b26 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Scoring { @@ -26,6 +27,35 @@ namespace osu.Game.Scoring // Local scores may not have an online ID. Fall back to date in these cases. .ThenBy(s => s.Date); + /// + /// Orders an array of s by the selected . + /// + /// The array of s to reorder. + /// The attribute to sort the scores by. + /// The given ordered by the selected mode. + public static IEnumerable OrderByCriteria(this IEnumerable scores, RankingsSort rankingSort) + { + switch (rankingSort) + { + case RankingsSort.Score: + return scores.OrderByDescending(s => s.TotalScore); + + case RankingsSort.Accuracy: + return scores.OrderByDescending(s => s.Accuracy).ThenByDescending(s => s.TotalScore); + + case RankingsSort.Combo: + return scores.OrderByDescending(s => s.MaxCombo).ThenByDescending(s => s.TotalScore); + + case RankingsSort.Misses: + return scores.OrderBy(s => s.Statistics.GetValueOrDefault(HitResult.Miss, 0)).ThenByDescending(s => s.TotalScore); + + case RankingsSort.Date: + return scores.OrderByDescending(s => s.Date); + + default: return scores; + } + } + /// /// Retrieves the maximum achievable combo for the provided score. /// diff --git a/osu.Game/Screens/Select/Leaderboards/RankingsSort.cs b/osu.Game/Screens/Select/Leaderboards/RankingsSort.cs new file mode 100644 index 0000000000..b1ec81e452 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/RankingsSort.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Select.Leaderboards +{ + public enum RankingsSort + { + Score, + Accuracy, + Combo, + Misses, + Date, + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs index 99e3155a7a..85bbf34837 100644 --- a/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs @@ -87,6 +87,7 @@ namespace osu.Game.Screens.SelectV2 currentContent = new BeatmapLeaderboardWedge { Scope = { BindTarget = header.Scope }, + Sorting = { BindTarget = header.Sorting }, FilterBySelectedMods = { BindTarget = header.FilterBySelectedMods }, }; diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs index 76734e110f..d1aeb89a2c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.SelectV2 private FillFlowContainer leaderboardControls = null!; private ShearedDropdown scopeDropdown = null!; + private ShearedDropdown sortDropdown = null!; private ShearedToggleButton selectedModsToggle = null!; public IBindable Type => tabControl.Current; @@ -32,6 +33,10 @@ namespace osu.Game.Screens.SelectV2 private readonly Bindable configDetailTab = new Bindable(); + public IBindable Sorting => sortDropdown.Current; + + private readonly Bindable configRankingsSort = new Bindable(); + public IBindable FilterBySelectedMods => selectedModsToggle.Active; [BackgroundDependencyLoader] @@ -58,52 +63,44 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + Height = 30, Spacing = new Vector2(5f, 0f), + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 125, Right = 133 }, Children = new Drawable[] { - new Container + scopeDropdown = new ScopeDropdown { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(128f, 30f), - Child = selectedModsToggle = new ShearedToggleButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = @"Selected Mods", - Height = 30, - }, + RelativeSizeAxes = Axes.X, + Current = { Value = BeatmapLeaderboardScope.Global }, }, - // new Container - // { - // Anchor = Anchor.CentreRight, - // Origin = Anchor.CentreRight, - // Size = new Vector2(150f, 33f), - // Child = new ShearedDropdown(@"Sort") - // { - // Width = 150f, - // Items = Enum.GetValues(), - // }, - // }, - new Container + sortDropdown = new ShearedDropdown("Sort") { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(160f, 32f), - Child = scopeDropdown = new ScopeDropdown - { - Width = 160f, - Current = { Value = BeatmapLeaderboardScope.Global }, - }, + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(), }, }, }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(128f, 30f), + Child = selectedModsToggle = new ShearedToggleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = @"Selected Mods", + Height = 30, + }, + }, }, }, }; config.BindWith(OsuSetting.BeatmapDetailTab, configDetailTab); + config.BindWith(OsuSetting.BeatmapRankingsSort, configRankingsSort); config.BindWith(OsuSetting.BeatmapDetailModsFilter, selectedModsToggle.Active); } @@ -114,12 +111,23 @@ namespace osu.Game.Screens.SelectV2 scopeDropdown.Current.Value = tryMapDetailTabToLeaderboardScope(configDetailTab.Value) ?? scopeDropdown.Current.Value; scopeDropdown.Current.BindValueChanged(_ => updateConfigDetailTab()); + sortDropdown.Current.Value = configRankingsSort.Value; + sortDropdown.Current.BindValueChanged(v => configRankingsSort.Value = v.NewValue); + tabControl.Current.Value = configDetailTab.Value == BeatmapDetailTab.Details ? Selection.Details : Selection.Ranking; tabControl.Current.BindValueChanged(v => { leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint); updateConfigDetailTab(); }, true); + + scopeDropdown.Current.BindValueChanged(v => + { + bool isLocal = v.NewValue == BeatmapLeaderboardScope.Local; + scopeDropdown.ResizeWidthTo(isLocal ? 0.5f : 1, 300, Easing.OutQuint); + sortDropdown.ResizeWidthTo(isLocal ? 0.5f : 0, 300, Easing.OutQuint); + sortDropdown.FadeTo(isLocal ? 1 : 0, 300, Easing.OutQuint); + }, true); } #region Reading / writing state from / to configuration @@ -197,15 +205,6 @@ namespace osu.Game.Screens.SelectV2 Ranking, } - // public enum RankingsSort - // { - // Score, - // Accuracy, - // Combo, - // Misses, - // Date, - // } - private partial class ScopeDropdown : ShearedDropdown { public ScopeDropdown() diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 10917f08ac..901c194296 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.SelectV2 public IBindable Scope { get; } = new Bindable(); + public IBindable Sorting { get; } = new Bindable(); + public IBindable FilterBySelectedMods { get; } = new BindableBool(); [Resolved] @@ -171,6 +173,7 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Scope.BindValueChanged(_ => refetchScores()); + Sorting.BindValueChanged(_ => refetchScores()); FilterBySelectedMods.BindValueChanged(_ => refetchScores()); beatmap.BindValueChanged(_ => refetchScores()); ruleset.BindValueChanged(_ => refetchScores()); @@ -220,8 +223,7 @@ namespace osu.Game.Screens.SelectV2 // For now, we forcefully refresh to keep things simple. // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios // (like returning from gameplay after setting a new score, returning to song select after main menu). - leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), - forceRefresh: true); + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, Sorting.Value), forceRefresh: true); if (!initialFetchComplete) { From d04094a2c4a8ac3ce49efcac210858bebeac636a Mon Sep 17 00:00:00 2001 From: Dani211e Date: Tue, 10 Jun 2025 07:41:31 +0200 Subject: [PATCH 02/12] Persist sorting between results screen and song select --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 56d175420f..0be44c4397 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -45,7 +45,8 @@ namespace osu.Game.Screens.Ranking Score.BeatmapInfo!, Score.Ruleset, leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global, - leaderboardManager.CurrentCriteria?.ExactMods + leaderboardManager.CurrentCriteria?.ExactMods, + leaderboardManager.CurrentCriteria?.Sorting ?? RankingsSort.Score ); var requestTaskSource = new TaskCompletionSource(); globalScores.BindValueChanged(_ => From 3e615d4192c30440ffb4e863dad320fbb037a861 Mon Sep 17 00:00:00 2001 From: Dani211e Date: Sat, 17 May 2025 00:50:00 +0200 Subject: [PATCH 03/12] Add basic test scene --- .../TestSceneBeatmapLeaderboardSorting.cs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs new file mode 100644 index 0000000000..74e33e2659 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs @@ -0,0 +1,151 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapLeaderboardSorting : SongSelectComponentsTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private BeatmapDetailsArea beatmapDetailsArea = null!; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; + private OsuContextMenuContainer contentContainer = null!; + private DialogOverlay dialogOverlay = null!; + + private LeaderboardManager leaderboardManager = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + + Dependencies.Cache(Realm); + + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(dialogOverlay = new DialogOverlay + { + Depth = -1 + }); + + LoadComponent(leaderboardManager); + + Child = contentContainer = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + Height = 500, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + dialogOverlay, + } + }; + } + + [SetUp] + public void SetUp() => Schedule(() => + { + if (beatmapDetailsArea.IsNotNull()) + contentContainer.Remove(beatmapDetailsArea, false); + + contentContainer.Add(beatmapDetailsArea = new BeatmapDetailsArea + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 50 }, + State = { Value = Visibility.Visible }, + }); + }); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + } + + [Test] + public void TestLocalScoresSorting() + { + BeatmapInfo beatmapInfo = null!; + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + AddStep(@"Import random scores", () => + { + for (int i = 0; i < 10; ++i) + importRandomScore(beatmapInfo); + }); + + AddStep("Clear all scores", () => scoreManager.Delete()); + } + + private void importRandomScore(BeatmapInfo beatmapInfo) + { + scoreManager.Import(new ScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = RNG.NextDouble(0, 1), + MaxCombo = RNG.Next(0, 1500), + TotalScore = RNG.Next(500000, 1200000), + Date = DateTime.Now.AddMinutes(RNG.Next(0, 1000) * -1), + Statistics = new Dictionary + { + { HitResult.Miss, RNG.Next(0, 25) }, + }, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + User = new APIUser + { + Id = 2, + Username = @"peppy", + CountryCode = CountryCode.JP, + }, + }); + } + } +} From c1a34d8c6a179c8ff410e1604ce8916baa88ba9a Mon Sep 17 00:00:00 2001 From: Dani211e Date: Thu, 26 Jun 2025 19:03:49 +0200 Subject: [PATCH 04/12] RankingsSort -> LeaderboardSortMode --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- .../Online/Leaderboards/LeaderboardManager.cs | 2 +- osu.Game/Scoring/ScoreInfoExtensions.cs | 18 +++++++++--------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- ...{RankingsSort.cs => LeaderboardSortMode.cs} | 2 +- .../SelectV2/BeatmapDetailsArea_Header.cs | 16 ++++++++-------- .../SelectV2/BeatmapLeaderboardWedge.cs | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) rename osu.Game/Screens/Select/Leaderboards/{RankingsSort.cs => LeaderboardSortMode.cs} (88%) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index af079003a0..062ea6b306 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -42,7 +42,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString()); SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local); - SetDefault(OsuSetting.BeatmapRankingsSort, RankingsSort.Score); + SetDefault(OsuSetting.BeatmapLeaderboardSortMode, LeaderboardSortMode.Score); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); SetDefault(OsuSetting.ShowConvertedBeatmaps, true); @@ -384,7 +384,7 @@ namespace osu.Game.Configuration MenuParallax, Prefer24HourTime, BeatmapDetailTab, - BeatmapRankingsSort, + BeatmapLeaderboardSortMode, BeatmapDetailModsFilter, Username, ReleaseStream, diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index e984b610b8..6a4ebde62d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -192,7 +192,7 @@ namespace osu.Game.Online.Leaderboards RulesetInfo? Ruleset, BeatmapLeaderboardScope Scope, Mod[]? ExactMods, - RankingsSort Sorting = RankingsSort.Score + LeaderboardSortMode Sorting = LeaderboardSortMode.Score ); public record LeaderboardScores diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 6cfd139b26..0554dc31e3 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -28,28 +28,28 @@ namespace osu.Game.Scoring .ThenBy(s => s.Date); /// - /// Orders an array of s by the selected . + /// Orders an array of s by the selected . /// /// The array of s to reorder. - /// The attribute to sort the scores by. + /// The attribute to sort the scores by. /// The given ordered by the selected mode. - public static IEnumerable OrderByCriteria(this IEnumerable scores, RankingsSort rankingSort) + public static IEnumerable OrderByCriteria(this IEnumerable scores, LeaderboardSortMode leaderboardSortMode) { - switch (rankingSort) + switch (leaderboardSortMode) { - case RankingsSort.Score: + case LeaderboardSortMode.Score: return scores.OrderByDescending(s => s.TotalScore); - case RankingsSort.Accuracy: + case LeaderboardSortMode.Accuracy: return scores.OrderByDescending(s => s.Accuracy).ThenByDescending(s => s.TotalScore); - case RankingsSort.Combo: + case LeaderboardSortMode.Combo: return scores.OrderByDescending(s => s.MaxCombo).ThenByDescending(s => s.TotalScore); - case RankingsSort.Misses: + case LeaderboardSortMode.Misses: return scores.OrderBy(s => s.Statistics.GetValueOrDefault(HitResult.Miss, 0)).ThenByDescending(s => s.TotalScore); - case RankingsSort.Date: + case LeaderboardSortMode.Date: return scores.OrderByDescending(s => s.Date); default: return scores; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 0be44c4397..2d772e5f09 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Ranking Score.Ruleset, leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global, leaderboardManager.CurrentCriteria?.ExactMods, - leaderboardManager.CurrentCriteria?.Sorting ?? RankingsSort.Score + leaderboardManager.CurrentCriteria?.Sorting ?? LeaderboardSortMode.Score ); var requestTaskSource = new TaskCompletionSource(); globalScores.BindValueChanged(_ => diff --git a/osu.Game/Screens/Select/Leaderboards/RankingsSort.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs similarity index 88% rename from osu.Game/Screens/Select/Leaderboards/RankingsSort.cs rename to osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs index b1ec81e452..1af34a7ceb 100644 --- a/osu.Game/Screens/Select/Leaderboards/RankingsSort.cs +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs @@ -3,7 +3,7 @@ namespace osu.Game.Screens.Select.Leaderboards { - public enum RankingsSort + public enum LeaderboardSortMode { Score, Accuracy, diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs index d1aeb89a2c..e3e8e73b06 100644 --- a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.SelectV2 private FillFlowContainer leaderboardControls = null!; private ShearedDropdown scopeDropdown = null!; - private ShearedDropdown sortDropdown = null!; + private ShearedDropdown sortDropdown = null!; private ShearedToggleButton selectedModsToggle = null!; public IBindable Type => tabControl.Current; @@ -33,9 +33,9 @@ namespace osu.Game.Screens.SelectV2 private readonly Bindable configDetailTab = new Bindable(); - public IBindable Sorting => sortDropdown.Current; + public IBindable Sorting => sortDropdown.Current; - private readonly Bindable configRankingsSort = new Bindable(); + private readonly Bindable configLeaderboardSortMode = new Bindable(); public IBindable FilterBySelectedMods => selectedModsToggle.Active; @@ -75,10 +75,10 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X, Current = { Value = BeatmapLeaderboardScope.Global }, }, - sortDropdown = new ShearedDropdown("Sort") + sortDropdown = new ShearedDropdown("Sort") { RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(), + Items = Enum.GetValues(), }, }, }, @@ -100,7 +100,7 @@ namespace osu.Game.Screens.SelectV2 }; config.BindWith(OsuSetting.BeatmapDetailTab, configDetailTab); - config.BindWith(OsuSetting.BeatmapRankingsSort, configRankingsSort); + config.BindWith(OsuSetting.BeatmapLeaderboardSortMode, configLeaderboardSortMode); config.BindWith(OsuSetting.BeatmapDetailModsFilter, selectedModsToggle.Active); } @@ -111,8 +111,8 @@ namespace osu.Game.Screens.SelectV2 scopeDropdown.Current.Value = tryMapDetailTabToLeaderboardScope(configDetailTab.Value) ?? scopeDropdown.Current.Value; scopeDropdown.Current.BindValueChanged(_ => updateConfigDetailTab()); - sortDropdown.Current.Value = configRankingsSort.Value; - sortDropdown.Current.BindValueChanged(v => configRankingsSort.Value = v.NewValue); + sortDropdown.Current.Value = configLeaderboardSortMode.Value; + sortDropdown.Current.BindValueChanged(v => configLeaderboardSortMode.Value = v.NewValue); tabControl.Current.Value = configDetailTab.Value == BeatmapDetailTab.Details ? Selection.Details : Selection.Ranking; tabControl.Current.BindValueChanged(v => diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 901c194296..a0a5b38c39 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.SelectV2 public IBindable Scope { get; } = new Bindable(); - public IBindable Sorting { get; } = new Bindable(); + public IBindable Sorting { get; } = new Bindable(); public IBindable FilterBySelectedMods { get; } = new BindableBool(); From 6e73a9299ecda190f6d2cd17d099ebf2825e75ff Mon Sep 17 00:00:00 2001 From: Dani211e Date: Fri, 27 Jun 2025 04:28:44 +0200 Subject: [PATCH 05/12] Throw ArgumentOutOfRangeException on default path --- osu.Game/Scoring/ScoreInfoExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 0554dc31e3..13a5594cf8 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -1,6 +1,7 @@ // 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.Game.Beatmaps; @@ -52,7 +53,7 @@ namespace osu.Game.Scoring case LeaderboardSortMode.Date: return scores.OrderByDescending(s => s.Date); - default: return scores; + default: throw new ArgumentOutOfRangeException(); } } From abfd4f6338669521f5fd0bf7c2dc40a0cd3a0260 Mon Sep 17 00:00:00 2001 From: Dani211e Date: Fri, 27 Jun 2025 04:46:49 +0200 Subject: [PATCH 06/12] Make sure you can't request non-local scores with a sort mode other than score. --- osu.Game/Online/Leaderboards/LeaderboardManager.cs | 3 +++ osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 6a4ebde62d..83d974a8e7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -106,6 +106,9 @@ namespace osu.Game.Online.Leaderboards return; } + if (newCriteria.Sorting != LeaderboardSortMode.Score) + throw new InvalidOperationException("Should not attempt to request online scores with a sort mode other than score"); + IReadOnlyList? requestMods = null; if (newCriteria.ExactMods != null) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index a0a5b38c39..09667cc50f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -219,11 +219,12 @@ namespace osu.Game.Screens.SelectV2 { var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + var fetchSorting = Scope.Value == BeatmapLeaderboardScope.Local ? Sorting.Value : LeaderboardSortMode.Score; // For now, we forcefully refresh to keep things simple. // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios // (like returning from gameplay after setting a new score, returning to song select after main menu). - leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, Sorting.Value), forceRefresh: true); + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, fetchSorting), forceRefresh: true); if (!initialFetchComplete) { From 99f9a3b1f4478bfa7fd77758f152ba6a505962c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Jul 2025 11:33:59 +0200 Subject: [PATCH 07/12] Move exception in better place (and also throw it better) --- osu.Game/Online/Leaderboards/LeaderboardManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 83d974a8e7..88cc9d5db5 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -76,6 +76,9 @@ namespace osu.Game.Online.Leaderboards default: { + if (newCriteria.Sorting != LeaderboardSortMode.Score) + throw new NotSupportedException($@"Requesting online scores with a {nameof(LeaderboardSortMode)} other than {nameof(LeaderboardSortMode.Score)} is not supported"); + if (!api.IsLoggedIn) { scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn); @@ -106,9 +109,6 @@ namespace osu.Game.Online.Leaderboards return; } - if (newCriteria.Sorting != LeaderboardSortMode.Score) - throw new InvalidOperationException("Should not attempt to request online scores with a sort mode other than score"); - IReadOnlyList? requestMods = null; if (newCriteria.ExactMods != null) From f489ffdfd722cc4b91087d6e7998094264fd91a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Jul 2025 11:38:17 +0200 Subject: [PATCH 08/12] Rename `{-> Max}Combo` sort mode I have a feeling this is going to save asses in an indeterminate future. --- osu.Game/Scoring/ScoreInfoExtensions.cs | 2 +- .../Screens/Select/Leaderboards/LeaderboardSortMode.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 13a5594cf8..1065510f42 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -44,7 +44,7 @@ namespace osu.Game.Scoring case LeaderboardSortMode.Accuracy: return scores.OrderByDescending(s => s.Accuracy).ThenByDescending(s => s.TotalScore); - case LeaderboardSortMode.Combo: + case LeaderboardSortMode.MaxCombo: return scores.OrderByDescending(s => s.MaxCombo).ThenByDescending(s => s.TotalScore); case LeaderboardSortMode.Misses: diff --git a/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs index 1af34a7ceb..edf38fa8cc 100644 --- a/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardSortMode.cs @@ -1,13 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Screens.Select.Leaderboards { public enum LeaderboardSortMode { Score, Accuracy, - Combo, + + [Description("Max Combo")] + MaxCombo, + Misses, Date, } From 2ffb5cbec5f66b47e73eaf93ff9579263879eed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Jul 2025 11:38:50 +0200 Subject: [PATCH 09/12] Throw another exception better --- osu.Game/Scoring/ScoreInfoExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 1065510f42..dd08326742 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -53,7 +53,8 @@ namespace osu.Game.Scoring case LeaderboardSortMode.Date: return scores.OrderByDescending(s => s.Date); - default: throw new ArgumentOutOfRangeException(); + default: + throw new ArgumentOutOfRangeException(nameof(leaderboardSortMode), leaderboardSortMode, null); } } From a80fecffe7d4a336f4e1d55ba52c1f1ef558949d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Jul 2025 11:43:15 +0200 Subject: [PATCH 10/12] Add a comment about a sneaky part of the leaderboard sorting changes --- osu.Game/Screens/Play/PlayerLoader.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 848b8292d4..b6a765153c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -277,6 +277,12 @@ namespace osu.Game.Screens.Play showStoryboards.BindValueChanged(val => epilepsyWarning?.FadeTo(val.NewValue ? 1 : 0, 250, Easing.OutQuint), true); epilepsyWarning?.FinishTransforms(true); + // this re-fetch has two purposes: + // - is a safety against potential unexpected screen transitions, making sure that the leaderboard + // displayed during gameplay definitely matches the beatmap and ruleset being played + // (as the solo gameplay leaderboard provider uses the global leaderboard manager to populate itself) + // - the sort mode is not specified and defaults to `Score` which is good because gameplay leaderboards only support sorting by score. + // this may change at some point in the future, at which point specifying a sort mode should be considered. leaderboardManager?.FetchWithCriteria(new LeaderboardCriteria( Beatmap.Value.BeatmapInfo, Ruleset.Value, From c59b4f9526e3aab3f6237e1d3df258b81ba23430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Jul 2025 13:15:31 +0200 Subject: [PATCH 11/12] Fix failing test Started failing after master merge. --- .../Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs index 74e33e2659..0f66122bb5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private DialogOverlay dialogOverlay = null!; private LeaderboardManager leaderboardManager = null!; + private RealmPopulatingOnlineLookupSource lookupSource = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -51,6 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.Cache(leaderboardManager = new LeaderboardManager()); + dependencies.Cache(lookupSource = new RealmPopulatingOnlineLookupSource()); Dependencies.Cache(Realm); @@ -66,6 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); LoadComponent(leaderboardManager); + LoadComponent(lookupSource); Child = contentContainer = new OsuContextMenuContainer { From ce05326fe07e071283a0e1a0465b52b63abd106c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jul 2025 22:35:52 +0900 Subject: [PATCH 12/12] Adjust dropdowns to closer match previous size and display --- .../SelectV2/BeatmapDetailsArea_Header.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs index e3e8e73b06..c1d424e7f8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs @@ -70,16 +70,22 @@ namespace osu.Game.Screens.SelectV2 Padding = new MarginPadding { Left = 125, Right = 133 }, Children = new Drawable[] { - scopeDropdown = new ScopeDropdown - { - RelativeSizeAxes = Axes.X, - Current = { Value = BeatmapLeaderboardScope.Global }, - }, sortDropdown = new ShearedDropdown("Sort") { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.X, + Width = 0, Items = Enum.GetValues(), }, + scopeDropdown = new ScopeDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + Current = { Value = BeatmapLeaderboardScope.Global }, + }, }, }, new Container @@ -124,8 +130,7 @@ namespace osu.Game.Screens.SelectV2 scopeDropdown.Current.BindValueChanged(v => { bool isLocal = v.NewValue == BeatmapLeaderboardScope.Local; - scopeDropdown.ResizeWidthTo(isLocal ? 0.5f : 1, 300, Easing.OutQuint); - sortDropdown.ResizeWidthTo(isLocal ? 0.5f : 0, 300, Easing.OutQuint); + sortDropdown.ResizeWidthTo(isLocal ? 0.4f : 0, 300, Easing.OutQuint); sortDropdown.FadeTo(isLocal ? 1 : 0, 300, Easing.OutQuint); }, true); }