diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index 4138c2757a..24f0188cf0 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -21,9 +21,9 @@ namespace osu.Game.Extensions /// Whether errors should be logged as errors visible to users, or as debug messages. /// Logging as debug will essentially silence the errors on non-release builds. /// - public static void CatchUnobservedExceptions(this Task task, bool logAsError = false) + public static Task CatchUnobservedExceptions(this Task task, bool logAsError = false) { - task.ContinueWith(t => + return task.ContinueWith(t => { Exception? exception = t.Exception?.AsSingular(); if (logAsError) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 0e736ed7c6..3d8ab4b4c7 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -110,37 +110,49 @@ namespace osu.Game.Online.Multiplayer } private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); + private CancellationTokenSource? joinCancellationSource; /// /// Joins the for a given API . /// /// The API . - public async Task JoinRoom(Room room) => await joinOrLeaveTaskChain.Add(async () => + public async Task JoinRoom(Room room) { - if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + var cancellationSource = new CancellationTokenSource(); - Debug.Assert(room.RoomID.Value != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value); - Debug.Assert(joinedRoom != null); - - // Populate users. - Debug.Assert(joinedRoom.Users != null); - await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); - - // Update the stored room (must be done on update thread for thread-safety). await scheduleAsync(() => { - Room = joinedRoom; - apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; - }); + joinCancellationSource?.Cancel(); + joinCancellationSource = cancellationSource; + }, CancellationToken.None); - // Update room settings. - await updateLocalRoomSettings(joinedRoom.Settings); - }); + await joinOrLeaveTaskChain.Add(async () => + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + Debug.Assert(room.RoomID.Value != null); + + // Join the server-side room. + var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + Debug.Assert(joinedRoom != null); + + // Populate users. + Debug.Assert(joinedRoom.Users != null); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); + + // Update the stored room (must be done on update thread for thread-safety). + await scheduleAsync(() => + { + Room = joinedRoom; + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + }, cancellationSource.Token); + + // Update room settings. + await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token); + }, cancellationSource.Token); + } /// /// Joins the with a given ID. @@ -151,14 +163,15 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { - if (Room == null) - return Task.FromCanceled(new CancellationToken(true)); - // Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background. // However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed. // For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time. var scheduledReset = scheduleAsync(() => { + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. + // This includes the setting of Room itself along with the initial update of the room settings on join. + joinCancellationSource?.Cancel(); + apiRoom = null; Room = null; CurrentMatchPlayingUserIds.Clear(); @@ -169,7 +182,7 @@ namespace osu.Game.Online.Multiplayer return joinOrLeaveTaskChain.Add(async () => { await scheduledReset; - await LeaveRoomInternal(); + await LeaveRoomInternal().CatchUnobservedExceptions(); }); } @@ -455,7 +468,8 @@ namespace osu.Game.Online.Multiplayer /// This updates both the joined and the respective API . /// /// The new to update from. - private Task updateLocalRoomSettings(MultiplayerRoomSettings settings) => scheduleAsync(() => + /// The to cancel the update. + private Task updateLocalRoomSettings(MultiplayerRoomSettings settings, CancellationToken cancellationToken = default) => scheduleAsync(() => { if (Room == null) return; @@ -473,10 +487,17 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => updatePlaylist(settings, res); + + req.Success += res => + { + if (cancellationToken.IsCancellationRequested) + return; + + updatePlaylist(settings, res); + }; api.Queue(req); - }); + }, cancellationToken); private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) { @@ -524,12 +545,15 @@ namespace osu.Game.Online.Multiplayer CurrentMatchPlayingUserIds.Remove(userId); } - private Task scheduleAsync(Action action) + private Task scheduleAsync(Action action, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(); Scheduler.Add(() => { + if (cancellationToken.IsCancellationRequested) + return; + try { action();