1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 05:52:36 +08:00
Files
osu-lazer/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.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

97 lines
4.2 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;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
using osu.Game.Utils;
namespace osu.Game.Online.Multiplayer
{
public static class MultiplayerClientExtensions
{
public static void FireAndForget(this Task task, Action? onSuccess = null, Action<Exception>? onError = null) =>
task.ContinueWith(t =>
{
if (t.IsFaulted)
{
Debug.Assert(t.Exception != null);
Exception exception = t.Exception.AsSingular();
onError?.Invoke(exception);
// OnlineStatusNotifier is already letting users know about interruptions to connections.
// Silence these because it gets very spammy otherwise.
if (SentryLogger.IsLocalUserConnectivityException(exception))
return;
if (exception.GetHubExceptionMessage() is string message)
{
// Hub exceptions generally contain something we can show the user directly.
Logger.Log(message, level: LogLevel.Important);
return;
}
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
}
else
{
onSuccess?.Invoke();
}
});
/// <summary>
/// Start a background process to disconnect/reconnect as soon as a specific condition is met.
/// </summary>
/// <remarks>
/// If a reconnect happens via another means, this will abort attempts.
/// We only want to reconnect once.
/// </remarks>
/// <param name="client">The client to operate on.</param>
/// <param name="isConnected">Connected state of client.</param>
/// <param name="readyFunction">The condition which should be <c>true</c> to continue with the shutdown.</param>
/// <param name="reconnectFunction">The method to run to perform the reconnect.</param>
public static void ReconnectWhenReady(this IStatefulUserHubClient client, IBindable<bool> isConnected, Func<bool> readyFunction, Func<Task> reconnectFunction)
{
Task.Run(async () =>
{
bool didReconnect = false;
var connected = isConnected.GetBoundCopy();
connected.ValueChanged += _ => didReconnect = true;
string clientName = client.GetType().ReadableName();
Logger.Log($"{clientName} has signalled shutdown");
while (!readyFunction())
{
Logger.Log($"{clientName} shutdown waiting for idle conditions...");
await Task.Delay(10000).ConfigureAwait(false);
}
Logger.Log($"{clientName} disconnecting due to shutdown signal");
if (!didReconnect)
await reconnectFunction().ConfigureAwait(false);
connected.UnbindAll();
}).FireAndForget();
}
public static string? GetHubExceptionMessage(this Exception exception)
{
if (exception is HubException hubException)
// HubExceptions arrive with additional message context added, but we want to display the human readable message:
// "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
// We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
return hubException.Message.Substring(exception.Message.IndexOf(':') + 1).Trim();
return null;
}
}
}