From 31b98ac7b99414870c6b02fb472d5ab39d64fa44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 10:19:04 +0200 Subject: [PATCH] Ensure correct global leaderboard state when presenting scores With `ReplayPlayer` now consuming the `LeaderboardManager`'s global state, flows such as presenting a score need to set the global state up correctly to avoid accidentally showing a leaderboard from a completely different score. This also incidentally closes https://github.com/ppy/osu/issues/27609. --- .../Online/Leaderboards/LeaderboardManager.cs | 14 ++++++------- osu.Game/OsuGame.cs | 20 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 6 +++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 314705eb02..ff3fe39a96 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online.Leaderboards public IBindable Scores => scores; private readonly Bindable scores = new Bindable(); - private LeaderboardCriteria? criteria; + public LeaderboardCriteria? CurrentCriteria { get; private set; } private IDisposable? localScoreSubscription; private TaskCompletionSource? localFetchCompletionSource; @@ -45,10 +45,10 @@ namespace osu.Game.Online.Leaderboards public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) { - if (criteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); - criteria = newCriteria; + CurrentCriteria = newCriteria; localScoreSubscription?.Dispose(); inFlightOnlineRequest?.Cancel(); lastFetchCompletionSource?.TrySetCanceled(); @@ -110,7 +110,7 @@ namespace osu.Game.Online.Leaderboards private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { - Debug.Assert(criteria != null); + Debug.Assert(CurrentCriteria != null); // This subscription may fire from changes to linked beatmaps, which we don't care about. // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. @@ -119,9 +119,9 @@ namespace osu.Game.Online.Leaderboards var newScores = sender.AsEnumerable(); - if (criteria.ExactMods != null) + if (CurrentCriteria.ExactMods != null) { - if (!criteria.ExactMods.Any()) + if (!CurrentCriteria.ExactMods.Any()) { // we need to filter out all scores that have any mods to get all local nomod scores newScores = newScores.Where(s => !s.Mods.Any()); @@ -130,7 +130,7 @@ namespace osu.Game.Online.Leaderboards { // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself - var selectedMods = criteria.ExactMods.Select(m => m.Acronym).ToHashSet(); + var selectedMods = CurrentCriteria.ExactMods.Select(m => m.Acronym).ToHashSet(); newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3381553970..76d370b4dc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -47,6 +47,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; @@ -67,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Seasonal; using osu.Game.Skinning; using osu.Game.Updater; @@ -784,6 +786,24 @@ namespace osu.Game if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap)) Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); + var currentLeaderboard = LeaderboardManager.CurrentCriteria; + + bool leaderboardBeatmapMatches = currentLeaderboard != null && databasedBeatmap.Equals(currentLeaderboard.Beatmap); + bool leaderboardRulesetMatches = currentLeaderboard != null && databasedScore.ScoreInfo.Ruleset.Equals(currentLeaderboard.Ruleset); + + if (!leaderboardBeatmapMatches || !leaderboardRulesetMatches) + { + var newLeaderboard = currentLeaderboard != null + ? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset } + : new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null); + LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard) + .ContinueWith(t => + { + if (t.Exception != null) + Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network); + }); + } + switch (presentType) { case ScorePresentType.Gameplay: diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fb28b8c5a4..9a8a127886 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -204,7 +204,7 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; - private LeaderboardManager leaderboardManager; + protected LeaderboardManager LeaderboardManager { get; private set; } private RulesetConfigCache rulesetConfigCache; @@ -367,8 +367,8 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); - dependencies.Cache(leaderboardManager = new LeaderboardManager()); - base.Content.Add(leaderboardManager); + dependencies.Cache(LeaderboardManager = new LeaderboardManager()); + base.Content.Add(LeaderboardManager); // add api components to hierarchy. if (API is APIAccess apiAccess)