mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 18:15:03 +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.
325 lines
13 KiB
C#
325 lines
13 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;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Logging;
|
|
using osu.Game.Configuration;
|
|
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 partial class OnlineMetadataClient : MetadataClient
|
|
{
|
|
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>();
|
|
|
|
public override UserPresence LocalUserPresence => localUserPresence;
|
|
private UserPresence localUserPresence;
|
|
|
|
public override IBindableDictionary<int, UserPresence> UserPresences => userPresences;
|
|
private readonly BindableDictionary<int, UserPresence> userPresences = new BindableDictionary<int, UserPresence>();
|
|
|
|
public override IBindableDictionary<int, UserPresence> FriendPresences => friendPresences;
|
|
private readonly BindableDictionary<int, UserPresence> friendPresences = new BindableDictionary<int, UserPresence>();
|
|
|
|
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
|
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
|
|
|
private readonly string endpoint;
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
private IHubClientConnector? connector;
|
|
private Bindable<int> lastQueueId = null!;
|
|
private IBindable<APIUser> localUser = null!;
|
|
private IBindable<UserStatus> userStatus = null!;
|
|
private IBindable<UserActivity?> userActivity = null!;
|
|
|
|
private HubConnection? connection => connector?.CurrentConnection;
|
|
|
|
public OnlineMetadataClient(EndpointConfiguration endpoints)
|
|
{
|
|
endpoint = endpoints.MetadataUrl;
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(OsuConfigManager config, SessionStatics session)
|
|
{
|
|
// Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization.
|
|
// More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code.
|
|
connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint);
|
|
|
|
if (connector != null)
|
|
{
|
|
connector.ConfigureConnection = connection =>
|
|
{
|
|
// this is kind of SILLY
|
|
// https://github.com/dotnet/aspnetcore/issues/15198
|
|
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
|
|
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
|
|
connection.On<int, UserPresence?>(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated);
|
|
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
|
|
connection.On<MultiplayerRoomScoreSetEvent>(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet);
|
|
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
|
|
connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
|
|
};
|
|
|
|
IsConnected.BindTo(connector.IsConnected);
|
|
IsConnected.BindValueChanged(isConnectedChanged, true);
|
|
}
|
|
|
|
localUser = api.LocalUser.GetBoundCopy();
|
|
lastQueueId = config.GetBindable<int>(OsuSetting.LastProcessedMetadataId);
|
|
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
|
|
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
userStatus.BindValueChanged(status =>
|
|
{
|
|
if (localUser.Value is not GuestUser)
|
|
UpdateStatus(status.NewValue).FireAndForget();
|
|
}, true);
|
|
|
|
userActivity.BindValueChanged(activity =>
|
|
{
|
|
if (localUser.Value is not GuestUser)
|
|
UpdateActivity(activity.NewValue).FireAndForget();
|
|
}, true);
|
|
}
|
|
|
|
private bool catchingUp;
|
|
|
|
private void isConnectedChanged(ValueChangedEvent<bool> connected)
|
|
{
|
|
if (!connected.NewValue)
|
|
{
|
|
Schedule(() =>
|
|
{
|
|
userPresences.Clear();
|
|
friendPresences.Clear();
|
|
WatchedRooms.Clear();
|
|
dailyChallengeInfo.Value = null;
|
|
localUserPresence = default;
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (IsWatchingUserPresence)
|
|
BeginWatchingUserPresenceInternal().FireAndForget();
|
|
|
|
if (localUser.Value is not GuestUser)
|
|
{
|
|
UpdateActivity(userActivity.Value).FireAndForget();
|
|
UpdateStatus(userStatus.Value).FireAndForget();
|
|
}
|
|
|
|
if (lastQueueId.Value >= 0)
|
|
{
|
|
catchingUp = true;
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
Logger.Log($"Requesting catch-up from {lastQueueId.Value}");
|
|
var catchUpChanges = await GetChangesSince(lastQueueId.Value).ConfigureAwait(true);
|
|
|
|
lastQueueId.Value = catchUpChanges.LastProcessedQueueID;
|
|
|
|
if (catchUpChanges.BeatmapSetIDs.Length == 0)
|
|
{
|
|
Logger.Log($"Catch-up complete at {lastQueueId.Value}");
|
|
break;
|
|
}
|
|
|
|
await ProcessChanges(catchUpChanges.BeatmapSetIDs).ConfigureAwait(true);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log($"Error while processing catch-up of metadata ({e.Message})");
|
|
}
|
|
finally
|
|
{
|
|
catchingUp = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public override async Task BeatmapSetsUpdated(BeatmapUpdates updates)
|
|
{
|
|
Logger.Log($"Received beatmap updates {updates.BeatmapSetIDs.Length} updates with last id {updates.LastProcessedQueueID}");
|
|
|
|
// If we're still catching up, avoid updating the last ID as it will interfere with catch-up efforts.
|
|
if (!catchingUp)
|
|
lastQueueId.Value = updates.LastProcessedQueueID;
|
|
|
|
await ProcessChanges(updates.BeatmapSetIDs).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task<BeatmapUpdates> GetChangesSince(int queueId)
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
return Task.FromCanceled<BeatmapUpdates>(CancellationToken.None);
|
|
|
|
Logger.Log($"Requesting any changes since last known queue id {queueId}");
|
|
|
|
Debug.Assert(connection != null);
|
|
|
|
return connection.InvokeAsync<BeatmapUpdates>(nameof(IMetadataServer.GetChangesSince), queueId);
|
|
}
|
|
|
|
public override Task UpdateActivity(UserActivity? activity)
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
return Task.FromCanceled(new CancellationToken(true));
|
|
|
|
Debug.Assert(connection != null);
|
|
return connection.InvokeAsync(nameof(IMetadataServer.UpdateActivity), activity);
|
|
}
|
|
|
|
public override Task UpdateStatus(UserStatus? status)
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
return Task.FromCanceled(new CancellationToken(true));
|
|
|
|
Debug.Assert(connection != null);
|
|
return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status);
|
|
}
|
|
|
|
protected override Task BeginWatchingUserPresenceInternal()
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
return Task.CompletedTask;
|
|
|
|
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
|
|
|
|
Debug.Assert(connection != null);
|
|
return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence));
|
|
}
|
|
|
|
protected override Task EndWatchingUserPresenceInternal()
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
return Task.CompletedTask;
|
|
|
|
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
|
|
|
|
// must be scheduled before any remote calls to avoid mis-ordering.
|
|
Schedule(() => userPresences.Clear());
|
|
|
|
Debug.Assert(connection != null);
|
|
return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence));
|
|
}
|
|
|
|
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
|
{
|
|
Schedule(() =>
|
|
{
|
|
if (presence?.Status != null)
|
|
{
|
|
if (userId == api.LocalUser.Value.OnlineID)
|
|
localUserPresence = presence.Value;
|
|
userPresences[userId] = presence.Value;
|
|
}
|
|
else
|
|
{
|
|
if (userId == api.LocalUser.Value.OnlineID)
|
|
localUserPresence = default;
|
|
userPresences.Remove(userId);
|
|
}
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task FriendPresenceUpdated(int userId, UserPresence? presence)
|
|
{
|
|
Schedule(() =>
|
|
{
|
|
if (presence?.Status != null)
|
|
friendPresences[userId] = presence.Value;
|
|
else
|
|
friendPresences.Remove(userId);
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
|
|
{
|
|
Schedule(() => dailyChallengeInfo.Value = info);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override async Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id)
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
throw new OperationCanceledException();
|
|
|
|
WatchedRooms.Add(id);
|
|
|
|
Debug.Assert(connection != null);
|
|
var result = await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
|
|
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network);
|
|
return result;
|
|
}
|
|
|
|
public override async Task EndWatchingMultiplayerRoom(long id)
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
throw new OperationCanceledException();
|
|
|
|
WatchedRooms.Remove(id);
|
|
|
|
Debug.Assert(connection != null);
|
|
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
|
|
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
|
|
}
|
|
|
|
public override async Task RefreshFriends()
|
|
{
|
|
if (connector?.IsConnected.Value != true)
|
|
throw new OperationCanceledException();
|
|
|
|
Debug.Assert(connection != null);
|
|
await connection.InvokeAsync(nameof(IMetadataServer.RefreshFriends)).ConfigureAwait(false);
|
|
}
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
{
|
|
base.Dispose(isDisposing);
|
|
connector?.Dispose();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|