1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-23 16:20:30 +08:00
Files
osu-lazer/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
T
Dean Herbert 51b4e89773 Eagerly connect to latest server instance for best online experience (#37506)
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.
2026-05-05 16:06:00 +09:00

139 lines
5.4 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.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.Spectator
{
public partial class OnlineSpectatorClient : SpectatorClient
{
private readonly string endpoint;
private IHubClientConnector? connector;
public override IBindable<bool> IsConnected { get; } = new BindableBool();
private HubConnection? connection => connector?.CurrentConnection;
public OnlineSpectatorClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorUrl;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
connector = api.GetHubConnector(nameof(SpectatorClient), endpoint);
if (connector != null)
{
connector.ConfigureConnection = connection =>
{
// until strong typed client support is added, each method must be manually bound
// (see https://github.com/dotnet/aspnetcore/issues/15198)
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
};
IsConnected.BindTo(connector.IsConnected);
}
}
protected override async Task<bool> BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
if (!IsConnected.Value)
return false;
Debug.Assert(connection != null);
try
{
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state).ConfigureAwait(false);
return true;
}
catch (Exception exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
{
Debug.Assert(connector != null);
await connector.Reconnect().ConfigureAwait(false);
return await BeginPlayingInternal(scoreToken, state).ConfigureAwait(false);
}
// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
// For now, let's ignore these so they don't cause unobserved exceptions to appear to the user (and sentry),
// but log to disk for diagnostic purposes.
Logger.Log($"{nameof(OnlineSpectatorClient)}.{nameof(BeginPlayingInternal)} failed: {exception.Message}", LoggingTarget.Network);
return false;
}
}
protected override Task SendFramesInternal(FrameDataBundle bundle)
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), bundle);
}
protected override Task EndPlayingInternal(SpectatorState state)
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(ISpectatorServer.EndPlaySession), state);
}
protected override Task WatchUserInternal(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
}
protected override Task StopWatchingUserInternal(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
}
public override async Task Reconnect()
{
if (connector != null)
await connector.Reconnect().ConfigureAwait(false);
}
protected override async Task DisconnectInternal()
{
if (connector != null)
await connector.Disconnect().ConfigureAwait(false);
}
}
}