mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 06:21:22 +08:00
51b4e89773
Client side requirements for making the client connect as soon as possible, based on how the client is being used. This is especially important with the introduction of ranked play: previously the worst case scenario would be that you couldn't join a multiplayer room (or spectate a user) and this was [automatically handled](https://github.com/ppy/osu/blob/f66e2c432fdb08db46477c4fa08ca74e551d037f/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs#L115-L121) mostly*, but now, if you leave the game open for a while, you can potentially be stuck queueing in ranked play with no users remaining on your server. Some samples of how this looks follow. Do note that the client is showing "Server is shutting down" errors. This is only going to happen in local debug environments – In production, when you reconnect to the endpoint you will always get a non-shutting-down instance. Idle scenario: https://github.com/user-attachments/assets/dd47fdf6-8d49-48e3-a19f-b196a581070b Non-idle scenario: https://github.com/user-attachments/assets/dfc8a41a-83fb-4b08-94b4-9595faf88294 * Spectator isn't handled properly, as one example.
220 lines
8.5 KiB
C#
220 lines
8.5 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.Judgements;
|
|
using osu.Game.Rulesets.Objects;
|
|
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 => isConnected;
|
|
private readonly BindableBool isConnected = new BindableBool(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.TryGetValue(userId, out int beatmapId))
|
|
return;
|
|
|
|
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
|
|
{
|
|
BeatmapID = beatmapId,
|
|
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>
|
|
/// <param name="initialResultCount">Add a number of misses to frame header data for testing purposes.</param>
|
|
public void SendFramesFromUser(int userId, int count, double startTime = 0, int initialResultCount = 0)
|
|
{
|
|
var frames = new List<LegacyReplayFrame>();
|
|
|
|
int currentFrameIndex = userNextFrameDictionary[userId];
|
|
int lastFrameIndex = currentFrameIndex + count - 1;
|
|
|
|
var scoreProcessor = new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance());
|
|
|
|
for (int i = 0; i < initialResultCount; i++)
|
|
{
|
|
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement())
|
|
{
|
|
Type = HitResult.Miss,
|
|
});
|
|
}
|
|
|
|
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),
|
|
Statistics = scoreProcessor.Statistics.ToDictionary(),
|
|
}, scoreProcessor, frames.ToArray());
|
|
|
|
if (initialResultCount > 0)
|
|
{
|
|
foreach (var f in frames)
|
|
f.Header = bundle.Header;
|
|
}
|
|
|
|
scoreProcessor.ResetFromReplayFrame(frames.Last());
|
|
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
|
|
|
frames.Clear();
|
|
}
|
|
}
|
|
|
|
protected override async Task<bool> 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();
|
|
|
|
await ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
protected override Task DisconnectInternal()
|
|
{
|
|
isConnected.Value = false;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task Reconnect()
|
|
{
|
|
isConnected.Value = true;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
}
|