1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-07 22:22:59 +08:00

Merge pull request #27128 from frenzibyte/user-statistics-provider

Introduce `UserStatisticsProvider` component and add support for respecting selected ruleset
This commit is contained in:
Dean Herbert 2024-11-27 13:13:47 +09:00 committed by GitHub
commit 573aaf6637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 498 additions and 219 deletions

View File

@ -15,6 +15,7 @@ using osu.Framework.Threading;
using osu.Game; using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -47,6 +48,9 @@ namespace osu.Desktop
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -117,7 +121,9 @@ namespace osu.Desktop
status.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
} }
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
@ -133,6 +139,8 @@ namespace osu.Desktop
private void onRoomUpdated() => schedulePresenceUpdate(); private void onRoomUpdated() => schedulePresenceUpdate();
private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate; private ScheduledDelegate? presenceUpdateDelegate;
private void schedulePresenceUpdate() private void schedulePresenceUpdate()
@ -229,10 +237,8 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value);
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); 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);
} }
// small image // small image
@ -346,6 +352,9 @@ namespace osu.Desktop
if (multiplayerClient.IsNotNull()) if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated; multiplayerClient.RoomUpdated -= onRoomUpdated;
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
client.Dispose(); client.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -64,6 +65,10 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded. // Beatmap must be imported before the collection manager is loaded.
if (withBeatmap) if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); 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();
} }
} }
} }

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Login; using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK.Input; using osuTK.Input;
@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private OsuConfigManager configManager { get; set; } = null!; private OsuConfigManager configManager { get; set; } = null!;
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088"); AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online); assertAPIState(APIState.Online);
AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () => AddStep("click on flag", () =>
{ {
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First()); InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());

View File

@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Gain", () => AddStep("Gain", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Loss", () => AddStep("Loss", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Tiny increase in PP", () => AddStep("Tiny increase in PP", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("No change 1", () => AddStep("No change 1", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Was null", () => AddStep("Was null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Became null", () => AddStep("Became null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {

View File

@ -0,0 +1,179 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 };
});
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public partial class TestSceneUserPanel : OsuTestScene public partial class TestSceneUserPanel : OsuTestScene
{ {
private readonly Bindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private UserGridPanel boundPanel1; private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2; private TestUserListPanel boundPanel2 = null!;
[Cached] [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] [Resolved]
private IRulesetStore rulesetStore { get; set; } private IRulesetStore rulesetStore { get; set; } = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online
activity.Value = null; activity.Value = null;
status.Value = null; status.Value = null;
Child = new FillFlowContainer Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 } }) { Width = 300 }
} }
}; });
boundPanel1.Status.BindTo(status); boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity); boundPanel1.Activity.BindTo(activity);
@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("update statistics", () => AddStep("update statistics", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(100000), GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000) CountryRank = RNG.Next(100000)
}); }, Ruleset.Value);
}); });
AddStep("set statistics to something big", () => AddStep("set statistics to something big", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(1_000_000, 100_000_000), GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000) CountryRank = RNG.Next(1_000_000, 100_000_000)
}); }, Ruleset.Value);
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}); });
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
} }
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
@ -201,5 +204,11 @@ namespace osu.Game.Tests.Visual.Online
public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; public new TextFlowContainer LastVisitMessage => base.LastVisitMessage;
} }
public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider
{
public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
=> base.UpdateStatistics(newStatistics, ruleset, callback);
}
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
protected override bool UseOnlineAPI => false; protected override bool UseOnlineAPI => false;
private LocalUserStatisticsProvider statisticsProvider = null!;
private UserStatisticsWatcher watcher = null!; private UserStatisticsWatcher watcher = null!;
[Resolved] [Resolved]
@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online
AddStep("create watcher", () => AddStep("create watcher", () =>
{ {
Child = watcher = new UserStatisticsWatcher(); Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(watcher = new UserStatisticsWatcher(statisticsProvider));
}); });
} }
@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -146,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online
// note ordering - in this test processing completes *before* the registration is added. // note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online
feignScoreProcessing(userId, ruleset, 6_000_000); feignScoreProcessing(userId, ruleset, 6_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null); AddUntilStep("update received", () => update != null);
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
} }
private int nextUserId = 2000; private int nextUserId = 2000;
@ -289,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<UserStatisticsUpdate> onUpdateReady) => private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<ScoreBasedUserStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => AddStep("register for updates", () =>
{ {
watcher.RegisterForStatisticsUpdateAfter( watcher.RegisterForStatisticsUpdateAfter(

View File

@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
}); });
private void displayUpdate(UserStatistics before, UserStatistics after) => private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
} }
} }

View File

@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking
UserStatisticsWatcher userStatisticsWatcher = null!; UserStatisticsWatcher userStatisticsWatcher = null!;
ScoreInfo score = null!; ScoreInfo score = null!;
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider())));
AddStep("set user statistics update", () => AddStep("set user statistics update", () =>
{ {
score = TestResources.CreateTestScoreInfo(); score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234; score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, ((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
new UserStatistics new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking
Score = { Value = score }, Score = { Value = score },
DisplayedUserStatisticsUpdate = DisplayedUserStatisticsUpdate =
{ {
Value = new UserStatisticsUpdate(score, new UserStatistics Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
{ {

View File

@ -8,14 +8,14 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; 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;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapRecommendations : OsuGameTestScene public partial class TestSceneBeatmapRecommendations : OsuGameTestScene
{ {
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("populate ruleset statistics", () => AddStep("populate ruleset statistics", () =>
{ {
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>(); ((DummyAPIAccess)API).HandleRequest = r =>
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{ {
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics switch (r)
{ {
PP = getNecessaryPP(rulesetInfo.OnlineID) case GetUserRequest userRequest:
}; userRequest.TriggerSuccess(new APIUser
}); {
Id = 99,
Statistics = new UserStatistics
{
PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0)
}
});
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; return true;
default:
return false;
}
};
}); });
decimal getNecessaryPP(int? rulesetID) decimal getNecessaryPP(int? rulesetID)

View File

@ -9,9 +9,11 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API; using osu.Game.Online;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -21,18 +23,63 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public partial class DifficultyRecommender : Component public partial class DifficultyRecommender : Component
{ {
[Resolved] private readonly LocalUserStatisticsProvider statisticsProvider;
private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } private Bindable<RulesetInfo> gameRuleset { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>(); private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>();
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || gameRuleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(gameRuleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(gameRuleset.Value.ShortName);
}
}
public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
api.LocalUser.BindValueChanged(_ => populateValues(), true); foreach (var ruleset in rulesets.AvailableRulesets)
{
if (statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics)
updateMapping(ruleset, statistics);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
}
private void onStatisticsUpdated(UserStatisticsUpdate update) => updateMapping(update.Ruleset, update.NewStatistics);
private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
} }
/// <summary> /// <summary>
@ -64,35 +111,12 @@ namespace osu.Game.Beatmaps
return null; return null;
} }
private void populateValues() protected override void Dispose(bool isDisposing)
{ {
if (api.LocalUser.Value.RulesetsStatistics == null) if (statisticsProvider.IsNotNull())
return; statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) base.Dispose(isDisposing);
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
}
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(ruleset.Value.ShortName);
}
} }
} }
} }

View File

@ -59,7 +59,6 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser; public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends; public IBindableList<APIRelation> Friends => friends;
public IBindable<UserActivity> Activity => activity; public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public INotificationsClient NotificationsClient { get; } public INotificationsClient NotificationsClient { get; }
@ -74,8 +73,6 @@ namespace osu.Game.Online.API
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatistics> statistics { get; } = new Bindable<UserStatistics>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -604,14 +601,6 @@ namespace osu.Game.Online.API
flushQueue(); flushQueue();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
if (!IsLoggedIn) if (!IsLoggedIn)
@ -630,11 +619,7 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser(); private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
{
localUser.Value = user;
statistics.Value = user.Statistics;
}, false);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -30,8 +30,6 @@ namespace osu.Game.Online.API
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>(); public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@ -178,11 +176,6 @@ namespace osu.Game.Online.API
private void onSuccessfulLogin() private void onSuccessfulLogin()
{ {
state.Value = APIState.Online; state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
} }
public void Logout() public void Logout()
@ -193,14 +186,6 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser(); LocalUser.Value = new GuestUser();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
Statistics.Value = newStatistics;
if (IsLoggedIn)
LocalUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
} }
@ -220,7 +205,6 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser; IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends; IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity; IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary> /// <summary>
/// Skip 2FA requirement for next login. /// Skip 2FA requirement for next login.

View File

@ -29,11 +29,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
IBindable<UserActivity> Activity { get; } IBindable<UserActivity> Activity { get; }
/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
/// <summary> /// <summary>
/// The language supplied by this provider to API requests. /// The language supplied by this provider to API requests.
/// </summary> /// </summary>
@ -129,11 +124,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
void Logout(); void Logout();
/// <summary>
/// Sets Statistics bindable.
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary> /// <summary>
/// Update the friends status of the current user. /// Update the friends status of the current user.
/// </summary> /// </summary>

View File

@ -223,8 +223,10 @@ namespace osu.Game.Online.API.Requests.Responses
/// <summary> /// <summary>
/// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response). /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response).
/// Otherwise empty.
/// </summary> /// </summary>
/// <remarks>
/// This returns null when accessed from <see cref="IAPIProvider.LocalUser"/>. Use <see cref="LocalUserStatisticsProvider"/> instead.
/// </remarks>
[JsonProperty(@"statistics")] [JsonProperty(@"statistics")]
public UserStatistics Statistics public UserStatistics Statistics
{ {

View File

@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.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
{
/// <summary>
/// A component that keeps track of the latest statistics for the local user.
/// </summary>
public partial class LocalUserStatisticsProvider : Component
{
/// <summary>
/// Invoked whenever a change occured to the statistics of any ruleset,
/// either due to change in local user (log out and log in) or as a result of score submission.
/// </summary>
/// <remarks>
/// This does not guarantee the presence of the old statistics,
/// specifically in the case of initial population or change in local user.
/// </remarks>
public event Action<UserStatisticsUpdate>? StatisticsUpdated;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary>
/// Returns the <see cref="UserStatistics"/> currently available for the given ruleset.
/// This may return null if the requested statistics has not been fetched before yet.
/// </summary>
/// <param name="ruleset">The ruleset to return the corresponding <see cref="UserStatistics"/> for.</param>
public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName);
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(_ =>
{
// queuing up requests directly on user change is unsafe, as the API status may have not been updated yet.
// schedule a frame to allow the API to be in its correct state sending requests.
Schedule(initialiseStatistics);
}, true);
}
private void initialiseStatistics()
{
statisticsCache.Clear();
if (api.LocalUser.Value == null || api.LocalUser.Value.Id <= 1)
return;
foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset()))
RefetchStatistics(ruleset);
}
public void RefetchStatistics(RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
if (!ruleset.IsLegacyRuleset())
throw new InvalidOperationException($@"Retrieving statistics is not supported for ruleset {ruleset.ShortName}");
var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset);
request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback);
api.Queue(request);
}
protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName);
statisticsCache[ruleset.ShortName] = newStatistics;
var update = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics);
callback?.Invoke(update);
StatisticsUpdated?.Invoke(update);
}
}
public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics);
}

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online
/// <summary> /// <summary>
/// Contains data about the change in a user's profile statistics after completing a score. /// Contains data about the change in a user's profile statistics after completing a score.
/// </summary> /// </summary>
public class UserStatisticsUpdate public class ScoreBasedUserStatisticsUpdate
{ {
/// <summary> /// <summary>
/// The score set by the user that triggered the update. /// The score set by the user that triggered the update.
@ -27,12 +27,12 @@ namespace osu.Game.Online
public UserStatistics After { get; } public UserStatistics After { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="UserStatisticsUpdate"/>. /// Creates a new <see cref="ScoreBasedUserStatisticsUpdate"/>.
/// </summary> /// </summary>
/// <param name="score">The score set by the user that triggered the update.</param> /// <param name="score">The score set by the user that triggered the update.</param>
/// <param name="before">The user's profile statistics prior to the score being set.</param> /// <param name="before">The user's profile statistics prior to the score being set.</param>
/// <param name="after">The user's profile statistics after the score was set.</param> /// <param name="after">The user's profile statistics after the score was set.</param>
public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
{ {
Score = score; Score = score;
Before = before; Before = before;

View File

@ -2,18 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online namespace osu.Game.Online
{ {
@ -22,8 +18,10 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public partial class UserStatisticsWatcher : Component public partial class UserStatisticsWatcher : Component
{ {
public IBindable<UserStatisticsUpdate?> LatestUpdate => latestUpdate; private readonly LocalUserStatisticsProvider statisticsProvider;
private readonly Bindable<UserStatisticsUpdate?> latestUpdate = new Bindable<UserStatisticsUpdate?>();
public IBindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<ScoreBasedUserStatisticsUpdate?> latestUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } = null!; private SpectatorClient spectatorClient { get; set; } = null!;
@ -33,13 +31,15 @@ namespace osu.Game.Online
private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>(); private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>();
private Dictionary<string, UserStatistics>? latestStatistics; public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
spectatorClient.OnUserScoreProcessed += userScoreProcessed; spectatorClient.OnUserScoreProcessed += userScoreProcessed;
} }
@ -61,35 +61,6 @@ namespace osu.Game.Online
}); });
} }
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
latestStatistics = null;
if (localUser == null || localUser.OnlineID <= 1)
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += initialiseUserStatistics;
api.Queue(userRequest);
});
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
{
var user = response.Users.SingleOrDefault();
// possible if the user is restricted or similar.
if (user == null)
return;
latestStatistics = new Dictionary<string, UserStatistics>();
if (user.RulesetsStatistics != null)
{
foreach (var rulesetStats in user.RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
}
});
private void userScoreProcessed(int userId, long scoreId) private void userScoreProcessed(int userId, long scoreId)
{ {
if (userId != api.LocalUser.Value?.OnlineID) if (userId != api.LocalUser.Value?.OnlineID)
@ -98,30 +69,11 @@ namespace osu.Game.Online
if (!watchedScores.Remove(scoreId, out var scoreInfo)) if (!watchedScores.Remove(scoreId, out var scoreInfo))
return; return;
requestStatisticsUpdate(userId, scoreInfo); statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() =>
} {
if (u.OldStatistics != null)
private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics);
{ }));
var request = new GetUserRequest(userId, scoreInfo.Ruleset);
request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics));
api.Queue(request);
}
private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics)
{
string rulesetName = scoreInfo.Ruleset.ShortName;
api.UpdateStatistics(updatedStatistics);
if (latestStatistics == null)
return;
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
latestRulesetStatistics ??= new UserStatistics();
latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
latestStatistics[rulesetName] = updatedStatistics;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -148,8 +148,7 @@ namespace osu.Game
[Resolved] [Resolved]
private FrameworkConfigManager frameworkConfig { get; set; } private FrameworkConfigManager frameworkConfig { get; set; }
[Cached] private DifficultyRecommender difficultyRecommender;
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
[Cached] [Cached]
private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); private readonly LegacyImportManager legacyImportManager = new LegacyImportManager();
@ -1069,7 +1068,11 @@ namespace osu.Game
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
}); });
loadComponentSingleFile(new UserStatisticsWatcher(), Add, true); LocalUserStatisticsProvider statisticsProvider;
loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true);
loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true);
loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true);
loadComponentSingleFile(Toolbar = new Toolbar loadComponentSingleFile(Toolbar = new Toolbar
{ {
OnHome = delegate OnHome = delegate
@ -1139,7 +1142,6 @@ namespace osu.Game
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler()); Add(new MusicKeyBindingHandler());
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar
{ {
public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable
{ {
public Bindable<UserStatisticsUpdate?> LatestUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private Statistic<int> globalRank = null!; private Statistic<int> globalRank = null!;
private Statistic<int> pp = null!; private Statistic<int> pp = null!;
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar
}; };
if (userStatisticsWatcher != null) if (userStatisticsWatcher != null)
((IBindable<UserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); ((IBindable<ScoreBasedUserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{ {
private const float transition_duration = 300; private const float transition_duration = 300;
public Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private LoadingLayer loadingLayer = null!; private LoadingLayer loadingLayer = null!;
private GridContainer content = null!; private GridContainer content = null!;
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
FinishTransforms(true); FinishTransforms(true);
} }
private void onUpdateReceived(ValueChangedEvent<UserStatisticsUpdate?> update) private void onUpdateReceived(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> update)
{ {
if (update.NewValue == null) if (update.NewValue == null)
{ {

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{ {
public abstract partial class RankingChangeRow<T> : CompositeDrawable public abstract partial class RankingChangeRow<T> : CompositeDrawable
{ {
public Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private readonly Func<UserStatistics, T> accessor; private readonly Func<UserStatistics, T> accessor;
@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true); StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true);
} }
private void onStatisticsUpdate(ValueChangedEvent<UserStatisticsUpdate?> statisticsUpdate) private void onStatisticsUpdate(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> statisticsUpdate)
{ {
var update = statisticsUpdate.NewValue; var update = statisticsUpdate.NewValue;

View File

@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics
{ {
private readonly ScoreInfo achievedScore; private readonly ScoreInfo achievedScore;
internal readonly Bindable<UserStatisticsUpdate?> DisplayedUserStatisticsUpdate = new Bindable<UserStatisticsUpdate?>(); internal readonly Bindable<ScoreBasedUserStatisticsUpdate?> DisplayedUserStatisticsUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private IBindable<UserStatisticsUpdate?> latestGlobalStatisticsUpdate = null!; private IBindable<ScoreBasedUserStatisticsUpdate?> latestGlobalStatisticsUpdate = null!;
public UserStatisticsPanel(ScoreInfo achievedScore) public UserStatisticsPanel(ScoreInfo achievedScore)
{ {

View File

@ -4,13 +4,16 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Online.API; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osuTK; using osuTK;
namespace osu.Game.Users namespace osu.Game.Users
@ -24,13 +27,9 @@ namespace osu.Game.Users
private const int padding = 10; private const int padding = 10;
private const int main_content_height = 80; private const int main_content_height = 80;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private ProfileValueDisplay globalRankDisplay = null!; private ProfileValueDisplay globalRankDisplay = null!;
private ProfileValueDisplay countryRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!;
private LoadingLayer loadingLayer = null!;
private readonly IBindable<UserStatistics?> statistics = new Bindable<UserStatistics?>();
public UserRankPanel(APIUser user) public UserRankPanel(APIUser user)
: base(user) : base(user)
@ -43,13 +42,37 @@ namespace osu.Game.Users
private void load() private void load()
{ {
BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter;
}
statistics.BindTo(api.Statistics); [Resolved]
statistics.BindValueChanged(stats => private LocalUserStatisticsProvider? statisticsProvider { get; set; }
{
globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; [Resolved]
countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
}, true);
protected override void LoadComplete()
{
base.LoadComplete();
if (statisticsProvider != null)
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
ruleset.BindValueChanged(_ => updateDisplay(), true);
}
private void onStatisticsUpdated(UserStatisticsUpdate update)
{
if (update.Ruleset.Equals(ruleset.Value))
updateDisplay();
}
private void updateDisplay()
{
var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value);
loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden;
globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-";
countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-";
} }
protected override Drawable CreateLayout() protected override Drawable CreateLayout()
@ -176,7 +199,8 @@ namespace osu.Game.Users
} }
} }
} }
} },
loadingLayer = new LoadingLayer(true),
} }
}; };
@ -205,5 +229,13 @@ namespace osu.Game.Users
} }
protected override Drawable? CreateBackground() => null; protected override Drawable? CreateBackground() => null;
protected override void Dispose(bool isDisposing)
{
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
base.Dispose(isDisposing);
}
} }
} }