mirror of
https://github.com/ppy/osu.git
synced 2024-09-21 16:47:29 +08:00
Merge pull request #27156 from bdach/score-statistics-updates-working-2
Watch online statistics changes after every play & display them in toolbar
This commit is contained in:
commit
ebea0d283e
@ -2,12 +2,17 @@
|
|||||||
// 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 NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Solo;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -87,5 +92,91 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,6 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
private Action<GetUsersRequest>? handleGetUsersRequest;
|
private Action<GetUsersRequest>? handleGetUsersRequest;
|
||||||
private Action<GetUserRequest>? handleGetUserRequest;
|
private Action<GetUserRequest>? handleGetUserRequest;
|
||||||
|
|
||||||
private IDisposable? subscription;
|
|
||||||
|
|
||||||
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
|
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
@ -252,26 +250,6 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
|
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]
|
[Test]
|
||||||
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
|
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
|
||||||
{
|
{
|
||||||
@ -312,13 +290,20 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
||||||
AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
|
AddStep("register for updates", () =>
|
||||||
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
{
|
||||||
|
watcher.RegisterForStatisticsUpdateAfter(
|
||||||
|
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
||||||
|
{
|
||||||
|
Ruleset = rulesetInfo,
|
||||||
|
OnlineID = scoreId
|
||||||
|
});
|
||||||
|
watcher.LatestUpdate.BindValueChanged(update =>
|
||||||
{
|
{
|
||||||
Ruleset = rulesetInfo,
|
if (update.NewValue?.Score.OnlineID == scoreId)
|
||||||
OnlineID = scoreId
|
onUpdateReady.Invoke(update.NewValue);
|
||||||
},
|
});
|
||||||
onUpdateReady));
|
});
|
||||||
|
|
||||||
private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
|
private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
|
||||||
=> AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
|
=> AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
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;
|
||||||
@ -22,14 +22,16 @@ namespace osu.Game.Online.Solo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SoloStatisticsWatcher : Component
|
public partial class SoloStatisticsWatcher : Component
|
||||||
{
|
{
|
||||||
|
public IBindable<SoloStatisticsUpdate?> LatestUpdate => latestUpdate;
|
||||||
|
private readonly Bindable<SoloStatisticsUpdate?> latestUpdate = new Bindable<SoloStatisticsUpdate?>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; } = null!;
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
private readonly Dictionary<long, StatisticsUpdateCallback> callbacks = new Dictionary<long, StatisticsUpdateCallback>();
|
private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>();
|
||||||
private long? lastProcessedScoreId;
|
|
||||||
|
|
||||||
private Dictionary<string, UserStatistics>? latestStatistics;
|
private Dictionary<string, UserStatistics>? latestStatistics;
|
||||||
|
|
||||||
@ -45,9 +47,7 @@ namespace osu.Game.Online.Solo
|
|||||||
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
|
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to listen for the statistics update for.</param>
|
/// <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>
|
public void RegisterForStatisticsUpdateAfter(ScoreInfo score)
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
@ -57,24 +57,12 @@ namespace osu.Game.Online.Solo
|
|||||||
if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
|
if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
|
watchedScores.Add(score.OnlineID, score);
|
||||||
|
|
||||||
if (lastProcessedScoreId == score.OnlineID)
|
|
||||||
{
|
|
||||||
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callbacks.Add(score.OnlineID, callback);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onUserChanged(APIUser? localUser) => Schedule(() =>
|
private void onUserChanged(APIUser? localUser) => Schedule(() =>
|
||||||
{
|
{
|
||||||
callbacks.Clear();
|
|
||||||
lastProcessedScoreId = null;
|
|
||||||
latestStatistics = null;
|
latestStatistics = null;
|
||||||
|
|
||||||
if (localUser == null || localUser.OnlineID <= 1)
|
if (localUser == null || localUser.OnlineID <= 1)
|
||||||
@ -107,25 +95,22 @@ namespace osu.Game.Online.Solo
|
|||||||
if (userId != api.LocalUser.Value?.OnlineID)
|
if (userId != api.LocalUser.Value?.OnlineID)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lastProcessedScoreId = scoreId;
|
if (!watchedScores.Remove(scoreId, out var scoreInfo))
|
||||||
|
|
||||||
if (!callbacks.TryGetValue(scoreId, out var callback))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
requestStatisticsUpdate(userId, callback);
|
requestStatisticsUpdate(userId, scoreInfo);
|
||||||
callbacks.Remove(scoreId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback)
|
private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo)
|
||||||
{
|
{
|
||||||
var request = new GetUserRequest(userId, callback.Score.Ruleset);
|
var request = new GetUserRequest(userId, scoreInfo.Ruleset);
|
||||||
request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics));
|
request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics));
|
||||||
api.Queue(request);
|
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);
|
api.UpdateStatistics(updatedStatistics);
|
||||||
|
|
||||||
@ -135,9 +120,7 @@ namespace osu.Game.Online.Solo
|
|||||||
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
|
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
|
||||||
latestRulesetStatistics ??= new UserStatistics();
|
latestRulesetStatistics ??= new UserStatistics();
|
||||||
|
|
||||||
var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics);
|
latestUpdate.Value = new SoloStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
|
||||||
callback.OnUpdateReady.Invoke(update);
|
|
||||||
|
|
||||||
latestStatistics[rulesetName] = updatedStatistics;
|
latestStatistics[rulesetName] = updatedStatistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,17 +131,5 @@ namespace osu.Game.Online.Solo
|
|||||||
|
|
||||||
base.Dispose(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ using osu.Game.Localisation;
|
|||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Online.Solo;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.BeatmapListing;
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
using osu.Game.Overlays.Music;
|
using osu.Game.Overlays.Music;
|
||||||
@ -1021,6 +1022,7 @@ namespace osu.Game
|
|||||||
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
|
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loadComponentSingleFile(new SoloStatisticsWatcher(), Add, true);
|
||||||
loadComponentSingleFile(Toolbar = new Toolbar
|
loadComponentSingleFile(Toolbar = new Toolbar
|
||||||
{
|
{
|
||||||
OnHome = delegate
|
OnHome = delegate
|
||||||
|
@ -50,7 +50,6 @@ using osu.Game.Online.API;
|
|||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osu.Game.Online.Metadata;
|
using osu.Game.Online.Metadata;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Solo;
|
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
@ -207,7 +206,6 @@ namespace osu.Game
|
|||||||
protected MultiplayerClient MultiplayerClient { get; private set; }
|
protected MultiplayerClient MultiplayerClient { get; private set; }
|
||||||
|
|
||||||
private MetadataClient metadataClient;
|
private MetadataClient metadataClient;
|
||||||
private SoloStatisticsWatcher soloStatisticsWatcher;
|
|
||||||
|
|
||||||
private RealmAccess realm;
|
private RealmAccess realm;
|
||||||
|
|
||||||
@ -328,7 +326,6 @@ namespace osu.Game
|
|||||||
dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
|
dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
|
||||||
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
|
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
|
||||||
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
||||||
dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
|
|
||||||
|
|
||||||
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
|
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
|
||||||
|
|
||||||
@ -371,7 +368,6 @@ namespace osu.Game
|
|||||||
base.Content.Add(SpectatorClient);
|
base.Content.Add(SpectatorClient);
|
||||||
base.Content.Add(MultiplayerClient);
|
base.Content.Add(MultiplayerClient);
|
||||||
base.Content.Add(metadataClient);
|
base.Content.Add(metadataClient);
|
||||||
base.Content.Add(soloStatisticsWatcher);
|
|
||||||
|
|
||||||
base.Content.Add(rulesetConfigCache);
|
base.Content.Add(rulesetConfigCache);
|
||||||
|
|
||||||
|
@ -78,6 +78,13 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Flow.Add(new TransientUserStatisticsUpdateDisplay
|
||||||
|
{
|
||||||
|
Alpha = 0
|
||||||
|
});
|
||||||
|
Flow.AutoSizeEasing = Easing.OutQuint;
|
||||||
|
Flow.AutoSizeDuration = 250;
|
||||||
|
|
||||||
apiState = api.State.GetBoundCopy();
|
apiState = api.State.GetBoundCopy();
|
||||||
apiState.BindValueChanged(onlineStateChanged, true);
|
apiState.BindValueChanged(onlineStateChanged, true);
|
||||||
|
|
||||||
|
@ -0,0 +1,235 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.Solo;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.Toolbar
|
||||||
|
{
|
||||||
|
public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable
|
||||||
|
{
|
||||||
|
public Bindable<SoloStatisticsUpdate?> LatestUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
|
||||||
|
|
||||||
|
private Statistic<int> globalRank = null!;
|
||||||
|
private Statistic<decimal> pp = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(SoloStatisticsWatcher? soloStatisticsWatcher)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
AutoSizeAxes = Axes.X;
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Padding = new MarginPadding { Horizontal = 10 },
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
globalRank = new Statistic<int>(UsersStrings.ShowRankGlobalSimple, @"#", Comparer<int>.Create((before, after) => before - after)),
|
||||||
|
pp = new Statistic<decimal>(RankingsStrings.StatPerformance, string.Empty, Comparer<decimal>.Create((before, after) => Math.Sign(after - before))),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (soloStatisticsWatcher != null)
|
||||||
|
((IBindable<SoloStatisticsUpdate?>)LatestUpdate).BindTo(soloStatisticsWatcher.LatestUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
LatestUpdate.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
if (val.NewValue == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var update = val.NewValue;
|
||||||
|
|
||||||
|
// null handling here is best effort because it is annoying.
|
||||||
|
|
||||||
|
globalRank.Alpha = update.After.GlobalRank == null ? 0 : 1;
|
||||||
|
pp.Alpha = update.After.PP == null ? 0 : 1;
|
||||||
|
|
||||||
|
if (globalRank.Alpha == 0 && pp.Alpha == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FinishTransforms(true);
|
||||||
|
|
||||||
|
this.FadeIn(500, Easing.OutQuint);
|
||||||
|
|
||||||
|
if (update.After.GlobalRank != null)
|
||||||
|
{
|
||||||
|
globalRank.Display(
|
||||||
|
update.Before.GlobalRank ?? update.After.GlobalRank.Value,
|
||||||
|
Math.Abs((update.After.GlobalRank.Value - update.Before.GlobalRank) ?? 0),
|
||||||
|
update.After.GlobalRank.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.After.PP != null)
|
||||||
|
pp.Display(update.Before.PP ?? update.After.PP.Value, Math.Abs((update.After.PP - update.Before.PP) ?? 0M), update.After.PP.Value);
|
||||||
|
|
||||||
|
this.Delay(5000).FadeOut(500, Easing.OutQuint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class Statistic<T> : CompositeDrawable
|
||||||
|
where T : struct, IEquatable<T>, IFormattable
|
||||||
|
{
|
||||||
|
private readonly LocalisableString title;
|
||||||
|
private readonly string mainValuePrefix;
|
||||||
|
private readonly IComparer<T> valueComparer;
|
||||||
|
|
||||||
|
private Counter<T> mainValue = null!;
|
||||||
|
private Counter<T> deltaValue = null!;
|
||||||
|
private OsuSpriteText titleText = null!;
|
||||||
|
private ScheduledDelegate? valueUpdateSchedule;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
public Statistic(LocalisableString title, string mainValuePrefix, IComparer<T> valueComparer)
|
||||||
|
{
|
||||||
|
this.title = title;
|
||||||
|
this.mainValuePrefix = mainValuePrefix;
|
||||||
|
this.valueComparer = valueComparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
AutoSizeAxes = Axes.X;
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
mainValue = new Counter<T>
|
||||||
|
{
|
||||||
|
ValuePrefix = mainValuePrefix,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
deltaValue = new Counter<T>
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Font = OsuFont.Default.With(size: 12, fixedWidth: true, weight: FontWeight.SemiBold),
|
||||||
|
AlwaysPresent = true,
|
||||||
|
},
|
||||||
|
titleText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
|
||||||
|
Text = title,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Display(T before, T delta, T after)
|
||||||
|
{
|
||||||
|
valueUpdateSchedule?.Cancel();
|
||||||
|
valueUpdateSchedule = null;
|
||||||
|
|
||||||
|
int comparison = valueComparer.Compare(before, after);
|
||||||
|
|
||||||
|
if (comparison > 0)
|
||||||
|
{
|
||||||
|
deltaValue.Colour = colours.Lime1;
|
||||||
|
deltaValue.ValuePrefix = "+";
|
||||||
|
}
|
||||||
|
else if (comparison < 0)
|
||||||
|
{
|
||||||
|
deltaValue.Colour = colours.Red1;
|
||||||
|
deltaValue.ValuePrefix = "-";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deltaValue.Colour = Colour4.White;
|
||||||
|
deltaValue.ValuePrefix = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainValue.SetCountWithoutRolling(before);
|
||||||
|
deltaValue.SetCountWithoutRolling(delta);
|
||||||
|
|
||||||
|
titleText.Alpha = 1;
|
||||||
|
deltaValue.Alpha = 0;
|
||||||
|
|
||||||
|
using (BeginDelayedSequence(1200))
|
||||||
|
{
|
||||||
|
titleText.FadeOut(250, Easing.OutQuad);
|
||||||
|
deltaValue.FadeIn(250, Easing.OutQuad);
|
||||||
|
|
||||||
|
using (BeginDelayedSequence(1250))
|
||||||
|
{
|
||||||
|
valueUpdateSchedule = Schedule(() =>
|
||||||
|
{
|
||||||
|
mainValue.Current.Value = after;
|
||||||
|
deltaValue.Current.SetDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class Counter<T> : RollingCounter<T>
|
||||||
|
where T : struct, IEquatable<T>, IFormattable
|
||||||
|
{
|
||||||
|
public FontUsage Font { get; init; } = OsuFont.Default.With(fixedWidth: true);
|
||||||
|
|
||||||
|
public string ValuePrefix
|
||||||
|
{
|
||||||
|
get => valuePrefix;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
valuePrefix = value;
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string valuePrefix = string.Empty;
|
||||||
|
|
||||||
|
protected override LocalisableString FormatCount(T count) => LocalisableString.Format(@"{0}{1:N0}", ValuePrefix, count);
|
||||||
|
|
||||||
|
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(t =>
|
||||||
|
{
|
||||||
|
t.Font = Font;
|
||||||
|
t.Spacing = new Vector2(-1.5f, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override double RollingDuration => 1500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ using osu.Game.Database;
|
|||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Online.Solo;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -42,6 +43,10 @@ namespace osu.Game.Screens.Play
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SessionStatics statics { get; set; }
|
private SessionStatics statics { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private SoloStatisticsWatcher soloStatisticsWatcher { get; set; }
|
||||||
|
|
||||||
private readonly object scoreSubmissionLock = new object();
|
private readonly object scoreSubmissionLock = new object();
|
||||||
private TaskCompletionSource<bool> scoreSubmissionSource;
|
private TaskCompletionSource<bool> scoreSubmissionSource;
|
||||||
|
|
||||||
@ -175,6 +180,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
await submitScore(score).ConfigureAwait(false);
|
await submitScore(score).ConfigureAwait(false);
|
||||||
spectatorClient.EndPlaying(GameplayState);
|
spectatorClient.EndPlaying(GameplayState);
|
||||||
|
soloStatisticsWatcher?.RegisterForStatisticsUpdateAfter(score.ScoreInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
|
@ -41,9 +41,6 @@ namespace osu.Game.Screens.Ranking
|
|||||||
|
|
||||||
public override bool? AllowGlobalTrackControl => true;
|
public override bool? AllowGlobalTrackControl => true;
|
||||||
|
|
||||||
// Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently.
|
|
||||||
public override bool HideOverlaysOnEnter => true;
|
|
||||||
|
|
||||||
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
|
@ -31,10 +31,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; } = null!;
|
private RulesetStore rulesets { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
private IBindable<SoloStatisticsUpdate?> latestUpdate = null!;
|
||||||
private SoloStatisticsWatcher soloStatisticsWatcher { get; set; } = null!;
|
|
||||||
|
|
||||||
private IDisposable? statisticsSubscription;
|
|
||||||
private readonly Bindable<SoloStatisticsUpdate?> statisticsUpdate = new Bindable<SoloStatisticsUpdate?>();
|
private readonly Bindable<SoloStatisticsUpdate?> statisticsUpdate = new Bindable<SoloStatisticsUpdate?>();
|
||||||
|
|
||||||
public SoloResultsScreen(ScoreInfo score, bool allowRetry)
|
public SoloResultsScreen(ScoreInfo score, bool allowRetry)
|
||||||
@ -42,14 +39,20 @@ namespace osu.Game.Screens.Ranking
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(SoloStatisticsWatcher? soloStatisticsWatcher)
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
if (ShowUserStatistics && soloStatisticsWatcher != null)
|
||||||
|
{
|
||||||
|
Debug.Assert(Score != null);
|
||||||
|
|
||||||
Debug.Assert(Score != null);
|
latestUpdate = soloStatisticsWatcher.LatestUpdate.GetBoundCopy();
|
||||||
|
latestUpdate.BindValueChanged(update =>
|
||||||
if (ShowUserStatistics)
|
{
|
||||||
statisticsSubscription = soloStatisticsWatcher.RegisterForStatisticsUpdateAfter(Score, update => statisticsUpdate.Value = update);
|
if (update.NewValue?.Score.MatchesOnlineID(Score) == true)
|
||||||
|
statisticsUpdate.Value = update.NewValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override StatisticsPanel CreateStatisticsPanel()
|
protected override StatisticsPanel CreateStatisticsPanel()
|
||||||
@ -84,7 +87,6 @@ namespace osu.Game.Screens.Ranking
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
getScoreRequest?.Cancel();
|
getScoreRequest?.Cancel();
|
||||||
statisticsSubscription?.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user