mirror of
https://github.com/ppy/osu.git
synced 2026-05-23 14:22:08 +08:00
Merge pull request #33871 from dani211e/local-score-sorting-v2
SSv2: Add ability to sort local scores by metrics other than total score
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// 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.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!;
|
||||
private RealmPopulatingOnlineLookupSource lookupSource = 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<AudioManager>(), Resources, dependencies.Get<GameHost>(), 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);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LoadComponent(dialogOverlay = new DialogOverlay
|
||||
{
|
||||
Depth = -1
|
||||
});
|
||||
|
||||
LoadComponent(leaderboardManager);
|
||||
LoadComponent(lookupSource);
|
||||
|
||||
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, int>
|
||||
{
|
||||
{ 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.BeatmapLeaderboardSortMode, LeaderboardSortMode.Score);
|
||||
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
||||
|
||||
SetDefault(OsuSetting.ShowConvertedBeatmaps, true);
|
||||
@@ -382,6 +384,7 @@ namespace osu.Game.Configuration
|
||||
MenuParallax,
|
||||
Prefer24HourTime,
|
||||
BeatmapDetailTab,
|
||||
BeatmapLeaderboardSortMode,
|
||||
BeatmapDetailModsFilter,
|
||||
Username,
|
||||
ReleaseStream,
|
||||
|
||||
@@ -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);
|
||||
@@ -180,7 +183,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 +194,8 @@ namespace osu.Game.Online.Leaderboards
|
||||
BeatmapInfo? Beatmap,
|
||||
RulesetInfo? Ruleset,
|
||||
BeatmapLeaderboardScope Scope,
|
||||
Mod[]? ExactMods
|
||||
Mod[]? ExactMods,
|
||||
LeaderboardSortMode Sorting = LeaderboardSortMode.Score
|
||||
);
|
||||
|
||||
public record LeaderboardScores
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// 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.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 +28,36 @@ namespace osu.Game.Scoring
|
||||
// Local scores may not have an online ID. Fall back to date in these cases.
|
||||
.ThenBy(s => s.Date);
|
||||
|
||||
/// <summary>
|
||||
/// Orders an array of <see cref="ScoreInfo"/>s by the selected <see cref="LeaderboardSortMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
|
||||
/// <param name="leaderboardSortMode">The attribute to sort the scores by.</param>
|
||||
/// <returns>The given <paramref name="scores"/> ordered by the selected mode.</returns>
|
||||
public static IEnumerable<ScoreInfo> OrderByCriteria(this IEnumerable<ScoreInfo> scores, LeaderboardSortMode leaderboardSortMode)
|
||||
{
|
||||
switch (leaderboardSortMode)
|
||||
{
|
||||
case LeaderboardSortMode.Score:
|
||||
return scores.OrderByDescending(s => s.TotalScore);
|
||||
|
||||
case LeaderboardSortMode.Accuracy:
|
||||
return scores.OrderByDescending(s => s.Accuracy).ThenByDescending(s => s.TotalScore);
|
||||
|
||||
case LeaderboardSortMode.MaxCombo:
|
||||
return scores.OrderByDescending(s => s.MaxCombo).ThenByDescending(s => s.TotalScore);
|
||||
|
||||
case LeaderboardSortMode.Misses:
|
||||
return scores.OrderBy(s => s.Statistics.GetValueOrDefault(HitResult.Miss, 0)).ThenByDescending(s => s.TotalScore);
|
||||
|
||||
case LeaderboardSortMode.Date:
|
||||
return scores.OrderByDescending(s => s.Date);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(leaderboardSortMode), leaderboardSortMode, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? LeaderboardSortMode.Score
|
||||
);
|
||||
var requestTaskSource = new TaskCompletionSource<LeaderboardScores>();
|
||||
globalScores.BindValueChanged(_ =>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public enum LeaderboardSortMode
|
||||
{
|
||||
Score,
|
||||
Accuracy,
|
||||
|
||||
[Description("Max Combo")]
|
||||
MaxCombo,
|
||||
|
||||
Misses,
|
||||
Date,
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
currentContent = new BeatmapLeaderboardWedge
|
||||
{
|
||||
Scope = { BindTarget = header.Scope },
|
||||
Sorting = { BindTarget = header.Sorting },
|
||||
FilterBySelectedMods = { BindTarget = header.FilterBySelectedMods },
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private FillFlowContainer leaderboardControls = null!;
|
||||
|
||||
private ShearedDropdown<BeatmapLeaderboardScope> scopeDropdown = null!;
|
||||
private ShearedDropdown<LeaderboardSortMode> sortDropdown = null!;
|
||||
private ShearedToggleButton selectedModsToggle = null!;
|
||||
|
||||
public IBindable<Selection> Type => tabControl.Current;
|
||||
@@ -32,6 +33,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly Bindable<BeatmapDetailTab> configDetailTab = new Bindable<BeatmapDetailTab>();
|
||||
|
||||
public IBindable<LeaderboardSortMode> Sorting => sortDropdown.Current;
|
||||
|
||||
private readonly Bindable<LeaderboardSortMode> configLeaderboardSortMode = new Bindable<LeaderboardSortMode>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods => selectedModsToggle.Active;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -58,52 +63,50 @@ 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
|
||||
sortDropdown = new ShearedDropdown<LeaderboardSortMode>("Sort")
|
||||
{
|
||||
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,
|
||||
},
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0,
|
||||
Items = Enum.GetValues<LeaderboardSortMode>(),
|
||||
},
|
||||
// new Container
|
||||
// {
|
||||
// Anchor = Anchor.CentreRight,
|
||||
// Origin = Anchor.CentreRight,
|
||||
// Size = new Vector2(150f, 33f),
|
||||
// Child = new ShearedDropdown<RankingsSort>(@"Sort")
|
||||
// {
|
||||
// Width = 150f,
|
||||
// Items = Enum.GetValues<RankingsSort>(),
|
||||
// },
|
||||
// },
|
||||
new Container
|
||||
scopeDropdown = new ScopeDropdown
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(160f, 32f),
|
||||
Child = scopeDropdown = new ScopeDropdown
|
||||
{
|
||||
Width = 160f,
|
||||
Current = { Value = BeatmapLeaderboardScope.Global },
|
||||
},
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f,
|
||||
Current = { Value = BeatmapLeaderboardScope.Global },
|
||||
},
|
||||
},
|
||||
},
|
||||
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.BeatmapLeaderboardSortMode, configLeaderboardSortMode);
|
||||
config.BindWith(OsuSetting.BeatmapDetailModsFilter, selectedModsToggle.Active);
|
||||
}
|
||||
|
||||
@@ -114,12 +117,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
scopeDropdown.Current.Value = tryMapDetailTabToLeaderboardScope(configDetailTab.Value) ?? scopeDropdown.Current.Value;
|
||||
scopeDropdown.Current.BindValueChanged(_ => updateConfigDetailTab());
|
||||
|
||||
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 =>
|
||||
{
|
||||
leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint);
|
||||
updateConfigDetailTab();
|
||||
}, true);
|
||||
|
||||
scopeDropdown.Current.BindValueChanged(v =>
|
||||
{
|
||||
bool isLocal = v.NewValue == BeatmapLeaderboardScope.Local;
|
||||
sortDropdown.ResizeWidthTo(isLocal ? 0.4f : 0, 300, Easing.OutQuint);
|
||||
sortDropdown.FadeTo(isLocal ? 1 : 0, 300, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
#region Reading / writing state from / to configuration
|
||||
@@ -197,15 +210,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
Ranking,
|
||||
}
|
||||
|
||||
// public enum RankingsSort
|
||||
// {
|
||||
// Score,
|
||||
// Accuracy,
|
||||
// Combo,
|
||||
// Misses,
|
||||
// Date,
|
||||
// }
|
||||
|
||||
private partial class ScopeDropdown : ShearedDropdown<BeatmapLeaderboardScope>
|
||||
{
|
||||
public ScopeDropdown()
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public IBindable<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
|
||||
|
||||
public IBindable<LeaderboardSortMode> Sorting { get; } = new Bindable<LeaderboardSortMode>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
@@ -188,6 +190,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
base.LoadComplete();
|
||||
|
||||
Scope.BindValueChanged(_ => refetchScores());
|
||||
Sorting.BindValueChanged(_ => refetchScores());
|
||||
FilterBySelectedMods.BindValueChanged(_ => refetchScores());
|
||||
beatmap.BindValueChanged(_ => refetchScores());
|
||||
ruleset.BindValueChanged(_ => refetchScores());
|
||||
@@ -233,12 +236,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),
|
||||
forceRefresh: true);
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, fetchSorting), forceRefresh: true);
|
||||
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user