diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index f0506ed35c..69fedf4a3a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Online.API; +using osu.Game.Online.Solo; using osu.Game.Overlays.Toolbar; +using osu.Game.Scoring; +using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -87,5 +92,91 @@ namespace osu.Game.Tests.Visual.Menus AddStep($"Change state to {state}", () => dummyAPI.SetState(state)); } } + + [Test] + public void TestTransientUserStatisticsDisplay() + { + AddStep("Log in", () => dummyAPI.Login("wang", "jang")); + AddStep("Gain", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 123_456, + PP = 1234 + }, + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }); + }); + AddStep("Loss", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }, + new UserStatistics + { + GlobalRank = 123_456, + PP = 1234 + }); + }); + AddStep("No change", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }, + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }); + }); + AddStep("Was null", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = null, + PP = null + }, + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }); + }); + AddStep("Became null", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357 + }, + new UserStatistics + { + GlobalRank = null, + PP = null + }); + }); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs index 3607b37c7e..19121b7f58 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs @@ -35,8 +35,6 @@ namespace osu.Game.Tests.Visual.Online private Action? handleGetUsersRequest; private Action? handleGetUserRequest; - private IDisposable? subscription; - private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>(); [SetUpSteps] @@ -252,26 +250,6 @@ namespace osu.Game.Tests.Visual.Online AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000)); } - [Test] - public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal() - { - int userId = getUserId(); - setUpUser(userId); - - long scoreId = getScoreId(); - var ruleset = new OsuRuleset().RulesetInfo; - - SoloStatisticsUpdate? update = null; - registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); - AddStep("unsubscribe", () => subscription!.Dispose()); - - feignScoreProcessing(userId, ruleset, 5_000_000); - - AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); - AddWaitStep("wait a bit", 5); - AddAssert("update not received", () => update == null); - } - [Test] public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed() { @@ -312,13 +290,20 @@ namespace osu.Game.Tests.Visual.Online } private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => - AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter( - new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser()) + AddStep("register for updates", () => + { + watcher.RegisterForStatisticsUpdateAfter( + new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser()) + { + Ruleset = rulesetInfo, + OnlineID = scoreId + }); + watcher.LatestUpdate.BindValueChanged(update => { - Ruleset = rulesetInfo, - OnlineID = scoreId - }, - onUpdateReady)); + if (update.NewValue?.Score.OnlineID == scoreId) + onUpdateReady.Invoke(update.NewValue); + }); + }); private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore) => AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore }); diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs index 55b27fb364..2072e8633f 100644 --- a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs +++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs @@ -1,10 +1,10 @@ // 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.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Extensions; @@ -22,14 +22,16 @@ namespace osu.Game.Online.Solo /// public partial class SoloStatisticsWatcher : Component { + public IBindable LatestUpdate => latestUpdate; + private readonly Bindable latestUpdate = new Bindable(); + [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly Dictionary callbacks = new Dictionary(); - private long? lastProcessedScoreId; + private readonly Dictionary watchedScores = new Dictionary(); private Dictionary? latestStatistics; @@ -45,9 +47,7 @@ namespace osu.Game.Online.Solo /// Registers for a user statistics update after the given has been processed server-side. /// /// The score to listen for the statistics update for. - /// The callback to be invoked once the statistics update has been prepared. - /// An representing the subscription. Disposing it is equivalent to unsubscribing from future notifications. - public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action onUpdateReady) + public void RegisterForStatisticsUpdateAfter(ScoreInfo score) { Schedule(() => { @@ -57,24 +57,12 @@ namespace osu.Game.Online.Solo if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0) return; - var callback = new StatisticsUpdateCallback(score, onUpdateReady); - - if (lastProcessedScoreId == score.OnlineID) - { - requestStatisticsUpdate(api.LocalUser.Value.Id, callback); - return; - } - - callbacks.Add(score.OnlineID, callback); + watchedScores.Add(score.OnlineID, score); }); - - return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID))); } private void onUserChanged(APIUser? localUser) => Schedule(() => { - callbacks.Clear(); - lastProcessedScoreId = null; latestStatistics = null; if (localUser == null || localUser.OnlineID <= 1) @@ -107,25 +95,22 @@ namespace osu.Game.Online.Solo if (userId != api.LocalUser.Value?.OnlineID) return; - lastProcessedScoreId = scoreId; - - if (!callbacks.TryGetValue(scoreId, out var callback)) + if (!watchedScores.Remove(scoreId, out var scoreInfo)) return; - requestStatisticsUpdate(userId, callback); - callbacks.Remove(scoreId); + requestStatisticsUpdate(userId, scoreInfo); } - private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback) + private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) { - var request = new GetUserRequest(userId, callback.Score.Ruleset); - request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics)); + var request = new GetUserRequest(userId, scoreInfo.Ruleset); + request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics)); api.Queue(request); } - private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserStatistics updatedStatistics) + private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics) { - string rulesetName = callback.Score.Ruleset.ShortName; + string rulesetName = scoreInfo.Ruleset.ShortName; api.UpdateStatistics(updatedStatistics); @@ -135,9 +120,7 @@ namespace osu.Game.Online.Solo latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics); latestRulesetStatistics ??= new UserStatistics(); - var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics); - callback.OnUpdateReady.Invoke(update); - + latestUpdate.Value = new SoloStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); latestStatistics[rulesetName] = updatedStatistics; } @@ -148,17 +131,5 @@ namespace osu.Game.Online.Solo base.Dispose(isDisposing); } - - private class StatisticsUpdateCallback - { - public ScoreInfo Score { get; } - public Action OnUpdateReady { get; } - - public StatisticsUpdateCallback(ScoreInfo score, Action onUpdateReady) - { - Score = score; - OnUpdateReady = onUpdateReady; - } - } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 11798c22ff..9ffa88947b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -47,6 +47,7 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; +using osu.Game.Online.Solo; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Music; @@ -1021,6 +1022,7 @@ namespace osu.Game ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); }); + loadComponentSingleFile(new SoloStatisticsWatcher(), Add, true); loadComponentSingleFile(Toolbar = new Toolbar { OnHome = delegate diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a2a6322665..81e3d8bed8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -50,7 +50,6 @@ using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Solo; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; @@ -207,7 +206,6 @@ namespace osu.Game protected MultiplayerClient MultiplayerClient { get; private set; } private MetadataClient metadataClient; - private SoloStatisticsWatcher soloStatisticsWatcher; private RealmAccess realm; @@ -328,7 +326,6 @@ 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()); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); @@ -371,7 +368,6 @@ namespace osu.Game base.Content.Add(SpectatorClient); base.Content.Add(MultiplayerClient); base.Content.Add(metadataClient); - base.Content.Add(soloStatisticsWatcher); base.Content.Add(rulesetConfigCache); diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 2620e850c8..96c0b15c44 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -78,6 +78,13 @@ namespace osu.Game.Overlays.Toolbar } }); + Flow.Add(new TransientUserStatisticsUpdateDisplay + { + Alpha = 0 + }); + Flow.AutoSizeEasing = Easing.OutQuint; + Flow.AutoSizeDuration = 250; + apiState = api.State.GetBoundCopy(); apiState.BindValueChanged(onlineStateChanged, true); diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs new file mode 100644 index 0000000000..f56a1a3dd2 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -0,0 +1,235 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Solo; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Overlays.Toolbar +{ + public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable + { + public Bindable LatestUpdate { get; } = new Bindable(); + + private Statistic globalRank = null!; + private Statistic pp = null!; + + [BackgroundDependencyLoader] + private void load(SoloStatisticsWatcher? soloStatisticsWatcher) + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + Alpha = 0; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 10 }, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + globalRank = new Statistic(UsersStrings.ShowRankGlobalSimple, @"#", Comparer.Create((before, after) => before - after)), + pp = new Statistic(RankingsStrings.StatPerformance, string.Empty, Comparer.Create((before, after) => Math.Sign(after - before))), + } + }; + + if (soloStatisticsWatcher != null) + ((IBindable)LatestUpdate).BindTo(soloStatisticsWatcher.LatestUpdate); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LatestUpdate.BindValueChanged(val => + { + if (val.NewValue == null) + return; + + var update = val.NewValue; + + // null handling here is best effort because it is annoying. + + globalRank.Alpha = update.After.GlobalRank == null ? 0 : 1; + pp.Alpha = update.After.PP == null ? 0 : 1; + + if (globalRank.Alpha == 0 && pp.Alpha == 0) + return; + + FinishTransforms(true); + + this.FadeIn(500, Easing.OutQuint); + + if (update.After.GlobalRank != null) + { + globalRank.Display( + update.Before.GlobalRank ?? update.After.GlobalRank.Value, + Math.Abs((update.After.GlobalRank.Value - update.Before.GlobalRank) ?? 0), + update.After.GlobalRank.Value); + } + + if (update.After.PP != null) + pp.Display(update.Before.PP ?? update.After.PP.Value, Math.Abs((update.After.PP - update.Before.PP) ?? 0M), update.After.PP.Value); + + this.Delay(5000).FadeOut(500, Easing.OutQuint); + }); + } + + private partial class Statistic : CompositeDrawable + where T : struct, IEquatable, IFormattable + { + private readonly LocalisableString title; + private readonly string mainValuePrefix; + private readonly IComparer valueComparer; + + private Counter mainValue = null!; + private Counter deltaValue = null!; + private OsuSpriteText titleText = null!; + private ScheduledDelegate? valueUpdateSchedule; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public Statistic(LocalisableString title, string mainValuePrefix, IComparer valueComparer) + { + this.title = title; + this.mainValuePrefix = mainValuePrefix; + this.valueComparer = valueComparer; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + mainValue = new Counter + { + ValuePrefix = mainValuePrefix, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = new Drawable[] + { + deltaValue = new Counter + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(size: 12, fixedWidth: true, weight: FontWeight.SemiBold), + AlwaysPresent = true, + }, + titleText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Text = title, + AlwaysPresent = true, + } + } + } + } + }; + } + + public void Display(T before, T delta, T after) + { + valueUpdateSchedule?.Cancel(); + valueUpdateSchedule = null; + + int comparison = valueComparer.Compare(before, after); + + if (comparison > 0) + { + deltaValue.Colour = colours.Lime1; + deltaValue.ValuePrefix = "+"; + } + else if (comparison < 0) + { + deltaValue.Colour = colours.Red1; + deltaValue.ValuePrefix = "-"; + } + else + { + deltaValue.Colour = Colour4.White; + deltaValue.ValuePrefix = string.Empty; + } + + mainValue.SetCountWithoutRolling(before); + deltaValue.SetCountWithoutRolling(delta); + + titleText.Alpha = 1; + deltaValue.Alpha = 0; + + using (BeginDelayedSequence(1200)) + { + titleText.FadeOut(250, Easing.OutQuad); + deltaValue.FadeIn(250, Easing.OutQuad); + + using (BeginDelayedSequence(1250)) + { + valueUpdateSchedule = Schedule(() => + { + mainValue.Current.Value = after; + deltaValue.Current.SetDefault(); + }); + } + } + } + } + + private partial class Counter : RollingCounter + where T : struct, IEquatable, IFormattable + { + public FontUsage Font { get; init; } = OsuFont.Default.With(fixedWidth: true); + + public string ValuePrefix + { + get => valuePrefix; + set + { + valuePrefix = value; + UpdateDisplay(); + } + } + + private string valuePrefix = string.Empty; + + protected override LocalisableString FormatCount(T count) => LocalisableString.Format(@"{0}{1:N0}", ValuePrefix, count); + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(t => + { + t.Font = Font; + t.Spacing = new Vector2(-1.5f, 0); + }); + + protected override double RollingDuration => 1500; + } + } +} diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index c8e84f1961..c45d46e993 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -17,6 +17,7 @@ using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Online.Solo; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -42,6 +43,10 @@ namespace osu.Game.Screens.Play [Resolved] private SessionStatics statics { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private SoloStatisticsWatcher soloStatisticsWatcher { get; set; } + private readonly object scoreSubmissionLock = new object(); private TaskCompletionSource scoreSubmissionSource; @@ -175,6 +180,7 @@ namespace osu.Game.Screens.Play await submitScore(score).ConfigureAwait(false); spectatorClient.EndPlaying(GameplayState); + soloStatisticsWatcher?.RegisterForStatisticsUpdateAfter(score.ScoreInfo); } [Resolved] diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 82dade40eb..69cfbed8f2 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -41,9 +41,6 @@ namespace osu.Game.Screens.Ranking public override bool? AllowGlobalTrackControl => true; - // Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently. - public override bool HideOverlaysOnEnter => true; - public readonly Bindable SelectedScore = new Bindable(); [CanBeNull] diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 22d631e137..866440bbd6 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,10 +31,7 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } = null!; - [Resolved] - private SoloStatisticsWatcher soloStatisticsWatcher { get; set; } = null!; - - private IDisposable? statisticsSubscription; + private IBindable latestUpdate = null!; private readonly Bindable statisticsUpdate = new Bindable(); public SoloResultsScreen(ScoreInfo score, bool allowRetry) @@ -42,14 +39,20 @@ namespace osu.Game.Screens.Ranking { } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load(SoloStatisticsWatcher? soloStatisticsWatcher) { - base.LoadComplete(); + if (ShowUserStatistics && soloStatisticsWatcher != null) + { + Debug.Assert(Score != null); - Debug.Assert(Score != null); - - if (ShowUserStatistics) - statisticsSubscription = soloStatisticsWatcher.RegisterForStatisticsUpdateAfter(Score, update => statisticsUpdate.Value = update); + latestUpdate = soloStatisticsWatcher.LatestUpdate.GetBoundCopy(); + latestUpdate.BindValueChanged(update => + { + if (update.NewValue?.Score.MatchesOnlineID(Score) == true) + statisticsUpdate.Value = update.NewValue; + }); + } } protected override StatisticsPanel CreateStatisticsPanel() @@ -84,7 +87,6 @@ namespace osu.Game.Screens.Ranking base.Dispose(isDisposing); getScoreRequest?.Cancel(); - statisticsSubscription?.Dispose(); } } }