mirror of https://github.com/ppy/osu
184 lines
7.3 KiB
C#
184 lines
7.3 KiB
C#
// 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;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Maximum number of frames sent per bundle via <see cref="SendFramesFromUser"/>.
|
|
/// </summary>
|
|
public const int FRAME_BUNDLE_SIZE = 10;
|
|
|
|
/// <summary>
|
|
/// Whether to force send operations to fail (simulating a network issue).
|
|
/// </summary>
|
|
public bool ShouldFailSendingFrames { get; set; }
|
|
|
|
public int FrameSendAttempts { get; private set; }
|
|
|
|
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>(true);
|
|
|
|
public IReadOnlyDictionary<int, ReplayFrame> LastReceivedUserFrames => lastReceivedUserFrames;
|
|
|
|
private readonly Dictionary<int, ReplayFrame> lastReceivedUserFrames = new Dictionary<int, ReplayFrame>();
|
|
|
|
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
|
private readonly Dictionary<int, APIMod[]> userModsDictionary = new Dictionary<int, APIMod[]>();
|
|
private readonly Dictionary<int, int> userNextFrameDictionary = new Dictionary<int, int>();
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private RulesetStore rulesetStore { get; set; } = null!;
|
|
|
|
public TestSpectatorClient()
|
|
{
|
|
OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts play for an arbitrary user.
|
|
/// </summary>
|
|
/// <param name="userId">The user to start play for.</param>
|
|
/// <param name="beatmapId">The playing beatmap id.</param>
|
|
/// <param name="mods">The mods the user has applied.</param>
|
|
public void SendStartPlay(int userId, int beatmapId, APIMod[]? mods = null)
|
|
{
|
|
userBeatmapDictionary[userId] = beatmapId;
|
|
userModsDictionary[userId] = mods ?? Array.Empty<APIMod>();
|
|
userNextFrameDictionary[userId] = 0;
|
|
sendPlayingState(userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends play for an arbitrary user.
|
|
/// </summary>
|
|
/// <param name="userId">The user to end play for.</param>
|
|
/// <param name="state">The spectator state to end play with.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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, <see cref="ShouldFailSendingFrames"/> will have no effect.
|
|
/// </summary>
|
|
/// <param name="userId">The user to send frames for.</param>
|
|
/// <param name="count">The total number of frames to send.</param>
|
|
/// <param name="startTime">The time to start gameplay frames from.</param>
|
|
public void SendFramesFromUser(int userId, int count, double startTime = 0)
|
|
{
|
|
var frames = new List<LegacyReplayFrame>();
|
|
|
|
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,
|
|
TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)),
|
|
Accuracy = RNG.NextDouble(0.98, 1),
|
|
}, 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
|
|
});
|
|
}
|
|
}
|
|
}
|