From 4a628287e260e0120adc2dd102fcfc8c81930a78 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:13:37 -0500 Subject: [PATCH] Decouple game-wide ruleset bindable and refactor `LocalUserStatisticsProvider` This also throws away the logic of updating `API.LocalUser.Value.Statistics`. Components should rely on `LocalUserStatisticsProvider` instead for proper behaviour and ability to update on statistics updates. --- osu.Desktop/DiscordRichPresence.cs | 13 ++- .../Menus/TestSceneToolbarUserButton.cs | 12 +-- .../TestSceneLocalUserStatisticsProvider.cs | 88 +++++++++++------ .../Visual/Online/TestSceneUserPanel.cs | 10 +- .../Online/TestSceneUserStatisticsWatcher.cs | 19 ++-- .../Visual/Ranking/TestSceneOverallRanking.cs | 2 +- .../Ranking/TestSceneStatisticsPanel.cs | 6 +- .../Online/API/Requests/Responses/APIUser.cs | 12 ++- .../Online/LocalUserStatisticsProvider.cs | 98 ++++++++----------- ...e.cs => ScoreBasedUserStatisticsUpdate.cs} | 6 +- osu.Game/Online/UserStatisticsWatcher.cs | 38 ++++--- .../TransientUserStatisticsUpdateDisplay.cs | 4 +- .../Ranking/Statistics/User/OverallRanking.cs | 4 +- .../Statistics/User/RankingChangeRow.cs | 4 +- .../Ranking/Statistics/UserStatisticsPanel.cs | 4 +- osu.Game/Users/UserRankPanel.cs | 26 +++-- 16 files changed, 188 insertions(+), 158 deletions(-) rename osu.Game/Online/{UserStatisticsUpdate.cs => ScoreBasedUserStatisticsUpdate.cs} (84%) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 3ad4112733..c9529d2f5e 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -69,7 +69,7 @@ namespace osu.Desktop }; private IBindable? user; - private IBindable? localStatistics; + private IBindable? statisticsUpdate; [BackgroundDependencyLoader] private void load() @@ -123,8 +123,8 @@ namespace osu.Desktop activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); - localStatistics = statisticsProvider.Statistics.GetBoundCopy(); - localStatistics.BindValueChanged(_ => schedulePresenceUpdate()); + statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); + statisticsUpdate.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; } @@ -167,7 +167,7 @@ namespace osu.Desktop private void updatePresence(bool hideIdentifiableInformation) { - if (user == null || localStatistics == null) + if (user == null) return; // user activity @@ -237,7 +237,10 @@ namespace osu.Desktop if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (localStatistics.Value?.GlobalRank > 0 ? $" (rank #{localStatistics.Value?.GlobalRank:N0})" : string.Empty); + { + var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value); + presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); + } // small image presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index 71a45e2398..1af4af8f6b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Gain", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Loss", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Tiny increase in PP", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("No change 1", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Was null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Became null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs index 1a27fd1de5..342d805be4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -3,14 +3,15 @@ using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -34,7 +35,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("setup provider", () => { - OsuSpriteText text; + OsuTextFlowContainer text; ((DummyAPIAccess)API).HandleRequest = r => { @@ -59,17 +60,31 @@ namespace osu.Game.Tests.Visual.Online Clear(); Add(statisticsProvider = new LocalUserStatisticsProvider()); - Add(text = new OsuSpriteText + Add(text = new OsuTextFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - statisticsProvider.Statistics.BindValueChanged(s => + statisticsProvider.StatisticsUpdate.BindValueChanged(s => { - text.Text = s.NewValue == null - ? "Statistics: (null)" - : $"Statistics: (total score: {s.NewValue.TotalScore:N0})"; + text.Clear(); + + foreach (var ruleset in Dependencies.Get().AvailableRulesets) + { + text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics + ? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})" + : $"{ruleset.Name} statistics: (null)"); + text.NewLine(); + } + + if (s.NewValue == null) + text.AddText("latest update: (null)"); + else + { + text.AddText($"latest update: {s.NewValue.Ruleset}" + + $" ({(s.NewValue.OldStatistics?.TotalScore.ToString() ?? "null")} -> {s.NewValue.NewStatistics.TotalScore})"); + } }); Ruleset.Value = new OsuRuleset().RulesetInfo; @@ -79,19 +94,10 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestInitialStatistics() { - AddAssert("initial statistics populated", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); - } - - [Test] - public void TestRulesetChanges() - { - AddAssert("statistics from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); - AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); - AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); - AddAssert("statistics from catch", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(2_000_000)); - AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); - AddAssert("statistics from mania", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(1_000_000)); + AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000)); + AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000)); + AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000)); + AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000)); } [Test] @@ -105,18 +111,44 @@ namespace osu.Game.Tests.Visual.Online serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 }; }); - AddAssert("statistics matches user 1001 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); + AddAssert("statistics matches user 1001 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); - AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics matches user 1001 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); + AddAssert("statistics matches user 1001 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(3_000_000)); - AddStep("change ruleset to osu", () => Ruleset.Value = new OsuRuleset().RulesetInfo); setUser(1000, false); - AddAssert("statistics matches user 1000 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics matches user 1000 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(5_000_000)); - AddStep("change ruleset to osu", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics matches user 1000 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(6_000_000)); + AddAssert("statistics matches user 1000 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(6_000_000)); + } + + [Test] + public void TestRefetchStatistics() + { + setUser(1001); + + AddStep("update statistics server side", + () => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 }); + + AddAssert("statistics match old score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); + + AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo)); + AddUntilStep("statistics update raised", + () => statisticsProvider.StatisticsUpdate.Value.NewStatistics.TotalScore, + () => Is.EqualTo(9_000_000)); + AddAssert("statistics match new score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(9_000_000)); } private UserStatistics tryGetStatistics(int userId, string rulesetName) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 365dce551c..e291b90361 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -34,8 +34,8 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Cached] - private readonly LocalUserStatisticsProvider statisticsProvider = new LocalUserStatisticsProvider(); + [Cached(typeof(LocalUserStatisticsProvider))] + private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); [Resolved] private IRulesetStore rulesetStore { get; set; } @@ -206,5 +206,11 @@ namespace osu.Game.Tests.Visual.Online public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; } + + private partial class TestUserStatisticsProvider : LocalUserStatisticsProvider + { + public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) + => base.UpdateStatistics(newStatistics, ruleset); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs index e5ccad703e..c91dfe9eb7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online // note ordering - in this test processing completes *before* the registration is added. feignScoreProcessing(userId, ruleset, 5_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online feignScoreProcessing(userId, ruleset, 6_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); @@ -262,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddUntilStep("update received", () => update != null); - AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); - AddAssert("statistics values are correct", () => statisticsProvider.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); } private int nextUserId = 2000; @@ -292,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online }); } - private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => + private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => AddStep("register for updates", () => { watcher.RegisterForStatisticsUpdateAfter( diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index ffc7d88a34..b406ea369f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking }); private void displayUpdate(UserStatistics before, UserStatistics after) => - AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); + AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f46f76cbb8..c12b9d29bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking UserStatisticsWatcher userStatisticsWatcher = null!; ScoreInfo score = null!; - AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); + AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider()))); AddStep("set user statistics update", () => { score = TestResources.CreateTestScoreInfo(); score.OnlineID = 1234; - ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, + ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking Score = { Value = score }, DisplayedUserStatisticsUpdate = { - Value = new UserStatisticsUpdate(score, new UserStatistics + Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo { diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 5d80fde515..452b5f7654 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -223,12 +223,14 @@ namespace osu.Game.Online.API.Requests.Responses /// /// User statistics for the requested ruleset (in the case of a or response). - /// Otherwise empty. /// + /// + /// This returns null when accessed from . Use instead. + /// [JsonProperty(@"statistics")] public UserStatistics Statistics { - get => statistics ??= new UserStatistics(); + get => statistics; set { if (statistics != null) @@ -242,7 +244,11 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"rank_history")] private APIRankHistory rankHistory { - set => Statistics.RankHistory = value; + set + { + statistics ??= new UserStatistics(); + statistics.RankHistory = value; + } } [JsonProperty(@"active_tournament_banners")] diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index e64f88759e..ea4688a307 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,84 +19,63 @@ namespace osu.Game.Online /// public partial class LocalUserStatisticsProvider : Component { + private readonly Bindable statisticsUpdate = new Bindable(); + + /// + /// A bindable communicating updates to the local user's statistics on any ruleset. + /// This does not guarantee the presence of old statistics, as it is invoked on initial population of statistics. + /// + public IBindable StatisticsUpdate => statisticsUpdate; + [Resolved] - private IBindable ruleset { get; set; } = null!; + private RulesetStore rulesets { get; set; } = null!; [Resolved] private IAPIProvider api { get; set; } = null!; + private readonly Dictionary statisticsCache = new Dictionary(); + private readonly Dictionary statisticsRequests = new Dictionary(); + /// - /// The statistics of the local user for the game-wide selected ruleset. + /// Returns the currently available for the given ruleset. + /// This may return null if the requested statistics has not been fetched before yet. /// - public IBindable Statistics => statistics; - - private readonly Bindable statistics = new Bindable(); - - private readonly Dictionary allStatistics = new Dictionary(); + /// The ruleset to return the corresponding for. + public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName); protected override void LoadComplete() { base.LoadComplete(); - - statistics.BindValueChanged(v => - { - if (api.LocalUser.Value != null && v.NewValue != null) - api.LocalUser.Value.Statistics = v.NewValue; - }); - - ruleset.BindValueChanged(_ => updateStatisticsBindable()); - - api.LocalUser.BindValueChanged(_ => - { - allStatistics.Clear(); - updateStatisticsBindable(); - }, true); + api.LocalUser.BindValueChanged(_ => initialiseStatistics(), true); } - private GetUserRequest? currentRequest; - - private void updateStatisticsBindable() => Schedule(() => + private void initialiseStatistics() { - statistics.Value = null; + statisticsCache.Clear(); - if (api.LocalUser.Value == null || api.LocalUser.Value.OnlineID <= 1 || !ruleset.Value.IsLegacyRuleset()) - { - statistics.Value = new UserStatistics(); - return; - } - - if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) - { - currentRequest.Cancel(); - currentRequest = null; - } - - if (allStatistics.TryGetValue(ruleset.Value.ShortName, out var existing)) - statistics.Value = existing; - else - requestStatistics(ruleset.Value); - }); - - private void requestStatistics(RulesetInfo ruleset) - { - currentRequest = new GetUserRequest(api.LocalUser.Value.OnlineID, ruleset); - currentRequest.Success += u => statistics.Value = allStatistics[ruleset.ShortName] = u.Statistics; - api.Queue(currentRequest); + foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) + RefetchStatistics(ruleset); } - /// - /// Returns the currently available for the given ruleset. - /// This may return null if the requested statistics has not been fetched yet. - /// - /// The ruleset to return the corresponding for. - internal UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => allStatistics.GetValueOrDefault(ruleset.ShortName); - - internal void UpdateStatistics(UserStatistics statistics, RulesetInfo statisticsRuleset) + public void RefetchStatistics(RulesetInfo ruleset) { - allStatistics[statisticsRuleset.ShortName] = statistics; + if (statisticsRequests.TryGetValue(ruleset.ShortName, out var previousRequest)) + previousRequest.Cancel(); - if (statisticsRuleset.ShortName == ruleset.Value.ShortName) - updateStatisticsBindable(); + var request = statisticsRequests[ruleset.ShortName] = new GetUserRequest(api.LocalUser.Value.Id, ruleset); + request.Success += u => UpdateStatistics(u.Statistics, ruleset); + api.Queue(request); + } + + protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) + { + var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName); + + statisticsRequests.Remove(ruleset.ShortName); + statisticsCache[ruleset.ShortName] = newStatistics; + statisticsUpdate.Value = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics); } } + + public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics); } diff --git a/osu.Game/Online/UserStatisticsUpdate.cs b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs similarity index 84% rename from osu.Game/Online/UserStatisticsUpdate.cs rename to osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs index f85b219ef0..dc55c57c68 100644 --- a/osu.Game/Online/UserStatisticsUpdate.cs +++ b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online /// /// Contains data about the change in a user's profile statistics after completing a score. /// - public class UserStatisticsUpdate + public class ScoreBasedUserStatisticsUpdate { /// /// The score set by the user that triggered the update. @@ -27,12 +27,12 @@ namespace osu.Game.Online public UserStatistics After { get; } /// - /// Creates a new . + /// Creates a new . /// /// The score set by the user that triggered the update. /// The user's profile statistics prior to the score being set. /// The user's profile statistics after the score was set. - public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) + public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) { Score = score; Before = before; diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index 162204e4e8..bd3c4b819f 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -8,10 +8,8 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Scoring; -using osu.Game.Users; namespace osu.Game.Online { @@ -20,9 +18,12 @@ namespace osu.Game.Online /// public partial class UserStatisticsWatcher : Component { - private readonly LocalUserStatisticsProvider? statisticsProvider; - public IBindable LatestUpdate => latestUpdate; - private readonly Bindable latestUpdate = new Bindable(); + private readonly LocalUserStatisticsProvider statisticsProvider; + + public IBindable LatestUpdate => latestUpdate; + private readonly Bindable latestUpdate = new Bindable(); + + private ScoreInfo? scorePendingUpdate; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -32,7 +33,7 @@ namespace osu.Game.Online private readonly Dictionary watchedScores = new Dictionary(); - public UserStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) + public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider) { this.statisticsProvider = statisticsProvider; } @@ -40,7 +41,9 @@ namespace osu.Game.Online protected override void LoadComplete() { base.LoadComplete(); + spectatorClient.OnUserScoreProcessed += userScoreProcessed; + statisticsProvider.StatisticsUpdate.ValueChanged += onStatisticsUpdated; } /// @@ -69,27 +72,20 @@ namespace osu.Game.Online if (!watchedScores.Remove(scoreId, out var scoreInfo)) return; - requestStatisticsUpdate(userId, scoreInfo); + scorePendingUpdate = scoreInfo; + statisticsProvider.RefetchStatistics(scoreInfo.Ruleset); } - private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) + private void onStatisticsUpdated(ValueChangedEvent update) => Schedule(() => { - var request = new GetUserRequest(userId, scoreInfo.Ruleset); - request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics)); - api.Queue(request); - } - - private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics) - { - if (statisticsProvider == null) + if (scorePendingUpdate == null || !update.NewValue.Ruleset.Equals(scorePendingUpdate.Ruleset)) return; - var latestRulesetStatistics = statisticsProvider.GetStatisticsFor(scoreInfo.Ruleset); - statisticsProvider.UpdateStatistics(updatedStatistics, scoreInfo.Ruleset); + if (update.NewValue.OldStatistics != null) + latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scorePendingUpdate, update.NewValue.OldStatistics, update.NewValue.NewStatistics); - if (latestRulesetStatistics != null) - latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); - } + scorePendingUpdate = null; + }); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index 07c2e72774..d5891da936 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable { - public Bindable LatestUpdate { get; } = new Bindable(); + public Bindable LatestUpdate { get; } = new Bindable(); private Statistic globalRank = null!; private Statistic pp = null!; @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar }; if (userStatisticsWatcher != null) - ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); + ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 1e60e09486..171a3f0f65 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { private const float transition_duration = 300; - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; private GridContainer content = null!; @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User FinishTransforms(true); } - private void onUpdateReceived(ValueChangedEvent update) + private void onUpdateReceived(ValueChangedEvent update) { if (update.NewValue == null) { diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index e5f07d9891..e6a6530345 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { public abstract partial class RankingChangeRow : CompositeDrawable { - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private readonly Func accessor; @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true); } - private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) + private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) { var update = statisticsUpdate.NewValue; diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index 4e9c07ab7b..86fed4a9bb 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics { private readonly ScoreInfo achievedScore; - internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); + internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); - private IBindable latestGlobalStatisticsUpdate = null!; + private IBindable latestGlobalStatisticsUpdate = null!; public UserStatisticsPanel(ScoreInfo achievedScore) { diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5804fce4c1..6fa926998e 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -12,6 +12,7 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Users @@ -29,8 +30,6 @@ namespace osu.Game.Users private ProfileValueDisplay countryRankDisplay = null!; private LoadingLayer loadingLayer = null!; - private readonly IBindable statistics = new Bindable(); - public UserRankPanel(APIUser user) : base(user) { @@ -47,22 +46,31 @@ namespace osu.Game.Users [Resolved] private LocalUserStatisticsProvider? statisticsProvider { get; set; } + [Resolved] + private IBindable ruleset { get; set; } = null!; + + private IBindable statisticsUpdate = null!; + protected override void LoadComplete() { base.LoadComplete(); if (statisticsProvider != null) { - statistics.BindTo(statisticsProvider.Statistics); - statistics.BindValueChanged(stats => - { - loadingLayer.State.Value = stats.NewValue == null ? Visibility.Visible : Visibility.Hidden; - globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; - countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; - }, true); + statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); + statisticsUpdate.BindValueChanged(_ => updateDisplay(), true); } } + private void updateDisplay() + { + var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value); + + loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden; + globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + } + protected override Drawable CreateLayout() { FillFlowContainer details;