Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch online statistics changes after every play & display them in toolbar #27156

Merged
merged 11 commits into from
Feb 14, 2024
91 changes: 91 additions & 0 deletions osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -87,5 +92,91 @@ public void TestStates()
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<TransientUserStatisticsUpdateDisplay>().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<TransientUserStatisticsUpdateDisplay>().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<TransientUserStatisticsUpdateDisplay>().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<TransientUserStatisticsUpdateDisplay>().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<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
GlobalRank = 111_111,
PP = 1357
},
new UserStatistics
{
GlobalRank = null,
PP = null
});
});
}
}
}
41 changes: 13 additions & 28 deletions osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
private Action<GetUsersRequest>? handleGetUsersRequest;
private Action<GetUserRequest>? handleGetUserRequest;

private IDisposable? subscription;

private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();

[SetUpSteps]
Expand Down Expand Up @@ -252,26 +250,6 @@ public void TestIgnoredScoreUpdateIsMergedIntoNextOne()
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()
{
Expand Down Expand Up @@ -312,13 +290,20 @@ private void setUpUser(int userId)
}

private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> 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 });
Expand Down
59 changes: 15 additions & 44 deletions osu.Game/Online/Solo/SoloStatisticsWatcher.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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;
Expand All @@ -22,14 +22,16 @@ namespace osu.Game.Online.Solo
/// </summary>
public partial class SoloStatisticsWatcher : Component
{
public IBindable<SoloStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<SoloStatisticsUpdate?> latestUpdate = new Bindable<SoloStatisticsUpdate?>();

[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;

[Resolved]
private IAPIProvider api { get; set; } = null!;

private readonly Dictionary<long, StatisticsUpdateCallback> callbacks = new Dictionary<long, StatisticsUpdateCallback>();
private long? lastProcessedScoreId;
private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>();

private Dictionary<string, UserStatistics>? latestStatistics;

Expand All @@ -45,9 +47,7 @@ protected override void LoadComplete()
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
/// </summary>
/// <param name="score">The score to listen for the statistics update for.</param>
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
/// <returns>An <see cref="IDisposable"/> representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.</returns>
public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
public void RegisterForStatisticsUpdateAfter(ScoreInfo score)
{
Schedule(() =>
{
Expand All @@ -57,24 +57,12 @@ public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<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)
Expand Down Expand Up @@ -107,25 +95,22 @@ private void userScoreProcessed(int userId, long scoreId)
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);

Expand All @@ -135,9 +120,7 @@ private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserSta
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;
}

Expand All @@ -148,17 +131,5 @@ protected override void Dispose(bool isDisposing)

base.Dispose(isDisposing);
}

private class StatisticsUpdateCallback
{
public ScoreInfo Score { get; }
public Action<SoloStatisticsUpdate> OnUpdateReady { get; }

public StatisticsUpdateCallback(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
{
Score = score;
OnUpdateReady = onUpdateReady;
}
}
}
}
2 changes: 2 additions & 0 deletions osu.Game/OsuGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;
Expand Down Expand Up @@ -1021,6 +1022,7 @@ protected override void LoadComplete()
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
});

loadComponentSingleFile(new SoloStatisticsWatcher(), Add, true);
loadComponentSingleFile(Toolbar = new Toolbar
{
OnHome = delegate
Expand Down
4 changes: 0 additions & 4 deletions osu.Game/OsuGameBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
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;
Expand Down Expand Up @@ -207,7 +206,6 @@ public virtual string Version
protected MultiplayerClient MultiplayerClient { get; private set; }

private MetadataClient metadataClient;
private SoloStatisticsWatcher soloStatisticsWatcher;

private RealmAccess realm;

Expand Down Expand Up @@ -328,7 +326,6 @@ private void load(ReadableKeyCombinationProvider keyCombinationProvider, Framewo
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));

Expand Down Expand Up @@ -371,7 +368,6 @@ private void load(ReadableKeyCombinationProvider keyCombinationProvider, Framewo
base.Content.Add(SpectatorClient);
base.Content.Add(MultiplayerClient);
base.Content.Add(metadataClient);
base.Content.Add(soloStatisticsWatcher);

base.Content.Add(rulesetConfigCache);

Expand Down
7 changes: 7 additions & 0 deletions osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login)
}
});

Flow.Add(new TransientUserStatisticsUpdateDisplay
{
Alpha = 0
});
Flow.AutoSizeEasing = Easing.OutQuint;
Flow.AutoSizeDuration = 250;

apiState = api.State.GetBoundCopy();
apiState.BindValueChanged(onlineStateChanged, true);

Expand Down
Loading
Loading