2020-10-22 18:41:10 +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.
2021-05-20 16:51:09 +08:00
#nullable enable
2020-10-21 18:05:20 +08:00
using System ;
using System.Collections.Generic ;
2020-10-22 17:37:19 +08:00
using System.Diagnostics ;
2020-10-21 18:05:20 +08:00
using System.Linq ;
using System.Threading.Tasks ;
2020-10-22 12:41:54 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2021-05-21 14:57:31 +08:00
using osu.Framework.Development ;
2021-02-09 12:46:00 +08:00
using osu.Framework.Graphics ;
2020-10-22 13:54:27 +08:00
using osu.Game.Beatmaps ;
2020-10-22 12:41:54 +08:00
using osu.Game.Online.API ;
2020-10-22 18:17:19 +08:00
using osu.Game.Replays.Legacy ;
2020-10-22 13:54:27 +08:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
2020-12-14 16:33:23 +08:00
using osu.Game.Scoring ;
2020-10-27 07:05:03 +08:00
using osu.Game.Screens.Play ;
2020-10-21 18:05:20 +08:00
namespace osu.Game.Online.Spectator
{
2021-05-20 15:30:56 +08:00
public abstract class SpectatorClient : Component , ISpectatorClient
2020-10-21 18:05:20 +08:00
{
2020-10-26 15:31:39 +08:00
/// <summary>
/// The maximum milliseconds between frame bundle sends.
/// </summary>
public const double TIME_BETWEEN_SENDS = 200 ;
2021-05-20 15:30:56 +08: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 18:05:20 +08:00
2020-10-22 17:37:19 +08: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 18:05:20 +08:00
2021-05-20 18:19:39 +08:00
public IBindableDictionary < int , SpectatorState > PlayingUserStates = > playingUserStates ;
private readonly BindableDictionary < int , SpectatorState > playingUserStates = new BindableDictionary < int , SpectatorState > ( ) ;
2021-04-19 15:06:40 +08:00
2021-05-20 16:51:09 +08:00
private IBeatmap ? currentBeatmap ;
private Score ? currentScore ;
2020-12-14 16:33:23 +08:00
2020-10-22 16:29:38 +08:00
private readonly SpectatorState currentState = new SpectatorState ( ) ;
2021-05-20 16:51:09 +08:00
/// <summary>
/// Whether the local user is playing.
/// </summary>
2021-05-20 15:30:56 +08:00
protected bool IsPlaying { get ; private set ; }
2020-10-22 17:37:19 +08:00
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
2021-05-20 16:51:09 +08:00
public event Action < int , FrameDataBundle > ? OnNewFrames ;
2020-10-22 17:10:27 +08:00
2020-10-26 19:05:11 +08:00
/// <summary>
2021-04-16 13:11:55 +08: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 19:05:11 +08:00
/// </summary>
2021-05-20 16:51:09 +08:00
public event Action < int , SpectatorState > ? OnUserBeganPlaying ;
2020-10-26 19:05:11 +08:00
/// <summary>
2020-11-01 21:39:10 +08:00
/// Called whenever a user finishes a play session.
2020-10-26 19:05:11 +08:00
/// </summary>
2021-05-20 16:51:09 +08:00
public event Action < int , SpectatorState > ? OnUserFinishedPlaying ;
2020-10-26 19:05:11 +08:00
2020-10-22 12:41:54 +08:00
[BackgroundDependencyLoader]
2021-05-20 15:30:56 +08:00
private void load ( )
2020-10-21 18:05:20 +08:00
{
2021-05-20 17:37:27 +08:00
IsConnected . BindValueChanged ( connected = > Schedule ( ( ) = >
2020-10-22 12:41:54 +08:00
{
2021-05-20 15:30:56 +08:00
if ( connected . NewValue )
2021-02-11 17:32:54 +08:00
{
2021-05-20 15:30:56 +08:00
// get all the users that were previously being watched
2021-05-20 17:37:27 +08:00
int [ ] users = watchingUsers . ToArray ( ) ;
watchingUsers . Clear ( ) ;
2021-05-20 15:30:56 +08:00
// resubscribe to watched users.
2021-10-27 12:04:41 +08:00
foreach ( int userId in users )
2021-05-20 15:30:56 +08:00
WatchUser ( userId ) ;
// re-send state in case it wasn't received
if ( IsPlaying )
BeginPlayingInternal ( currentState ) ;
}
else
{
2021-05-20 17:37:27 +08:00
playingUsers . Clear ( ) ;
playingUserStates . Clear ( ) ;
2021-05-20 15:30:56 +08:00
}
2021-05-20 17:37:27 +08:00
} ) , true ) ;
2020-10-21 18:05:20 +08:00
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserBeganPlaying ( int userId , SpectatorState state )
2020-10-21 18:05:20 +08:00
{
2021-05-20 17:37:27 +08:00
Schedule ( ( ) = >
2021-04-19 15:06:40 +08:00
{
if ( ! playingUsers . Contains ( userId ) )
playingUsers . Add ( userId ) ;
2021-05-12 12:10:59 +08: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 13:11:55 +08:00
2021-05-20 17:37:27 +08:00
OnUserBeganPlaying ? . Invoke ( userId , state ) ;
} ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserFinishedPlaying ( int userId , SpectatorState state )
2020-10-21 18:05:20 +08:00
{
2021-05-20 17:37:27 +08:00
Schedule ( ( ) = >
2021-04-19 15:06:40 +08:00
{
playingUsers . Remove ( userId ) ;
2021-04-19 15:07:00 +08:00
playingUserStates . Remove ( userId ) ;
2021-04-16 13:11:55 +08:00
2021-05-20 17:37:27 +08:00
OnUserFinishedPlaying ? . Invoke ( userId , state ) ;
} ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserSentFrames ( int userId , FrameDataBundle data )
2020-10-21 18:05:20 +08:00
{
2021-05-20 17:37:27 +08:00
Schedule ( ( ) = > OnNewFrames ? . Invoke ( userId , data ) ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2021-10-02 01:22:23 +08:00
public void BeginPlaying ( GameplayState state , Score score )
2020-10-22 14:27:04 +08:00
{
2021-12-06 15:34:15 +08:00
// This schedule is only here to match the one below in `EndPlaying`.
Schedule ( ( ) = >
{
if ( IsPlaying )
throw new InvalidOperationException ( $"Cannot invoke {nameof(BeginPlaying)} when already playing" ) ;
2020-10-22 17:37:19 +08:00
2021-12-06 15:34:15 +08:00
IsPlaying = true ;
2020-10-22 17:37:19 +08:00
2021-12-06 15:34:15 +08:00
// transfer state at point of beginning play
currentState . BeatmapID = score . ScoreInfo . BeatmapInfo . OnlineID ;
currentState . RulesetID = score . ScoreInfo . RulesetID ;
currentState . Mods = score . ScoreInfo . Mods . Select ( m = > new APIMod ( m ) ) . ToArray ( ) ;
2020-10-22 16:29:43 +08:00
2021-12-06 15:34:15 +08:00
currentBeatmap = state . Beatmap ;
currentScore = score ;
2020-12-14 16:33:23 +08:00
2021-12-06 15:34:15 +08:00
BeginPlayingInternal ( currentState ) ;
} ) ;
2020-10-22 14:27:04 +08:00
}
2020-10-21 18:05:20 +08:00
2021-05-20 15:30:56 +08:00
public void SendFrames ( FrameDataBundle data ) = > lastSend = SendFramesInternal ( data ) ;
2020-10-22 14:27:04 +08:00
2020-10-22 16:29:38 +08:00
public void EndPlaying ( )
2020-10-22 14:27:04 +08:00
{
2021-12-06 15:34:15 +08:00
// This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue).
// We probably need to find a better way to handle this...
2021-05-21 14:57:39 +08:00
Schedule ( ( ) = >
{
2021-05-21 15:00:58 +08:00
if ( ! IsPlaying )
return ;
2020-10-22 21:56:23 +08:00
2021-05-21 14:57:39 +08:00
IsPlaying = false ;
currentBeatmap = null ;
2020-10-22 21:56:23 +08:00
2021-05-21 14:57:39 +08:00
EndPlayingInternal ( currentState ) ;
} ) ;
2020-10-22 14:27:04 +08:00
}
2021-05-20 15:30:56 +08:00
public void WatchUser ( int userId )
2020-10-22 14:27:04 +08:00
{
2021-05-21 14:57:31 +08:00
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2020-10-22 17:37:19 +08:00
2021-05-20 17:37:27 +08:00
if ( watchingUsers . Contains ( userId ) )
return ;
2020-10-22 17:37:19 +08:00
2021-05-20 17:37:27 +08:00
watchingUsers . Add ( userId ) ;
2020-10-22 18:17:19 +08:00
2021-05-20 15:30:56 +08:00
WatchUserInternal ( userId ) ;
2020-10-22 14:27:04 +08:00
}
2020-10-22 13:54:27 +08:00
2021-05-20 15:30:56 +08:00
public void StopWatchingUser ( int userId )
2020-10-22 18:17:19 +08:00
{
2021-05-20 18:45:11 +08: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 18:46:26 +08:00
playingUserStates . Remove ( userId ) ;
2021-05-20 18:45:11 +08:00
StopWatchingUserInternal ( userId ) ;
} ) ;
2020-10-22 18:17:19 +08:00
}
2021-05-20 15:30:56 +08: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 18:17:19 +08:00
private readonly Queue < LegacyReplayFrame > pendingFrames = new Queue < LegacyReplayFrame > ( ) ;
private double lastSendTime ;
2021-05-20 16:51:09 +08:00
private Task ? lastSend ;
2020-10-22 18:17:19 +08:00
private const int max_pending_frames = 30 ;
protected override void Update ( )
{
base . Update ( ) ;
2020-10-26 15:31:39 +08:00
if ( pendingFrames . Count > 0 & & Time . Current - lastSendTime > TIME_BETWEEN_SENDS )
2020-10-22 18:17:19 +08:00
purgePendingFrames ( ) ;
}
2020-10-22 13:54:27 +08:00
public void HandleFrame ( ReplayFrame frame )
{
2021-05-21 14:57:31 +08:00
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2021-05-31 09:02:02 +08:00
if ( ! IsPlaying )
return ;
2020-10-22 13:54:27 +08:00
if ( frame is IConvertibleReplayFrame convertible )
2020-10-27 07:05:03 +08:00
pendingFrames . Enqueue ( convertible . ToLegacy ( currentBeatmap ) ) ;
2020-10-22 18:17:19 +08: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 16:33:23 +08:00
Debug . Assert ( currentScore ! = null ) ;
SendFrames ( new FrameDataBundle ( currentScore . ScoreInfo , frames ) ) ;
2020-10-22 18:17:19 +08:00
lastSendTime = Time . Current ;
2020-10-22 13:54:27 +08:00
}
2020-10-21 18:05:20 +08:00
}
}