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;
|
2022-02-23 23:59:25 +08:00
|
|
|
using osu.Framework.Logging;
|
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
|
|
|
|
2022-02-02 22:19:43 +08:00
|
|
|
/// <summary>
|
|
|
|
/// The states of all users currently being watched.
|
|
|
|
/// </summary>
|
2022-02-09 11:09:04 +08:00
|
|
|
public IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
|
2022-02-02 22:19:43 +08:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A global list of all players currently playing.
|
|
|
|
/// </summary>
|
|
|
|
public IBindableList<int> PlayingUsers => playingUsers;
|
|
|
|
|
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>
|
2022-04-11 17:51:37 +08:00
|
|
|
public virtual 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>
|
2022-04-11 17:51:37 +08:00
|
|
|
public virtual 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>
|
2022-04-11 17:51:37 +08:00
|
|
|
public virtual event Action<int, SpectatorState>? OnUserFinishedPlaying;
|
2020-10-26 19:05:11 +08:00
|
|
|
|
2022-02-24 01:29:49 +08:00
|
|
|
/// <summary>
|
|
|
|
/// All users currently being watched.
|
|
|
|
/// </summary>
|
|
|
|
private readonly List<int> watchedUsers = new List<int>();
|
|
|
|
|
|
|
|
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
|
|
|
|
private readonly BindableList<int> playingUsers = new BindableList<int>();
|
|
|
|
private readonly SpectatorState currentState = new SpectatorState();
|
|
|
|
|
|
|
|
private IBeatmap? currentBeatmap;
|
|
|
|
private Score? currentScore;
|
|
|
|
|
|
|
|
private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
|
|
|
|
|
|
|
|
private readonly Queue<LegacyReplayFrame> pendingFrames = new Queue<LegacyReplayFrame>();
|
|
|
|
|
|
|
|
private double lastPurgeTime;
|
|
|
|
|
|
|
|
private Task? lastSend;
|
|
|
|
|
|
|
|
private const int max_pending_frames = 30;
|
|
|
|
|
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
|
2022-02-09 11:09:04 +08:00
|
|
|
int[] users = watchedUsers.ToArray();
|
|
|
|
watchedUsers.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)
|
2022-02-15 18:08:32 +08:00
|
|
|
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
|
2021-05-20 15:30:56 +08:00
|
|
|
BeginPlayingInternal(currentState);
|
|
|
|
}
|
|
|
|
else
|
2022-02-02 22:19:43 +08:00
|
|
|
{
|
|
|
|
playingUsers.Clear();
|
2022-02-09 11:09:04 +08:00
|
|
|
watchedUserStates.Clear();
|
2022-02-02 22:19:43 +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
|
|
|
{
|
2022-02-02 22:19:43 +08:00
|
|
|
if (!playingUsers.Contains(userId))
|
|
|
|
playingUsers.Add(userId);
|
|
|
|
|
2022-02-09 11:09:04 +08:00
|
|
|
if (watchedUsers.Contains(userId))
|
|
|
|
watchedUserStates[userId] = state;
|
2022-02-02 22:19:43 +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
|
|
|
{
|
2022-02-02 22:19:43 +08:00
|
|
|
playingUsers.Remove(userId);
|
|
|
|
|
2022-02-09 11:09:04 +08:00
|
|
|
if (watchedUsers.Contains(userId))
|
|
|
|
watchedUserStates[userId] = state;
|
2022-02-02 22:19:43 +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
|
|
|
{
|
2022-01-31 17:32:17 +08:00
|
|
|
if (data.Frames.Count > 0)
|
|
|
|
data.Frames[^1].Header = data.Header;
|
|
|
|
|
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();
|
2022-02-09 11:09:04 +08:00
|
|
|
currentState.State = SpectatedUserState.Playing;
|
2022-05-30 18:14:03 +08:00
|
|
|
currentState.MaxAchievableCombo = state.ScoreProcessor.MaxAchievableCombo;
|
|
|
|
currentState.MaxAchievableBaseScore = state.ScoreProcessor.MaxAchievableBaseScore;
|
|
|
|
currentState.TotalBasicHitObjects = state.ScoreProcessor.TotalBasicHitObjects;
|
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
|
|
|
|
2022-02-23 23:59:25 +08:00
|
|
|
public void HandleFrame(ReplayFrame frame) => Schedule(() =>
|
|
|
|
{
|
|
|
|
if (!IsPlaying)
|
|
|
|
{
|
|
|
|
Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (frame is IConvertibleReplayFrame convertible)
|
|
|
|
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));
|
|
|
|
|
|
|
|
if (pendingFrames.Count > max_pending_frames)
|
|
|
|
purgePendingFrames();
|
|
|
|
});
|
|
|
|
|
2022-02-01 14:51:41 +08:00
|
|
|
public void EndPlaying(GameplayState state)
|
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
|
|
|
|
2022-01-28 21:26:05 +08:00
|
|
|
if (pendingFrames.Count > 0)
|
2022-02-15 18:05:46 +08:00
|
|
|
purgePendingFrames();
|
2022-01-28 21:26:05 +08:00
|
|
|
|
2021-05-21 14:57:39 +08:00
|
|
|
IsPlaying = false;
|
|
|
|
currentBeatmap = null;
|
2020-10-22 21:56:23 +08:00
|
|
|
|
2022-02-01 14:51:41 +08:00
|
|
|
if (state.HasPassed)
|
2022-02-09 11:09:04 +08:00
|
|
|
currentState.State = SpectatedUserState.Passed;
|
2022-02-01 14:51:41 +08:00
|
|
|
else if (state.HasFailed)
|
2022-02-09 11:09:04 +08:00
|
|
|
currentState.State = SpectatedUserState.Failed;
|
2022-02-01 14:51:41 +08:00
|
|
|
else
|
2022-02-09 11:09:04 +08:00
|
|
|
currentState.State = SpectatedUserState.Quit;
|
2022-02-01 14:51:41 +08:00
|
|
|
|
2021-05-21 14:57:39 +08:00
|
|
|
EndPlayingInternal(currentState);
|
|
|
|
});
|
2020-10-22 14:27:04 +08:00
|
|
|
}
|
|
|
|
|
2022-04-11 18:27:14 +08:00
|
|
|
public virtual 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
|
|
|
|
2022-02-09 11:09:04 +08:00
|
|
|
if (watchedUsers.Contains(userId))
|
2021-05-20 17:37:27 +08:00
|
|
|
return;
|
2020-10-22 17:37:19 +08:00
|
|
|
|
2022-02-09 11:09:04 +08:00
|
|
|
watchedUsers.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(() =>
|
|
|
|
{
|
2022-02-09 11:09:04 +08:00
|
|
|
watchedUsers.Remove(userId);
|
|
|
|
watchedUserStates.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);
|
|
|
|
|
2022-02-24 01:19:12 +08:00
|
|
|
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
|
2022-02-24 01:29:49 +08:00
|
|
|
|
2021-05-20 15:30:56 +08:00
|
|
|
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
|
|
|
protected override void Update()
|
|
|
|
{
|
|
|
|
base.Update();
|
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
if (pendingFrames.Count > 0 && Time.Current - lastPurgeTime > TIME_BETWEEN_SENDS)
|
2020-10-22 18:17:19 +08:00
|
|
|
purgePendingFrames();
|
|
|
|
}
|
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
private void purgePendingFrames()
|
2020-10-22 18:17:19 +08:00
|
|
|
{
|
2022-01-28 21:26:05 +08:00
|
|
|
if (pendingFrames.Count == 0)
|
2020-10-22 18:17:19 +08:00
|
|
|
return;
|
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
Debug.Assert(currentScore != null);
|
|
|
|
|
2020-10-22 18:17:19 +08:00
|
|
|
var frames = pendingFrames.ToArray();
|
2022-02-15 18:05:46 +08:00
|
|
|
var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames);
|
2020-10-22 18:17:19 +08:00
|
|
|
|
|
|
|
pendingFrames.Clear();
|
2022-02-15 18:05:46 +08:00
|
|
|
lastPurgeTime = Time.Current;
|
2020-10-22 18:17:19 +08:00
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
pendingFrameBundles.Enqueue(bundle);
|
2020-12-14 16:33:23 +08:00
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
sendNextBundleIfRequired();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void sendNextBundleIfRequired()
|
|
|
|
{
|
2022-02-24 01:19:12 +08:00
|
|
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
if (lastSend?.IsCompleted == false)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!pendingFrameBundles.TryPeek(out var bundle))
|
|
|
|
return;
|
|
|
|
|
2022-02-24 01:19:12 +08:00
|
|
|
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
|
|
|
|
|
|
|
lastSend = tcs.Task;
|
|
|
|
|
|
|
|
SendFramesInternal(bundle).ContinueWith(t => Schedule(() =>
|
2022-02-15 18:05:46 +08:00
|
|
|
{
|
2022-02-24 01:19:12 +08:00
|
|
|
bool wasSuccessful = t.Exception == null;
|
|
|
|
|
2022-02-15 18:05:46 +08:00
|
|
|
// If the last bundle send wasn't successful, try again without dequeuing.
|
2022-02-24 01:19:12 +08:00
|
|
|
if (wasSuccessful)
|
2022-02-15 18:05:46 +08:00
|
|
|
pendingFrameBundles.Dequeue();
|
2020-10-22 18:17:19 +08:00
|
|
|
|
2022-02-24 01:19:12 +08:00
|
|
|
tcs.SetResult(wasSuccessful);
|
2022-02-15 18:05:46 +08:00
|
|
|
sendNextBundleIfRequired();
|
|
|
|
}));
|
2020-10-22 13:54:27 +08:00
|
|
|
}
|
2020-10-21 18:05:20 +08:00
|
|
|
}
|
|
|
|
}
|