2022-12-22 08:04:53 +00:00
// 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.Extensions.ObjectExtensions ;
using osu.Framework.Graphics ;
2022-12-24 10:28:46 +00:00
using osu.Game.Extensions ;
2022-12-22 08:04:53 +00:00
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.Scoring ;
using osu.Game.Users ;
namespace osu.Game.Online.Solo
{
/// <summary>
/// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
/// </summary>
public partial class SoloStatisticsWatcher : Component
{
[Resolved]
private SpectatorClient spectatorClient { get ; set ; } = null ! ;
[Resolved]
private IAPIProvider api { get ; set ; } = null ! ;
private readonly Dictionary < long , StatisticsUpdateCallback > callbacks = new Dictionary < long , StatisticsUpdateCallback > ( ) ;
2022-12-22 18:29:51 +00:00
private long? lastProcessedScoreId ;
2022-12-22 08:04:53 +00:00
2022-12-24 12:42:32 +00:00
private Dictionary < string , UserStatistics > ? latestStatistics ;
2022-12-22 08:04:53 +00:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
api . LocalUser . BindValueChanged ( user = > onUserChanged ( user . NewValue ) , true ) ;
spectatorClient . OnUserScoreProcessed + = userScoreProcessed ;
}
/// <summary>
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
/// </summary>
/// <param name="score">The score to listen for the statistics update for.</param>
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
2022-12-28 06:50:10 +00:00
/// <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 )
2022-12-22 08:04:53 +00:00
{
2022-12-28 06:50:10 +00:00
Schedule ( ( ) = >
{
if ( ! api . IsLoggedIn )
return ;
2022-12-22 08:04:53 +00:00
2022-12-28 06:50:10 +00:00
if ( ! score . Ruleset . IsLegacyRuleset ( ) | | score . OnlineID < = 0 )
return ;
2022-12-24 10:28:46 +00:00
2022-12-28 06:50:10 +00:00
var callback = new StatisticsUpdateCallback ( score , onUpdateReady ) ;
2022-12-22 08:04:53 +00:00
2022-12-28 06:50:10 +00:00
if ( lastProcessedScoreId = = score . OnlineID )
{
requestStatisticsUpdate ( api . LocalUser . Value . Id , callback ) ;
return ;
}
2022-12-22 08:04:53 +00:00
2022-12-28 06:50:10 +00:00
callbacks . Add ( score . OnlineID , callback ) ;
} ) ;
return new InvokeOnDisposal ( ( ) = > Schedule ( ( ) = > callbacks . Remove ( score . OnlineID ) ) ) ;
}
2022-12-22 08:04:53 +00:00
private void onUserChanged ( APIUser ? localUser ) = > Schedule ( ( ) = >
{
callbacks . Clear ( ) ;
2022-12-22 18:29:51 +00:00
lastProcessedScoreId = null ;
2022-12-24 12:42:32 +00:00
latestStatistics = null ;
2022-12-22 08:04:53 +00:00
2022-12-24 12:28:25 +00:00
if ( localUser = = null | | localUser . OnlineID < = 1 )
2022-12-22 08:04:53 +00:00
return ;
var userRequest = new GetUsersRequest ( new [ ] { localUser . OnlineID } ) ;
2022-12-28 06:28:18 +00:00
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 )
2022-12-22 08:04:53 +00:00
{
2022-12-28 06:28:18 +00:00
foreach ( var rulesetStats in user . RulesetsStatistics )
2022-12-22 08:04:53 +00:00
latestStatistics . Add ( rulesetStats . Key , rulesetStats . Value ) ;
2022-12-28 06:28:18 +00:00
}
2022-12-22 08:04:53 +00:00
} ) ;
private void userScoreProcessed ( int userId , long scoreId )
{
if ( userId ! = api . LocalUser . Value ? . OnlineID )
return ;
2022-12-22 18:29:51 +00:00
lastProcessedScoreId = scoreId ;
2022-12-22 08:04:53 +00:00
if ( ! callbacks . TryGetValue ( scoreId , out var callback ) )
return ;
requestStatisticsUpdate ( userId , callback ) ;
callbacks . Remove ( scoreId ) ;
}
private void requestStatisticsUpdate ( int userId , StatisticsUpdateCallback callback )
{
var request = new GetUserRequest ( userId , callback . Score . Ruleset ) ;
request . Success + = user = > Schedule ( ( ) = > dispatchStatisticsUpdate ( callback , user . Statistics ) ) ;
api . Queue ( request ) ;
}
private void dispatchStatisticsUpdate ( StatisticsUpdateCallback callback , UserStatistics updatedStatistics )
{
string rulesetName = callback . Score . Ruleset . ShortName ;
2024-01-03 08:37:57 +00:00
api . UpdateStatistics ( updatedStatistics ) ;
2022-12-24 12:42:32 +00:00
if ( latestStatistics = = null )
2022-12-22 08:04:53 +00:00
return ;
2022-12-24 12:42:32 +00:00
latestStatistics . TryGetValue ( rulesetName , out UserStatistics ? latestRulesetStatistics ) ;
latestRulesetStatistics ? ? = new UserStatistics ( ) ;
2022-12-22 08:04:53 +00:00
var update = new SoloStatisticsUpdate ( callback . Score , latestRulesetStatistics , updatedStatistics ) ;
callback . OnUpdateReady . Invoke ( update ) ;
latestStatistics [ rulesetName ] = updatedStatistics ;
}
protected override void Dispose ( bool isDisposing )
{
if ( spectatorClient . IsNotNull ( ) )
spectatorClient . OnUserScoreProcessed - = userScoreProcessed ;
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 ;
}
}
}
}