osu/osu.Game/Online/Spectator/SpectatorClient.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

349 lines
12 KiB
C#
Raw Normal View History

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.
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 06:57:31 +00:00
using osu.Framework.Development;
2021-02-09 04:46:00 +00:00
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
2020-10-22 10:17:19 +00: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 partial 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; }
2022-02-02 14:19:43 +00:00
/// <summary>
/// The states of all users currently being watched.
/// </summary>
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
2022-02-02 14:19:43 +00:00
/// <summary>
/// A global list of all players currently playing.
/// </summary>
public IBindableList<int> PlayingUsers => playingUsers;
2021-05-20 08:51:09 +00: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>
public virtual event Action<int, FrameDataBundle>? OnNewFrames;
2020-10-22 09:10:27 +00:00
2020-10-26 11:05:11 +00: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 11:05:11 +00:00
/// </summary>
public virtual 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>
public virtual event Action<int, SpectatorState>? OnUserFinishedPlaying;
2020-10-26 11:05:11 +00:00
/// <summary>
/// Called whenever a user-submitted score has been fully processed.
/// </summary>
public virtual event Action<int, long>? OnUserScoreProcessed;
/// <summary>
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
/// </summary>
private readonly Dictionary<int, int> watchedUsersRefCounts = new Dictionary<int, 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;
2022-12-12 04:59:27 +00:00
private long? currentScoreToken;
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;
[BackgroundDependencyLoader]
private void load()
{
2021-05-20 09:37:27 +00:00
IsConnected.BindValueChanged(connected => Schedule(() =>
{
if (connected.NewValue)
{
// get all the users that were previously being watched
var users = new Dictionary<int, int>(watchedUsersRefCounts);
watchedUsersRefCounts.Clear();
// resubscribe to watched users.
foreach ((int user, int watchers) in users)
{
for (int i = 0; i < watchers; i++)
WatchUser(user);
}
// re-send state in case it wasn't received
if (IsPlaying)
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
2022-12-12 04:59:27 +00:00
BeginPlayingInternal(currentScoreToken, currentState);
}
else
2022-02-02 14:19:43 +00:00
{
playingUsers.Clear();
2022-02-09 03:09:04 +00:00
watchedUserStates.Clear();
2022-02-02 14:19:43 +00:00
}
2021-05-20 09:37:27 +00:00
}), true);
}
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
{
2021-05-20 09:37:27 +00:00
Schedule(() =>
2021-04-19 07:06:40 +00:00
{
2022-02-02 14:19:43 +00:00
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
if (watchedUsersRefCounts.ContainsKey(userId))
2022-02-09 03:09:04 +00:00
watchedUserStates[userId] = state;
2022-02-02 14:19:43 +00:00
2021-05-20 09:37:27 +00:00
OnUserBeganPlaying?.Invoke(userId, state);
});
2020-10-26 11:05:11 +00:00
return Task.CompletedTask;
}
Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state)
{
2021-05-20 09:37:27 +00:00
Schedule(() =>
2021-04-19 07:06:40 +00:00
{
2022-02-02 14:19:43 +00:00
playingUsers.Remove(userId);
if (watchedUsersRefCounts.ContainsKey(userId))
2022-02-09 03:09:04 +00:00
watchedUserStates[userId] = state;
2022-02-02 14:19:43 +00:00
2021-05-20 09:37:27 +00:00
OnUserFinishedPlaying?.Invoke(userId, state);
});
2020-10-26 11:05:11 +00:00
return Task.CompletedTask;
}
Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data)
{
if (data.Frames.Count > 0)
data.Frames[^1].Header = data.Header;
2021-05-20 09:37:27 +00:00
Schedule(() => OnNewFrames?.Invoke(userId, data));
2020-10-26 11:05:11 +00:00
return Task.CompletedTask;
}
Task ISpectatorClient.UserScoreProcessed(int userId, long scoreId)
{
Schedule(() => OnUserScoreProcessed?.Invoke(userId, scoreId));
return Task.CompletedTask;
}
2022-12-12 04:59:27 +00:00
public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
2020-10-22 06:27:04 +00: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();
2022-02-09 03:09:04 +00:00
currentState.State = SpectatedUserState.Playing;
currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
2020-10-22 08:29:43 +00:00
currentBeatmap = state.Beatmap;
currentScore = score;
2022-12-12 04:59:27 +00:00
currentScoreToken = scoreToken;
2022-12-12 04:59:27 +00:00
BeginPlayingInternal(currentScoreToken, currentState);
});
2020-10-22 06:27:04 +00: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)
2022-07-03 11:27:56 +00:00
{
Debug.Assert(currentBeatmap != null);
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));
}
if (pendingFrames.Count > max_pending_frames)
purgePendingFrames();
});
2022-02-01 06:51:41 +00:00
public void EndPlaying(GameplayState state)
2020-10-22 06:27:04 +00: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 07:00:58 +00:00
if (!IsPlaying)
return;
2020-10-22 13:56:23 +00:00
2022-07-25 01:34:42 +00:00
// Disposal can take some time, leading to EndPlaying potentially being called after a future play session.
// Account for this by ensuring the score of the current play matches the one in the provided state.
if (currentScore != state.Score)
return;
if (pendingFrames.Count > 0)
purgePendingFrames();
IsPlaying = false;
currentBeatmap = null;
2020-10-22 13:56:23 +00:00
2022-02-01 06:51:41 +00:00
if (state.HasPassed)
2022-02-09 03:09:04 +00:00
currentState.State = SpectatedUserState.Passed;
2022-02-01 06:51:41 +00:00
else if (state.HasFailed)
2022-02-09 03:09:04 +00:00
currentState.State = SpectatedUserState.Failed;
2022-02-01 06:51:41 +00:00
else
2022-02-09 03:09:04 +00:00
currentState.State = SpectatedUserState.Quit;
2022-02-01 06:51:41 +00:00
EndPlayingInternal(currentState);
});
2020-10-22 06:27:04 +00:00
}
public virtual void WatchUser(int userId)
2020-10-22 06:27:04 +00:00
{
2021-05-21 06:57:31 +00:00
Debug.Assert(ThreadSafety.IsUpdateThread);
if (watchedUsersRefCounts.ContainsKey(userId))
{
watchedUsersRefCounts[userId]++;
2021-05-20 09:37:27 +00:00
return;
}
watchedUsersRefCounts.Add(userId, 1);
WatchUserInternal(userId);
2020-10-22 06:27:04 +00:00
}
public void StopWatchingUser(int userId)
2020-10-22 10:17:19 +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(() =>
{
if (watchedUsersRefCounts.TryGetValue(userId, out int watchers) && watchers > 1)
{
watchedUsersRefCounts[userId]--;
return;
}
watchedUsersRefCounts.Remove(userId);
2022-02-09 03:09:04 +00:00
watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
});
2020-10-22 10:17:19 +00:00
}
2022-12-12 04:59:27 +00:00
protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
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
protected override void Update()
{
base.Update();
if (pendingFrames.Count > 0 && Time.Current - lastPurgeTime > TIME_BETWEEN_SENDS)
2020-10-22 10:17:19 +00:00
purgePendingFrames();
}
private void purgePendingFrames()
2020-10-22 10:17:19 +00:00
{
if (pendingFrames.Count == 0)
2020-10-22 10:17:19 +00:00
return;
Debug.Assert(currentScore != null);
2020-10-22 10:17:19 +00:00
var frames = pendingFrames.ToArray();
var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames);
2020-10-22 10:17:19 +00:00
pendingFrames.Clear();
lastPurgeTime = Time.Current;
2020-10-22 10:17:19 +00:00
pendingFrameBundles.Enqueue(bundle);
sendNextBundleIfRequired();
}
private void sendNextBundleIfRequired()
{
Debug.Assert(ThreadSafety.IsUpdateThread);
if (lastSend?.IsCompleted == false)
return;
if (!pendingFrameBundles.TryPeek(out var bundle))
return;
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
lastSend = tcs.Task;
SendFramesInternal(bundle).ContinueWith(t =>
{
// Handle exception outside of `Schedule` to ensure it doesn't go unobserved.
bool wasSuccessful = t.Exception == null;
return Schedule(() =>
{
// If the last bundle send wasn't successful, try again without dequeuing.
if (wasSuccessful)
pendingFrameBundles.Dequeue();
2020-10-22 10:17:19 +00:00
tcs.SetResult(wasSuccessful);
sendNextBundleIfRequired();
});
});
}
}
}