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

Introduce UserStatisticsProvider component and add support for respecting selected ruleset #27128

Merged
merged 28 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
91fb59e
Introduce `LocalUserStatisticsProvider` component
frenzibyte Feb 11, 2024
3ab60b7
Remove `IAPIProvider.Statistics` in favour of the new component
frenzibyte Feb 11, 2024
633d854
Update `UserRankPanel` implementation to use new component
frenzibyte Feb 11, 2024
bc2b705
Fix `ImportTest.TestOsuGameBase` having null ruleset
frenzibyte Feb 11, 2024
11b3fa8
Fix `TestSceneUserPanel` tests failing
frenzibyte Feb 11, 2024
701fb56
Merge branch 'master' into user-statistics-provider
frenzibyte Oct 25, 2024
2fd4952
Fix post-merge errors
frenzibyte Oct 25, 2024
3a57b21
Move `LocalUserStatisticsProvider` to non-base game class and make de…
frenzibyte Oct 25, 2024
44dd813
Make `UserStatisticsWatcher` fully rely on `LocalUserStatisticsProvider`
frenzibyte Oct 25, 2024
fdeb8b9
Merge branch 'master' into user-statistics-provider
frenzibyte Oct 25, 2024
663b769
Update `DiscordRichPresence` to use new statistics provider component
frenzibyte Oct 25, 2024
979065c
Reorder code slightly
frenzibyte Oct 27, 2024
0760451
Merge branch 'master' into user-statistics-provider
peppy Nov 13, 2024
6c8a900
Merge branch 'master' into user-statistics-provider
frenzibyte Nov 17, 2024
4a62828
Decouple game-wide ruleset bindable and refactor `LocalUserStatistics…
frenzibyte Nov 17, 2024
28f8740
Make `DifficultyRecommender` rely on the statistics provider
frenzibyte Nov 17, 2024
07609b6
Fix `UserRankPanel` not updating on ruleset change
frenzibyte Nov 17, 2024
1847b67
Only update user rank panel display when ruleset matches
frenzibyte Nov 17, 2024
caf56af
Fix various test failures
frenzibyte Nov 18, 2024
b106833
Fix more test / component breakage
frenzibyte Nov 18, 2024
74daf85
Replace bindable with an event
frenzibyte Nov 18, 2024
0b52080
Handle logged out user
frenzibyte Nov 18, 2024
631bfad
Replace event subscription with callback in `UserStatisticsWatcher`
frenzibyte Nov 24, 2024
aa1358b
Enable NRT and fix code
frenzibyte Nov 24, 2024
53b3906
Fix failing test
frenzibyte Nov 24, 2024
0a3f3c3
Add guard against fetching statistics for non-legacy rulesets
bdach Nov 25, 2024
b76460f
Schedule the thing
frenzibyte Nov 26, 2024
42c68ba
Add inline comment
frenzibyte Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 };
});
}
}
}
33 changes: 22 additions & 11 deletions osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
Expand All @@ -31,7 +32,10 @@ public partial class TestSceneUserPanel : OsuTestScene
private TestUserListPanel boundPanel2;

[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);

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

[Resolved]
private IRulesetStore rulesetStore { get; set; }
Expand All @@ -42,7 +46,11 @@ public void SetUp() => Schedule(() =>
activity.Value = null;
status.Value = null;

Child = new FillFlowContainer
Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);

Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Expand Down Expand Up @@ -108,7 +116,7 @@ public void SetUp() => Schedule(() =>
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
}
};
});

boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity);
Expand Down Expand Up @@ -162,24 +170,21 @@ public void TestUserStatisticsChange()
{
AddStep("update statistics", () =>
{
API.UpdateStatistics(new UserStatistics
statisticsProvider.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000)
});
}, Ruleset.Value);
});
AddStep("set statistics to something big", () =>
{
API.UpdateStatistics(new UserStatistics
statisticsProvider.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000)
});
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}, Ruleset.Value);
});
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
}

private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
Expand All @@ -201,5 +206,11 @@ public TestUserListPanel(APIUser user)

public new TextFlowContainer LastVisitMessage => base.LastVisitMessage;
}

private partial class TestUserStatisticsProvider : LocalUserStatisticsProvider
{
public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset)
=> base.UpdateStatistics(newStatistics, ruleset);
}
}
}
Loading
Loading