2021-03-23 14:00:02 +08: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.
2022-06-17 15:37:17 +08:00
#nullable disable
2021-03-23 15:41:36 +08:00
using System ;
2021-05-11 10:24:55 +08:00
using System.Linq ;
2021-03-23 14:00:02 +08:00
using System.Threading.Tasks ;
2021-03-24 12:17:13 +08:00
using JetBrains.Annotations ;
2021-03-23 14:00:02 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Logging ;
using osu.Framework.Screens ;
2022-07-13 15:36:55 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Database ;
2021-03-23 14:00:02 +08:00
using osu.Game.Online.API ;
using osu.Game.Online.Rooms ;
2022-07-28 19:44:02 +08:00
using osu.Game.Online.Spectator ;
2021-07-04 14:14:25 +08:00
using osu.Game.Rulesets.Scoring ;
2021-03-23 14:00:02 +08:00
using osu.Game.Scoring ;
namespace osu.Game.Screens.Play
{
2021-03-23 14:33:31 +08:00
/// <summary>
/// A player instance which supports submitting scores to an online store.
/// </summary>
2021-03-23 14:00:02 +08:00
public abstract class SubmittingPlayer : Player
{
2021-03-24 12:02:17 +08:00
/// <summary>
2021-03-24 12:02:37 +08:00
/// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequest"/>.
2021-03-24 12:02:17 +08:00
/// </summary>
2021-03-24 12:20:44 +08:00
private long? token ;
2021-03-23 14:00:02 +08:00
[Resolved]
private IAPIProvider api { get ; set ; }
2022-07-28 19:44:02 +08:00
[Resolved]
private SpectatorClient spectatorClient { get ; set ; }
2021-07-01 16:02:33 +08:00
private TaskCompletionSource < bool > scoreSubmissionSource ;
2021-03-23 14:35:06 +08:00
protected SubmittingPlayer ( PlayerConfiguration configuration = null )
2021-03-23 14:00:02 +08:00
: base ( configuration )
{
}
2021-03-23 15:05:40 +08:00
protected override void LoadAsyncComplete ( )
2021-03-25 12:48:41 +08:00
{
base . LoadAsyncComplete ( ) ;
2021-05-11 10:24:35 +08:00
handleTokenRetrieval ( ) ;
2021-03-25 12:48:41 +08:00
}
private bool handleTokenRetrieval ( )
2021-03-23 14:00:02 +08:00
{
2021-03-23 15:05:40 +08:00
// Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
2021-03-23 15:41:36 +08:00
var tcs = new TaskCompletionSource < bool > ( ) ;
2021-03-23 14:00:02 +08:00
2021-06-09 13:32:48 +08:00
if ( Mods . Value . Any ( m = > ! m . UserPlayable ) )
2021-05-11 10:24:55 +08:00
{
2021-06-09 13:32:48 +08:00
handleTokenFailure ( new InvalidOperationException ( "Non-user playable mod selected." ) ) ;
2021-05-11 10:24:55 +08:00
return false ;
}
2021-03-23 15:41:36 +08:00
if ( ! api . IsLoggedIn )
2021-03-23 14:00:02 +08:00
{
2021-03-24 12:20:44 +08:00
handleTokenFailure ( new InvalidOperationException ( "API is not online." ) ) ;
2021-03-25 12:48:41 +08:00
return false ;
2021-03-23 15:41:36 +08:00
}
2021-03-23 14:00:02 +08:00
2021-03-24 12:02:37 +08:00
var req = CreateTokenRequest ( ) ;
2021-03-23 14:00:02 +08:00
2021-03-23 15:41:36 +08:00
if ( req = = null )
{
2021-03-24 12:20:44 +08:00
handleTokenFailure ( new InvalidOperationException ( "Request could not be constructed." ) ) ;
2021-03-25 12:48:41 +08:00
return false ;
2021-03-23 15:41:36 +08:00
}
req . Success + = r = >
{
2022-10-03 16:22:30 +08:00
Logger . Log ( $"Score submission token retrieved ({r.ID})" ) ;
2021-03-24 12:20:44 +08:00
token = r . ID ;
2021-03-23 15:41:36 +08:00
tcs . SetResult ( true ) ;
2021-03-23 14:00:02 +08:00
} ;
2021-03-24 12:20:44 +08:00
req . Failure + = handleTokenFailure ;
2021-03-23 14:00:02 +08:00
api . Queue ( req ) ;
2022-08-20 16:19:17 +08:00
// Generally a timeout would not happen here as APIAccess will timeout first.
if ( ! tcs . Task . Wait ( 60000 ) )
handleTokenFailure ( new InvalidOperationException ( "Token retrieval timed out (request never run)" ) ) ;
2021-03-25 12:48:41 +08:00
return true ;
2021-03-24 12:20:44 +08:00
void handleTokenFailure ( Exception exception )
2021-03-23 15:41:36 +08:00
{
2022-10-03 16:29:46 +08:00
// This method may be invoked multiple times due to the Task.Wait call above.
// We only really care about the first error.
if ( ! tcs . TrySetResult ( false ) )
return ;
2021-03-23 15:41:36 +08:00
if ( HandleTokenRetrievalFailure ( exception ) )
{
if ( string . IsNullOrEmpty ( exception . Message ) )
Logger . Error ( exception , "Failed to retrieve a score submission token." ) ;
else
Logger . Log ( $"You are not able to submit a score: {exception.Message}" , level : LogLevel . Important ) ;
Schedule ( ( ) = >
{
ValidForResume = false ;
this . Exit ( ) ;
} ) ;
}
2022-10-03 16:22:30 +08:00
else
{
// Gameplay is allowed to continue, but we still should keep track of the error.
// In the future, this should be visible to the user in some way.
Logger . Log ( $"Score submission token retrieval failed ({exception.Message})" ) ;
}
2021-03-23 15:41:36 +08:00
}
2021-03-23 14:00:02 +08:00
}
2021-03-23 15:41:36 +08:00
/// <summary>
/// Called when a token could not be retrieved for submission.
/// </summary>
/// <param name="exception">The error causing the failure.</param>
/// <returns>Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.</returns>
protected virtual bool HandleTokenRetrievalFailure ( Exception exception ) = > true ;
2021-07-01 16:02:33 +08:00
protected override async Task PrepareScoreForResultsAsync ( Score score )
{
await base . PrepareScoreForResultsAsync ( score ) . ConfigureAwait ( false ) ;
2021-07-19 18:28:35 +08:00
score . ScoreInfo . Date = DateTimeOffset . Now ;
2021-07-01 16:02:33 +08:00
await submitScore ( score ) . ConfigureAwait ( false ) ;
}
2022-07-13 15:36:55 +08:00
[Resolved]
private RealmAccess realm { get ; set ; }
protected override void StartGameplay ( )
{
base . StartGameplay ( ) ;
// User expectation is that last played should be updated when entering the gameplay loop
2022-07-13 21:37:20 +08:00
// from multiplayer / playlists / solo.
2022-07-13 15:36:55 +08:00
realm . WriteAsync ( r = >
{
var realmBeatmap = r . Find < BeatmapInfo > ( Beatmap . Value . BeatmapInfo . ID ) ;
if ( realmBeatmap ! = null )
realmBeatmap . LastPlayed = DateTimeOffset . Now ;
} ) ;
2022-07-28 19:44:02 +08:00
spectatorClient . BeginPlaying ( GameplayState , Score ) ;
2022-07-13 15:36:55 +08:00
}
2022-04-21 23:52:44 +08:00
public override bool OnExiting ( ScreenExitEvent e )
2021-06-30 19:23:48 +08:00
{
2022-04-21 23:52:44 +08:00
bool exiting = base . OnExiting ( e ) ;
2021-07-01 16:38:28 +08:00
2022-03-09 16:50:05 +08:00
if ( LoadedBeatmapSuccessfully )
2022-07-28 19:44:02 +08:00
{
2022-03-09 16:50:05 +08:00
submitScore ( Score . DeepClone ( ) ) ;
2022-07-28 19:44:02 +08:00
spectatorClient . EndPlaying ( GameplayState ) ;
}
2021-07-01 16:02:33 +08:00
2021-07-01 16:38:28 +08:00
return exiting ;
2021-06-30 19:23:48 +08:00
}
2021-07-01 16:02:33 +08:00
/// <summary>
/// Construct a request to be used for retrieval of the score token.
/// Can return null, at which point <see cref="HandleTokenRetrievalFailure"/> will be fired.
/// </summary>
[CanBeNull]
protected abstract APIRequest < APIScoreToken > CreateTokenRequest ( ) ;
2021-03-23 14:00:02 +08:00
2021-07-01 16:02:33 +08:00
/// <summary>
/// Construct a request to submit the score.
/// Will only be invoked if the request constructed via <see cref="CreateTokenRequest"/> was successful.
/// </summary>
/// <param name="score">The score to be submitted.</param>
/// <param name="token">The submission token.</param>
protected abstract APIRequest < MultiplayerScore > CreateSubmissionRequest ( Score score , long token ) ;
private Task submitScore ( Score score )
{
2021-03-23 15:41:36 +08:00
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
2021-03-24 12:20:44 +08:00
if ( token = = null )
2021-07-01 16:02:33 +08:00
return Task . CompletedTask ;
2021-03-23 14:00:02 +08:00
2021-07-01 16:02:33 +08:00
if ( scoreSubmissionSource ! = null )
return scoreSubmissionSource . Task ;
2021-07-04 14:14:25 +08:00
// if the user never hit anything, this score should not be counted in any way.
2021-07-04 15:41:09 +08:00
if ( ! score . ScoreInfo . Statistics . Any ( s = > s . Key . IsHit ( ) & & s . Value > 0 ) )
2021-07-04 14:14:25 +08:00
return Task . CompletedTask ;
2021-07-01 16:02:33 +08:00
scoreSubmissionSource = new TaskCompletionSource < bool > ( ) ;
2021-03-24 12:20:44 +08:00
var request = CreateSubmissionRequest ( score , token . Value ) ;
2021-03-23 14:00:02 +08:00
request . Success + = s = >
{
2021-12-10 14:37:12 +08:00
score . ScoreInfo . OnlineID = s . ID ;
2021-12-08 14:22:02 +08:00
score . ScoreInfo . Position = s . Position ;
2021-07-01 16:02:33 +08:00
scoreSubmissionSource . SetResult ( true ) ;
2021-03-23 14:00:02 +08:00
} ;
request . Failure + = e = >
{
2022-07-12 14:10:59 +08:00
Logger . Error ( e , $"Failed to submit score ({e.Message})" ) ;
2021-07-01 16:02:33 +08:00
scoreSubmissionSource . SetResult ( false ) ;
2021-03-23 14:00:02 +08:00
} ;
api . Queue ( request ) ;
2021-07-01 16:02:33 +08:00
return scoreSubmissionSource . Task ;
2021-03-23 14:00:02 +08:00
}
}
}