2020-10-22 10:41:10 +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.
2021-05-20 08:51:09 +00:00
#nullable enable
2020-10-21 10:05:20 +00:00
using System ;
using System.Collections.Generic ;
2020-10-22 09:37:19 +00:00
using System.Diagnostics ;
2020-10-21 10:05:20 +00:00
using System.Linq ;
using System.Threading.Tasks ;
2020-10-22 04:41:54 +00:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2021-05-21 06:57:31 +00:00
using osu.Framework.Development ;
2021-02-09 04:46:00 +00:00
using osu.Framework.Graphics ;
2020-10-22 05:54:27 +00:00
using osu.Game.Beatmaps ;
2020-10-22 04:41:54 +00:00
using osu.Game.Online.API ;
2020-10-22 10:17:19 +00:00
using osu.Game.Replays.Legacy ;
2020-10-22 05:54:27 +00:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
2020-12-14 08:33:23 +00:00
using osu.Game.Scoring ;
2020-10-26 23:05:03 +00:00
using osu.Game.Screens.Play ;
2020-10-21 10:05:20 +00:00
namespace osu.Game.Online.Spectator
{
2021-05-20 07:30:56 +00:00
public abstract class SpectatorClient : Component , ISpectatorClient
2020-10-21 10:05:20 +00:00
{
2020-10-26 07:31:39 +00:00
/// <summary>
/// The maximum milliseconds between frame bundle sends.
/// </summary>
public const double TIME_BETWEEN_SENDS = 200 ;
2021-05-20 07:30:56 +00:00
/// <summary>
/// Whether the <see cref="SpectatorClient"/> is currently connected.
/// This is NOT thread safe and usage should be scheduled.
/// </summary>
public abstract IBindable < bool > IsConnected { get ; }
2020-10-21 10:05:20 +00:00
2020-10-22 09:37:19 +00:00
private readonly List < int > watchingUsers = new List < int > ( ) ;
public IBindableList < int > PlayingUsers = > playingUsers ;
private readonly BindableList < int > playingUsers = new BindableList < int > ( ) ;
2020-10-21 10:05:20 +00:00
2021-05-20 10:19:39 +00:00
public IBindableDictionary < int , SpectatorState > PlayingUserStates = > playingUserStates ;
private readonly BindableDictionary < int , SpectatorState > playingUserStates = new BindableDictionary < int , SpectatorState > ( ) ;
2021-04-19 07:06:40 +00:00
2021-05-20 08:51:09 +00:00
private IBeatmap ? currentBeatmap ;
private Score ? currentScore ;
2020-12-14 08:33:23 +00:00
2020-10-22 08:29:38 +00:00
private readonly SpectatorState currentState = new SpectatorState ( ) ;
2021-05-20 08:51:09 +00:00
/// <summary>
/// Whether the local user is playing.
/// </summary>
2021-05-20 07:30:56 +00:00
protected bool IsPlaying { get ; private set ; }
2020-10-22 09:37:19 +00:00
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
2021-05-20 08:51:09 +00:00
public event Action < int , FrameDataBundle > ? OnNewFrames ;
2020-10-22 09:10:27 +00:00
2020-10-26 11:05:11 +00:00
/// <summary>
2021-04-16 05:11:55 +00:00
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
2020-10-26 11:05:11 +00:00
/// </summary>
2021-05-20 08:51:09 +00:00
public event Action < int , SpectatorState > ? OnUserBeganPlaying ;
2020-10-26 11:05:11 +00:00
/// <summary>
2020-11-01 13:39:10 +00:00
/// Called whenever a user finishes a play session.
2020-10-26 11:05:11 +00:00
/// </summary>
2021-05-20 08:51:09 +00:00
public event Action < int , SpectatorState > ? OnUserFinishedPlaying ;
2020-10-26 11:05:11 +00:00
2020-10-22 04:41:54 +00:00
[BackgroundDependencyLoader]
2021-05-20 07:30:56 +00:00
private void load ( )
2020-10-21 10:05:20 +00:00
{
2021-05-20 09:37:27 +00:00
IsConnected . BindValueChanged ( connected = > Schedule ( ( ) = >
2020-10-22 04:41:54 +00:00
{
2021-05-20 07:30:56 +00:00
if ( connected . NewValue )
2021-02-11 09:32:54 +00:00
{
2021-05-20 07:30:56 +00:00
// get all the users that were previously being watched
2021-05-20 09:37:27 +00:00
int [ ] users = watchingUsers . ToArray ( ) ;
watchingUsers . Clear ( ) ;
2021-05-20 07:30:56 +00:00
// resubscribe to watched users.
2021-10-27 04:04:41 +00:00
foreach ( int userId in users )
2021-05-20 07:30:56 +00:00
WatchUser ( userId ) ;
// re-send state in case it wasn't received
if ( IsPlaying )
BeginPlayingInternal ( currentState ) ;
}
else
{
2021-05-20 09:37:27 +00:00
playingUsers . Clear ( ) ;
playingUserStates . Clear ( ) ;
2021-05-20 07:30:56 +00:00
}
2021-05-20 09:37:27 +00:00
} ) , true ) ;
2020-10-21 10:05:20 +00:00
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserBeganPlaying ( int userId , SpectatorState state )
2020-10-21 10:05:20 +00:00
{
2021-05-20 09:37:27 +00:00
Schedule ( ( ) = >
2021-04-19 07:06:40 +00:00
{
if ( ! playingUsers . Contains ( userId ) )
playingUsers . Add ( userId ) ;
2021-05-12 04:10:59 +00:00
// UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched.
// This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29).
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
if ( watchingUsers . Contains ( userId ) )
playingUserStates [ userId ] = state ;
2021-04-16 05:11:55 +00:00
2021-05-20 09:37:27 +00:00
OnUserBeganPlaying ? . Invoke ( userId , state ) ;
} ) ;
2020-10-26 11:05:11 +00:00
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserFinishedPlaying ( int userId , SpectatorState state )
2020-10-21 10:05:20 +00:00
{
2021-05-20 09:37:27 +00:00
Schedule ( ( ) = >
2021-04-19 07:06:40 +00:00
{
playingUsers . Remove ( userId ) ;
2021-04-19 07:07:00 +00:00
playingUserStates . Remove ( userId ) ;
2021-04-16 05:11:55 +00:00
2021-05-20 09:37:27 +00:00
OnUserFinishedPlaying ? . Invoke ( userId , state ) ;
} ) ;
2020-10-26 11:05:11 +00:00
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserSentFrames ( int userId , FrameDataBundle data )
2020-10-21 10:05:20 +00:00
{
2021-05-20 09:37:27 +00:00
Schedule ( ( ) = > OnNewFrames ? . Invoke ( userId , data ) ) ;
2020-10-26 11:05:11 +00:00
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2021-10-01 17:22:23 +00:00
public void BeginPlaying ( GameplayState state , Score score )
2020-10-22 06:27:04 +00:00
{
2021-05-21 06:57:31 +00:00
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2021-05-20 07:30:56 +00:00
if ( IsPlaying )
2020-10-22 09:37:19 +00:00
throw new InvalidOperationException ( $"Cannot invoke {nameof(BeginPlaying)} when already playing" ) ;
2021-05-20 07:30:56 +00:00
IsPlaying = true ;
2020-10-22 09:37:19 +00:00
2020-10-22 08:29:43 +00:00
// transfer state at point of beginning play
2021-11-12 08:45:05 +00:00
currentState . BeatmapID = score . ScoreInfo . BeatmapInfo . OnlineID ;
2021-09-14 08:22:58 +00:00
currentState . RulesetID = score . ScoreInfo . RulesetID ;
currentState . Mods = score . ScoreInfo . Mods . Select ( m = > new APIMod ( m ) ) . ToArray ( ) ;
2020-10-22 08:29:43 +00:00
2021-10-01 17:22:23 +00:00
currentBeatmap = state . Beatmap ;
2020-12-14 08:33:23 +00:00
currentScore = score ;
2021-05-20 07:30:56 +00:00
BeginPlayingInternal ( currentState ) ;
2020-10-22 06:27:04 +00:00
}
2020-10-21 10:05:20 +00:00
2021-05-20 07:30:56 +00:00
public void SendFrames ( FrameDataBundle data ) = > lastSend = SendFramesInternal ( data ) ;
2020-10-22 06:27:04 +00:00
2020-10-22 08:29:38 +00:00
public void EndPlaying ( )
2020-10-22 06:27:04 +00:00
{
2021-05-21 06:57:39 +00:00
// This method is most commonly called via Dispose(), which is asynchronous.
// Todo: This should not be a thing, but requires framework changes.
Schedule ( ( ) = >
{
2021-05-21 07:00:58 +00:00
if ( ! IsPlaying )
return ;
2020-10-22 13:56:23 +00:00
2021-05-21 06:57:39 +00:00
IsPlaying = false ;
currentBeatmap = null ;
2020-10-22 13:56:23 +00:00
2021-05-21 06:57:39 +00:00
EndPlayingInternal ( currentState ) ;
} ) ;
2020-10-22 06:27:04 +00:00
}
2021-05-20 07:30:56 +00:00
public void WatchUser ( int userId )
2020-10-22 06:27:04 +00:00
{
2021-05-21 06:57:31 +00:00
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2020-10-22 09:37:19 +00:00
2021-05-20 09:37:27 +00:00
if ( watchingUsers . Contains ( userId ) )
return ;
2020-10-22 09:37:19 +00:00
2021-05-20 09:37:27 +00:00
watchingUsers . Add ( userId ) ;
2020-10-22 10:17:19 +00:00
2021-05-20 07:30:56 +00:00
WatchUserInternal ( userId ) ;
2020-10-22 06:27:04 +00:00
}
2020-10-22 05:54:27 +00:00
2021-05-20 07:30:56 +00:00
public void StopWatchingUser ( int userId )
2020-10-22 10:17:19 +00:00
{
2021-05-20 10:45:11 +00:00
// This method is most commonly called via Dispose(), which is asynchronous.
// Todo: This should not be a thing, but requires framework changes.
Schedule ( ( ) = >
{
watchingUsers . Remove ( userId ) ;
2021-05-20 10:46:26 +00:00
playingUserStates . Remove ( userId ) ;
2021-05-20 10:45:11 +00:00
StopWatchingUserInternal ( userId ) ;
} ) ;
2020-10-22 10:17:19 +00:00
}
2021-05-20 07:30:56 +00:00
protected abstract Task BeginPlayingInternal ( SpectatorState state ) ;
protected abstract Task SendFramesInternal ( FrameDataBundle data ) ;
protected abstract Task EndPlayingInternal ( SpectatorState state ) ;
protected abstract Task WatchUserInternal ( int userId ) ;
protected abstract Task StopWatchingUserInternal ( int userId ) ;
2020-10-22 10:17:19 +00:00
private readonly Queue < LegacyReplayFrame > pendingFrames = new Queue < LegacyReplayFrame > ( ) ;
private double lastSendTime ;
2021-05-20 08:51:09 +00:00
private Task ? lastSend ;
2020-10-22 10:17:19 +00:00
private const int max_pending_frames = 30 ;
protected override void Update ( )
{
base . Update ( ) ;
2020-10-26 07:31:39 +00:00
if ( pendingFrames . Count > 0 & & Time . Current - lastSendTime > TIME_BETWEEN_SENDS )
2020-10-22 10:17:19 +00:00
purgePendingFrames ( ) ;
}
2020-10-22 05:54:27 +00:00
public void HandleFrame ( ReplayFrame frame )
{
2021-05-21 06:57:31 +00:00
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2021-05-31 01:02:02 +00:00
if ( ! IsPlaying )
return ;
2020-10-22 05:54:27 +00:00
if ( frame is IConvertibleReplayFrame convertible )
2020-10-26 23:05:03 +00:00
pendingFrames . Enqueue ( convertible . ToLegacy ( currentBeatmap ) ) ;
2020-10-22 10:17:19 +00:00
if ( pendingFrames . Count > max_pending_frames )
purgePendingFrames ( ) ;
}
private void purgePendingFrames ( )
{
if ( lastSend ? . IsCompleted = = false )
return ;
var frames = pendingFrames . ToArray ( ) ;
pendingFrames . Clear ( ) ;
2020-12-14 08:33:23 +00:00
Debug . Assert ( currentScore ! = null ) ;
SendFrames ( new FrameDataBundle ( currentScore . ScoreInfo , frames ) ) ;
2020-10-22 10:17:19 +00:00
lastSendTime = Time . Current ;
2020-10-22 05:54:27 +00:00
}
2020-10-21 10:05:20 +00:00
}
}