Skip to content

Commit

Permalink
Merge pull request #27128 from frenzibyte/user-statistics-provider
Browse files Browse the repository at this point in the history
Introduce `UserStatisticsProvider` component and add support for respecting selected ruleset
  • Loading branch information
peppy authored Nov 27, 2024
2 parents a1a6360 + 42c68ba commit 573aaf6
Show file tree
Hide file tree
Showing 24 changed files with 498 additions and 219 deletions.
17 changes: 13 additions & 4 deletions osu.Desktop/DiscordRichPresence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
Expand Down Expand Up @@ -47,6 +48,9 @@ internal partial class DiscordRichPresence : Component
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;

[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;

[Resolved]
private OsuConfigManager config { get; set; } = null!;

Expand Down Expand Up @@ -117,7 +121,9 @@ protected override void LoadComplete()
status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());

multiplayerClient.RoomUpdated += onRoomUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
}

private void onReady(object _, ReadyMessage __)
Expand All @@ -133,6 +139,8 @@ private void onReady(object _, ReadyMessage __)

private void onRoomUpdated() => schedulePresenceUpdate();

private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate();

private ScheduledDelegate? presenceUpdateDelegate;

private void schedulePresenceUpdate()
Expand Down Expand Up @@ -229,10 +237,8 @@ private void updatePresence(bool hideIdentifiableInformation)
presence.Assets.LargeImageText = string.Empty;
else
{
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics))
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.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
Expand Down Expand Up @@ -346,6 +352,9 @@ protected override void Dispose(bool isDisposing)
if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated;

if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;

client.Dispose();
base.Dispose(isDisposing);
}
Expand Down
5 changes: 5 additions & 0 deletions osu.Game.Tests/ImportTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
Expand Down Expand Up @@ -64,6 +65,10 @@ private void load()
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();

// the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase.
// the ruleset bindable is not meant to be nullable, so assign any ruleset in here.
Ruleset.Value = RulesetStore.AvailableRulesets.First();
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK.Input;
Expand All @@ -31,6 +33,9 @@ public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;

[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();

[BackgroundDependencyLoader]
private void load()
{
Expand Down Expand Up @@ -170,6 +175,7 @@ public void TestClickingOnFlagClosesOverlay()
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online);

AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());
Expand Down
12 changes: 6 additions & 6 deletions osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("Gain", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand All @@ -118,7 +118,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("Loss", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand All @@ -136,7 +136,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("Tiny increase in PP", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand All @@ -153,7 +153,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("No change 1", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand All @@ -170,7 +170,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("Was null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand All @@ -187,7 +187,7 @@ public void TestTransientUserStatisticsDisplay()
AddStep("Became null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
Expand Down
179 changes: 179 additions & 0 deletions osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
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;
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", () =>
{
OsuTextFlowContainer 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 OsuTextFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
statisticsProvider.StatisticsUpdated += update =>
{
text.Clear();
foreach (var ruleset in Dependencies.Get<RulesetStore>().AvailableRulesets)
{
text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics
? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})"
: $"{ruleset.Name} statistics: (null)");
text.NewLine();
}
text.AddText($"latest update: {update.Ruleset}"
+ $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})");
};
Ruleset.Value = new OsuRuleset().RulesetInfo;
});
}

[Test]
public void TestInitialStatistics()
{
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]
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 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));

AddAssert("statistics matches user 1001 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(3_000_000));

setUser(1000, false);

AddAssert("statistics matches user 1000 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(5_000_000));

AddAssert("statistics matches user 1000 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(6_000_000));
}

[Test]
public void TestRefetchStatistics()
{
UserStatisticsUpdate? update = null;

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("setup event", () =>
{
update = null;
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
});

AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo));
AddUntilStep("statistics update raised",
() => update?.NewStatistics.TotalScore,
() => Is.EqualTo(9_000_000));
AddAssert("statistics match new score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(9_000_000));

void onStatisticsUpdated(UserStatisticsUpdate u) => update = u;
}

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 };
});
}
}
}
Loading

0 comments on commit 573aaf6

Please sign in to comment.