1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-16 18:38:32 +08:00
Files
osu-lazer/osu.Game/Online/Metadata/MetadataClient.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

216 lines
7.1 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Online.Metadata
{
public abstract partial class MetadataClient : Component, IMetadataClient, IMetadataServer
{
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// A list of all watched multiplayer rooms (see <see cref="BeginWatchingMultiplayerRoom"/>).
/// </summary>
protected readonly HashSet<long> WatchedRooms = new HashSet<long>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly IBindableList<APIRelation> localFriends = new BindableList<APIRelation>();
protected override void LoadComplete()
{
base.LoadComplete();
localFriends.BindTo(api.LocalUserState.Friends);
localFriends.BindCollectionChanged((_, _) => RefreshFriends().FireAndForget());
}
#region Beatmap metadata updates
public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);
public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
public event Action<int[]>? ChangedBeatmapSetsArrived;
protected Task ProcessChanges(int[] beatmapSetIDs)
{
ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray());
return Task.CompletedTask;
}
#endregion
#region User presence updates
/// <summary>
/// The <see cref="UserPresence"/> information about the current user.
/// </summary>
public abstract UserPresence LocalUserPresence { get; }
/// <summary>
/// Dictionary keyed by user ID containing all of the <see cref="UserPresence"/> information about currently online users received from the server.
/// </summary>
public abstract IBindableDictionary<int, UserPresence> UserPresences { get; }
/// <summary>
/// Dictionary keyed by user ID containing all of the <see cref="UserPresence"/> information about currently online friends received from the server.
/// </summary>
public abstract IBindableDictionary<int, UserPresence> FriendPresences { get; }
/// <summary>
/// Attempts to retrieve the presence of a user.
/// </summary>
/// <remarks>
/// This will return data if the client is currently receiving presence data. See <see cref="BeginWatchingUserPresence"/>.
/// </remarks>
/// <param name="userId">The user ID.</param>
/// <returns>The user presence, or null if not available or the user's offline.</returns>
public UserPresence? GetPresence(int userId)
{
if (userId == api.LocalUser.Value.OnlineID)
return LocalUserPresence;
if (FriendPresences.TryGetValue(userId, out UserPresence presence))
return presence;
if (UserPresences.TryGetValue(userId, out presence))
return presence;
return null;
}
public abstract Task UpdateActivity(UserActivity? activity);
public abstract Task UpdateStatus(UserStatus? status);
private int userPresenceWatchCount;
protected bool IsWatchingUserPresence
=> Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0;
/// <summary>
/// Signals to the server that we want to begin receiving status updates for all users.
/// </summary>
/// <returns>An <see cref="IDisposable"/> which will end the session when disposed.</returns>
public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this);
Task IMetadataServer.BeginWatchingUserPresence()
{
if (Interlocked.Increment(ref userPresenceWatchCount) == 1)
return BeginWatchingUserPresenceInternal();
return Task.CompletedTask;
}
Task IMetadataServer.EndWatchingUserPresence()
{
if (Interlocked.Decrement(ref userPresenceWatchCount) == 0)
return EndWatchingUserPresenceInternal();
return Task.CompletedTask;
}
protected abstract Task BeginWatchingUserPresenceInternal();
protected abstract Task EndWatchingUserPresenceInternal();
public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence);
private class UserPresenceWatchToken : IDisposable
{
private readonly IMetadataServer server;
private bool isDisposed;
public UserPresenceWatchToken(IMetadataServer server)
{
this.server = server;
server.BeginWatchingUserPresence().FireAndForget();
}
public void Dispose()
{
if (isDisposed)
return;
server.EndWatchingUserPresence().FireAndForget();
isDisposed = true;
}
}
#endregion
#region Daily Challenge
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
#endregion
#region Multiplayer room watching
public abstract Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id);
public abstract Task EndWatchingMultiplayerRoom(long id);
public abstract Task RefreshFriends();
public event Action<MultiplayerRoomScoreSetEvent>? MultiplayerRoomScoreSet;
Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent)
{
if (MultiplayerRoomScoreSet != null)
Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent);
return Task.CompletedTask;
}
#endregion
#region Disconnection handling
/// <summary>
/// Invoked just prior to disconnection.
/// </summary>
public event Action? Disconnecting;
public abstract Task Reconnect();
protected abstract Task DisconnectInternal();
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() =>
{
Disconnecting?.Invoke();
DisconnectInternal().FireAndForget();
});
return Task.CompletedTask;
}
Task IStatefulUserHubClient.ServerShuttingDown()
{
this.ReconnectWhenReady(IsConnected, () => WatchedRooms.Count == 0, Reconnect);
return Task.CompletedTask;
}
#endregion
}
}