// Copyright (c) ppy Pty Ltd . 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; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Tests.Visual.Spectator { public partial class TestSpectatorClient : SpectatorClient { /// /// Maximum number of frames sent per bundle via . /// public const int FRAME_BUNDLE_SIZE = 10; /// /// Whether to force send operations to fail (simulating a network issue). /// public bool ShouldFailSendingFrames { get; set; } public int FrameSendAttempts { get; private set; } public override IBindable IsConnected { get; } = new Bindable(true); public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; private readonly Dictionary lastReceivedUserFrames = new Dictionary(); private readonly Dictionary userBeatmapDictionary = new Dictionary(); private readonly Dictionary userModsDictionary = new Dictionary(); private readonly Dictionary userNextFrameDictionary = new Dictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; [Resolved] private RulesetStore rulesetStore { get; set; } = null!; public TestSpectatorClient() { OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// /// Starts play for an arbitrary user. /// /// The user to start play for. /// The playing beatmap id. /// The mods the user has applied. public void SendStartPlay(int userId, int beatmapId, APIMod[]? mods = null) { userBeatmapDictionary[userId] = beatmapId; userModsDictionary[userId] = mods ?? Array.Empty(); userNextFrameDictionary[userId] = 0; sendPlayingState(userId); } /// /// Ends play for an arbitrary user. /// /// The user to end play for. /// The spectator state to end play with. public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, Mods = userModsDictionary[userId], State = state }); userBeatmapDictionary.Remove(userId); userModsDictionary.Remove(userId); } /// /// Sends frames for an arbitrary user, in bundles containing 10 frames each. /// This bypasses the standard queueing mechanism completely and should only be used to test cases where multiple users need to be sending data. /// Importantly, will have no effect. /// /// The user to send frames for. /// The total number of frames to send. /// The time to start gameplay frames from. public void SendFramesFromUser(int userId, int count, double startTime = 0) { var frames = new List(); int currentFrameIndex = userNextFrameDictionary[userId]; int lastFrameIndex = currentFrameIndex + count - 1; for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++) { // This is done in the next frame so that currentFrameIndex is updated to the correct value. if (frames.Count == FRAME_BUNDLE_SIZE) flush(); var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1; frames.Add(new LegacyReplayFrame(currentFrameIndex * 100 + startTime, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); } flush(); userNextFrameDictionary[userId] = currentFrameIndex; void flush() { if (frames.Count == 0) return; var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); ((ISpectatorClient)this).UserSentFrames(userId, bundle); frames.Clear(); } } protected override Task BeginPlayingInternal(long? scoreToken, SpectatorState state) { // Track the local user's playing beatmap ID. Debug.Assert(state.BeatmapID != null); userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value; userModsDictionary[api.LocalUser.Value.Id] = state.Mods.ToArray(); return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state); } protected override Task SendFramesInternal(FrameDataBundle bundle) { FrameSendAttempts++; if (ShouldFailSendingFrames) return Task.FromException(new InvalidOperationException($"Intentional fail via {nameof(ShouldFailSendingFrames)}")); return ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, bundle); } protected override Task EndPlayingInternal(SpectatorState state) => ((ISpectatorClient)this).UserFinishedPlaying(api.LocalUser.Value.Id, state); protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; } protected override Task StopWatchingUserInternal(int userId) => Task.CompletedTask; private void sendPlayingState(int userId) { ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, Mods = userModsDictionary[userId], State = SpectatedUserState.Playing }); } } }