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-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-23 16:24:19 +08:00
using osu.Game.Rulesets ;
2020-10-22 16:29:38 +08:00
using osu.Game.Rulesets.Mods ;
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 > ( ) ;
2020-12-02 18:02:49 +08:00
private readonly object userLock = new object ( ) ;
2020-10-22 17:37:19 +08:00
public IBindableList < int > PlayingUsers = > playingUsers ;
private readonly BindableList < int > playingUsers = new BindableList < int > ( ) ;
2020-10-21 18:05:20 +08:00
2021-04-19 15:07:00 +08:00
private readonly Dictionary < int , SpectatorState > playingUserStates = new Dictionary < int , SpectatorState > ( ) ;
2021-04-19 15:06:40 +08:00
2021-05-20 16:51:09 +08:00
private IBeatmap ? currentBeatmap ;
2020-10-22 13:54:27 +08:00
2021-05-20 16:51:09 +08:00
private Score ? currentScore ;
2020-12-14 16:33:23 +08:00
2020-10-23 16:24:19 +08:00
[Resolved]
2021-05-20 16:51:09 +08:00
private IBindable < RulesetInfo > currentRuleset { get ; set ; } = null ! ;
2020-10-23 16:24:19 +08:00
2020-10-22 16:29:38 +08:00
[Resolved]
2021-05-20 16:51:09 +08:00
private IBindable < IReadOnlyList < Mod > > currentMods { get ; set ; } = null ! ;
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 15:30:56 +08:00
IsConnected . BindValueChanged ( connected = >
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
int [ ] users ;
lock ( userLock )
2020-10-22 14:27:04 +08:00
{
2021-05-20 15:30:56 +08:00
users = watchingUsers . ToArray ( ) ;
watchingUsers . Clear ( ) ;
2021-02-09 07:15:51 +08:00
}
2021-05-20 15:30:56 +08:00
// resubscribe to watched users.
foreach ( var userId in users )
WatchUser ( userId ) ;
// re-send state in case it wasn't received
if ( IsPlaying )
BeginPlayingInternal ( currentState ) ;
}
else
{
lock ( userLock )
2021-02-11 17:32:54 +08:00
{
2021-05-20 15:30:56 +08:00
playingUsers . Clear ( ) ;
playingUserStates . Clear ( ) ;
2021-02-11 17:32:54 +08:00
}
2021-05-20 15:30:56 +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-04-16 13:11:55 +08:00
lock ( userLock )
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-19 15:06:40 +08:00
}
2021-04-16 13:11:55 +08:00
2021-04-16 16:29:42 +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-04-16 13:11:55 +08:00
lock ( userLock )
2021-04-19 15:06:40 +08:00
{
playingUsers . Remove ( userId ) ;
2021-04-19 15:07:00 +08:00
playingUserStates . Remove ( userId ) ;
2021-04-19 15:06:40 +08:00
}
2021-04-16 13:11:55 +08:00
2020-10-26 19:05:11 +08:00
OnUserFinishedPlaying ? . Invoke ( userId , state ) ;
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
{
2020-10-22 17:37:19 +08:00
OnNewFrames ? . Invoke ( userId , data ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-12-14 16:33:23 +08:00
public void BeginPlaying ( GameplayBeatmap beatmap , Score score )
2020-10-22 14:27:04 +08:00
{
2021-05-20 15:30:56 +08:00
if ( IsPlaying )
2020-10-22 17:37:19 +08:00
throw new InvalidOperationException ( $"Cannot invoke {nameof(BeginPlaying)} when already playing" ) ;
2021-05-20 15:30:56 +08:00
IsPlaying = true ;
2020-10-22 17:37:19 +08:00
2020-10-22 16:29:43 +08:00
// transfer state at point of beginning play
2020-10-27 07:05:03 +08:00
currentState . BeatmapID = beatmap . BeatmapInfo . OnlineBeatmapID ;
2020-10-27 09:59:24 +08:00
currentState . RulesetID = currentRuleset . Value . ID ;
currentState . Mods = currentMods . Value . Select ( m = > new APIMod ( m ) ) ;
2020-10-22 16:29:43 +08:00
2020-10-27 07:05:03 +08:00
currentBeatmap = beatmap . PlayableBeatmap ;
2020-12-14 16:33:23 +08:00
currentScore = score ;
2021-05-20 15:30:56 +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-05-20 15:30:56 +08:00
IsPlaying = false ;
2020-10-27 07:05:03 +08:00
currentBeatmap = null ;
2020-10-22 21:56:23 +08:00
2021-05-20 15:30:56 +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
{
2020-12-02 18:02:49 +08:00
lock ( userLock )
{
if ( watchingUsers . Contains ( userId ) )
return ;
2020-10-22 17:37:19 +08:00
2020-12-02 18:02:49 +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
{
2020-12-02 18:02:49 +08:00
lock ( userLock )
{
watchingUsers . Remove ( userId ) ;
}
2020-10-22 18:17:19 +08:00
2021-05-20 15:30:56 +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 )
{
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
}
2021-04-16 16:29:42 +08:00
2021-04-19 15:48:55 +08:00
/// <summary>
/// Attempts to retrieve the <see cref="SpectatorState"/> for a currently-playing user.
/// </summary>
/// <param name="userId">The user.</param>
/// <param name="state">The current <see cref="SpectatorState"/> for the user, if they're playing. <c>null</c> if the user is not playing.</param>
/// <returns><c>true</c> if successful (the user is playing), <c>false</c> otherwise.</returns>
public bool TryGetPlayingUserState ( int userId , out SpectatorState state )
{
lock ( userLock )
return playingUserStates . TryGetValue ( userId , out state ) ;
}
2021-04-16 16:29:42 +08:00
/// <summary>
/// Bind an action to <see cref="OnUserBeganPlaying"/> with the option of running the bound action once immediately.
/// </summary>
/// <param name="callback">The action to perform when a user begins playing.</param>
/// <param name="runOnceImmediately">Whether the action provided in <paramref name="callback"/> should be run once immediately for all users currently playing.</param>
public void BindUserBeganPlaying ( Action < int , SpectatorState > callback , bool runOnceImmediately = false )
{
2021-04-20 20:20:08 +08:00
// The lock is taken before the event is subscribed to to prevent doubling of events.
2021-04-16 16:29:42 +08:00
lock ( userLock )
{
2021-04-20 20:20:08 +08:00
OnUserBeganPlaying + = callback ;
if ( ! runOnceImmediately )
return ;
2021-04-19 15:07:00 +08:00
foreach ( var ( userId , state ) in playingUserStates )
2021-04-16 16:29:42 +08:00
callback ( userId , state ) ;
}
}
2020-10-21 18:05:20 +08:00
}
}