From 91fb59ee15caf75b08a43bd6508a4edf3f49e8f5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 07:37:13 +0300 Subject: [PATCH] Introduce `LocalUserStatisticsProvider` component --- .../TestSceneLocalUserStatisticsProvider.cs | 141 ++++++++++++++++++ .../Online/LocalUserStatisticsProvider.cs | 94 ++++++++++++ osu.Game/OsuGameBase.cs | 6 +- 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs create mode 100644 osu.Game/Online/LocalUserStatisticsProvider.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..1a27fd1de5 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -0,0 +1,141 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.Sprites; +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.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene + { + private LocalUserStatisticsProvider statisticsProvider = null!; + + private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("clear statistics", () => serverSideStatistics.Clear()); + + setUser(1000); + + AddStep("setup provider", () => + { + OsuSpriteText text; + + ((DummyAPIAccess)API).HandleRequest = r => + { + switch (r) + { + case GetUserRequest userRequest: + int userId = int.Parse(userRequest.Lookup); + string rulesetName = userRequest.Ruleset!.ShortName; + var response = new APIUser + { + Id = userId, + Statistics = tryGetStatistics(userId, rulesetName) + }; + + userRequest.TriggerSuccess(response); + return true; + + default: + return false; + } + }; + + Clear(); + Add(statisticsProvider = new LocalUserStatisticsProvider()); + Add(text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + statisticsProvider.Statistics.BindValueChanged(s => + { + text.Text = s.NewValue == null + ? "Statistics: (null)" + : $"Statistics: (total score: {s.NewValue.TotalScore:N0})"; + }); + + Ruleset.Value = new OsuRuleset().RulesetInfo; + }); + } + + [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)); + } + + [Test] + public void TestUserChanges() + { + setUser(1001); + + AddStep("update statistics for user 1000", () => + { + serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 }; + 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)); + + 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)); + + 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)); + + 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)); + } + + private UserStatistics tryGetStatistics(int userId, string rulesetName) + => serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics(); + + private void setUser(int userId, bool generateStatistics = true) + { + AddStep($"set local user to {userId}", () => + { + if (generateStatistics) + { + serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 }; + serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 }; + serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 }; + serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 }; + } + + ((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId }; + }); + } + } +} diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..e2f016b336 --- /dev/null +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Online +{ + /// + /// A component that is responsible for providing the latest statistics of the logged-in user for the game-wide selected ruleset. + /// + public partial class LocalUserStatisticsProvider : Component + { + /// + /// The statistics of the logged-in user for the game-wide selected ruleset. + /// + public IBindable Statistics => statistics; + + private readonly Bindable statistics = new Bindable(); + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly Dictionary allStatistics = new Dictionary(); + + 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); + } + + private GetUserRequest? currentRequest; + + private void updateStatisticsBindable() => Schedule(() => + { + statistics.Value = null; + + 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); + } + + internal void UpdateStatistics(UserStatistics statistics, RulesetInfo statisticsRuleset) + { + allStatistics[statisticsRuleset.ShortName] = statistics; + + if (statisticsRuleset.ShortName == ruleset.Value.ShortName) + updateStatisticsBindable(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a2a6322665..f574885757 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -208,6 +208,7 @@ namespace osu.Game private MetadataClient metadataClient; private SoloStatisticsWatcher soloStatisticsWatcher; + private LocalUserStatisticsProvider localUserStatisticsProvider; private RealmAccess realm; @@ -328,7 +329,9 @@ namespace osu.Game dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); - dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher()); + + dependencies.CacheAs(localUserStatisticsProvider = new LocalUserStatisticsProvider()); + dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher(localUserStatisticsProvider)); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); @@ -371,6 +374,7 @@ namespace osu.Game base.Content.Add(SpectatorClient); base.Content.Add(MultiplayerClient); base.Content.Add(metadataClient); + base.Content.Add(localUserStatisticsProvider); base.Content.Add(soloStatisticsWatcher); base.Content.Add(rulesetConfigCache);