1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-17 03:42:53 +08:00
osu-lazer/osu.Game/Online/Spectator/SpectatorClient.cs

258 lines
8.6 KiB
C#
Raw Normal View History

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
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
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;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
2020-10-22 18:17:19 +08:00
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Online.Spectator
{
public abstract class SpectatorClient : Component, ISpectatorClient
{
/// <summary>
/// The maximum milliseconds between frame bundle sends.
/// </summary>
public const double TIME_BETWEEN_SENDS = 200;
/// <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; }
private readonly List<int> watchingUsers = new List<int>();
public IBindableList<int> PlayingUsers => playingUsers;
private readonly BindableList<int> playingUsers = new BindableList<int>();
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-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>
protected bool IsPlaying { get; private set; }
/// <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>
/// 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
[BackgroundDependencyLoader]
private void load()
{
2021-05-20 17:37:27 +08:00
IsConnected.BindValueChanged(connected => Schedule(() =>
{
if (connected.NewValue)
{
// get all the users that were previously being watched
2021-05-20 17:37:27 +08:00
int[] users = watchingUsers.ToArray();
watchingUsers.Clear();
// resubscribe to watched users.
foreach (int userId in users)
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 17:37:27 +08:00
}), true);
}
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
{
2021-05-20 17:37:27 +08:00
Schedule(() =>
2021-04-19 15:06:40 +08:00
{
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
// 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-05-20 17:37:27 +08:00
OnUserBeganPlaying?.Invoke(userId, state);
});
2020-10-26 19:05:11 +08:00
return Task.CompletedTask;
}
Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state)
{
2021-05-20 17:37:27 +08:00
Schedule(() =>
2021-04-19 15:06:40 +08:00
{
playingUsers.Remove(userId);
playingUserStates.Remove(userId);
2021-05-20 17:37:27 +08:00
OnUserFinishedPlaying?.Invoke(userId, state);
});
2020-10-26 19:05:11 +08:00
return Task.CompletedTask;
}
Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data)
{
2021-05-20 17:37:27 +08:00
Schedule(() => OnNewFrames?.Invoke(userId, data));
2020-10-26 19:05:11 +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
{
// 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");
IsPlaying = true;
// 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
currentBeatmap = state.Beatmap;
currentScore = score;
BeginPlayingInternal(currentState);
});
2020-10-22 14:27:04 +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
{
// 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...
Schedule(() =>
{
2021-05-21 15:00:58 +08:00
if (!IsPlaying)
return;
2020-10-22 21:56:23 +08:00
IsPlaying = false;
currentBeatmap = null;
2020-10-22 21:56:23 +08:00
EndPlayingInternal(currentState);
});
2020-10-22 14:27:04 +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);
2021-05-20 17:37:27 +08:00
if (watchingUsers.Contains(userId))
return;
2021-05-20 17:37:27 +08:00
watchingUsers.Add(userId);
2020-10-22 18:17:19 +08:00
WatchUserInternal(userId);
2020-10-22 14:27:04 +08:00
}
public void StopWatchingUser(int userId)
2020-10-22 18:17:19 +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);
playingUserStates.Remove(userId);
StopWatchingUserInternal(userId);
});
2020-10-22 18:17:19 +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();
if (pendingFrames.Count > 0 && Time.Current - lastSendTime > TIME_BETWEEN_SENDS)
2020-10-22 18:17:19 +08:00
purgePendingFrames();
}
public void HandleFrame(ReplayFrame frame)
{
2021-05-21 14:57:31 +08:00
Debug.Assert(ThreadSafety.IsUpdateThread);
if (!IsPlaying)
return;
if (frame is IConvertibleReplayFrame convertible)
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();
Debug.Assert(currentScore != null);
SendFrames(new FrameDataBundle(currentScore.ScoreInfo, frames));
2020-10-22 18:17:19 +08:00
lastSendTime = Time.Current;
}
}
}