diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 8fd79bd703..f5a8678613 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -102,6 +102,8 @@ namespace osu.Game.Online return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection)); } + Task IHubClientConnector.Disconnect() => base.Disconnect(); + protected override string ClientName { get; } } } diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs index 53c4897e73..052972e6b4 100644 --- a/osu.Game/Online/IHubClientConnector.cs +++ b/osu.Game/Online/IHubClientConnector.cs @@ -30,6 +30,11 @@ namespace osu.Game.Online /// public Action? ConfigureConnection { get; set; } + /// + /// Forcefully disconnects the client from the server. + /// + Task Disconnect(); + /// /// Reconnect if already connected. /// diff --git a/osu.Game/Online/IStatefulUserHubClient.cs b/osu.Game/Online/IStatefulUserHubClient.cs new file mode 100644 index 0000000000..86105dd629 --- /dev/null +++ b/osu.Game/Online/IStatefulUserHubClient.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; + +namespace osu.Game.Online +{ + /// + /// Common interface for clients of "stateful user hubs", i.e. server-side hubs + /// that preserve user state. + /// In the case of such hubs, concurrency constraints are enforced (only one client + /// can be connected at a time). + /// + public interface IStatefulUserHubClient + { + Task DisconnectRequested(); + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 327fb0d76a..a5fa49a94b 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -13,7 +13,7 @@ namespace osu.Game.Online.Multiplayer /// /// An interface defining a multiplayer client instance. /// - public interface IMultiplayerClient + public interface IMultiplayerClient : IStatefulUserHubClient { /// /// Signals that the room has changed state. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 515a0dda08..4b351663d8 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -357,6 +357,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + public abstract Task DisconnectInternal(); + /// /// Change the local user's mods in the currently joined room. /// @@ -876,5 +878,11 @@ namespace osu.Game.Online.Multiplayer return tcs.Task; } + + Task IStatefulUserHubClient.DisconnectRequested() + { + Schedule(() => DisconnectInternal()); + return Task.CompletedTask; + } } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 20ec030eac..e400132693 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -68,6 +68,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -255,6 +256,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + public override Task DisconnectInternal() + { + if (connector == null) + return Task.CompletedTask; + + return connector.Disconnect(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index e33924047d..8c1b58a750 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -159,6 +159,8 @@ namespace osu.Game.Online await Task.Run(connect, default).ConfigureAwait(false); } + protected Task Disconnect() => disconnect(true); + private async Task disconnect(bool takeLock) { cancelExistingConnect(); diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 9605604966..2dc2283c23 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Spectator /// /// An interface defining a spectator client instance. /// - public interface ISpectatorClient + public interface ISpectatorClient : IStatefulUserHubClient { /// /// Signals that a user has begun a new play session. diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 3118e05053..cd68abdea6 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -42,6 +42,7 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -113,5 +114,13 @@ namespace osu.Game.Online.Spectator return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } + + protected override Task DisconnectInternal() + { + if (connector == null) + return Task.CompletedTask; + + return connector.Disconnect(); + } } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 14e137caf1..9c78f27e15 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -174,6 +174,12 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } + Task IStatefulUserHubClient.DisconnectRequested() + { + Schedule(() => DisconnectInternal()); + return Task.CompletedTask; + } + public void BeginPlaying(long? scoreToken, GameplayState state, Score score) { // This schedule is only here to match the one below in `EndPlaying`. @@ -291,6 +297,8 @@ namespace osu.Game.Online.Spectator protected abstract Task StopWatchingUserInternal(int userId); + protected abstract Task DisconnectInternal(); + protected override void Update() { base.Update(); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6007c7c076..577104db45 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -658,5 +658,11 @@ namespace osu.Game.Tests.Visual.Multiplayer PlayedAt = item.PlayedAt, StarRating = item.Beatmap.StarRating, }; + + public override Task DisconnectInternal() + { + isConnected.Value = false; + return Task.CompletedTask; + } } } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 5db08810ca..ce2eee8aa4 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Spectator public int FrameSendAttempts { get; private set; } - public override IBindable IsConnected { get; } = new Bindable(true); + public override IBindable IsConnected => isConnected; + private readonly BindableBool isConnected = new BindableBool(true); public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; @@ -179,5 +180,11 @@ namespace osu.Game.Tests.Visual.Spectator State = SpectatedUserState.Playing }); } + + protected override Task DisconnectInternal() + { + isConnected.Value = false; + return Task.CompletedTask; + } } }