diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index e7494e50cc..9d414deade 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -100,7 +100,11 @@ namespace osu.Game.Online return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection)); } - Task IHubClientConnector.Disconnect() => base.Disconnect(); + async Task IHubClientConnector.Disconnect() + { + await Disconnect().ConfigureAwait(false); + API.Logout(); + } protected override string ClientName { get; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4b351663d8..79f46c2095 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -12,7 +12,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -88,6 +87,11 @@ namespace osu.Game.Online.Multiplayer /// public event Action? ResultsReady; + /// + /// Invoked just prior to disconnection requested by the server via . + /// + public event Action? Disconnecting; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -155,10 +159,7 @@ namespace osu.Game.Online.Multiplayer { // clean up local room state on server disconnect. if (!connected.NewValue && Room != null) - { - Logger.Log("Clearing room due to multiplayer server connection loss.", LoggingTarget.Runtime, LogLevel.Important); LeaveRoom(); - } })); } @@ -881,7 +882,11 @@ namespace osu.Game.Online.Multiplayer Task IStatefulUserHubClient.DisconnectRequested() { - Schedule(() => DisconnectInternal()); + Schedule(() => + { + Disconnecting?.Invoke(); + DisconnectInternal(); + }); return Task.CompletedTask; } } diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs new file mode 100644 index 0000000000..0cf672ac3c --- /dev/null +++ b/osu.Game/Online/OnlineStatusNotifier.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Spectator; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.OnlinePlay; + +namespace osu.Game.Online +{ + public partial class OnlineStatusNotifier : Component + { + private readonly Func getCurrentScreen; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + + [Resolved] + private SpectatorClient spectatorClient { get; set; } = null!; + + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + private IBindable apiState = null!; + private IBindable multiplayerState = null!; + private IBindable spectatorState = null!; + private bool forcedDisconnection; + + public OnlineStatusNotifier(Func getCurrentScreen) + { + this.getCurrentScreen = getCurrentScreen; + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + apiState = api.State.GetBoundCopy(); + multiplayerState = multiplayerClient.IsConnected.GetBoundCopy(); + spectatorState = spectatorClient.IsConnected.GetBoundCopy(); + + multiplayerClient.Disconnecting += notifyAboutForcedDisconnection; + spectatorClient.Disconnecting += notifyAboutForcedDisconnection; + } + + private void notifyAboutForcedDisconnection() + { + if (forcedDisconnection) + return; + + forcedDisconnection = true; + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "You have been logged out on this device due to a login to your account on another device." + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + apiState.BindValueChanged(_ => + { + if (apiState.Value == APIState.Online) + forcedDisconnection = false; + + Scheduler.AddOnce(updateState); + }); + multiplayerState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + spectatorState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + } + + private void updateState() + { + if (forcedDisconnection) + return; + + if (apiState.Value == APIState.Offline && getCurrentScreen() is OnlinePlayScreen) + { + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "API connection was lost. Can't continue with online play." + }); + return; + } + + if (!multiplayerClient.IsConnected.Value && multiplayerClient.Room != null) + { + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "Connection to the multiplayer server was lost. Exiting multiplayer." + }); + } + + // TODO: handle spectator server failure somehow? + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient.IsNotNull()) + spectatorClient.Disconnecting -= notifyAboutForcedDisconnection; + + if (multiplayerClient.IsNotNull()) + multiplayerClient.Disconnecting -= notifyAboutForcedDisconnection; + } + } +} diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index cd68abdea6..036cfa1d76 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -115,12 +115,14 @@ namespace osu.Game.Online.Spectator return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } - protected override Task DisconnectInternal() + protected override async Task DisconnectInternal() { - if (connector == null) - return Task.CompletedTask; + await base.DisconnectInternal().ConfigureAwait(false); - return connector.Disconnect(); + if (connector == null) + return; + + await connector.Disconnect().ConfigureAwait(false); } } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 9c78f27e15..ca4ec52f4a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -70,6 +70,11 @@ namespace osu.Game.Online.Spectator /// public virtual event Action? OnUserScoreProcessed; + /// + /// Invoked just prior to disconnection requested by the server via . + /// + public event Action? Disconnecting; + /// /// A dictionary containing all users currently being watched, with the number of watching components for each user. /// @@ -297,7 +302,11 @@ namespace osu.Game.Online.Spectator protected abstract Task StopWatchingUserInternal(int userId); - protected abstract Task DisconnectInternal(); + protected virtual Task DisconnectInternal() + { + Disconnecting?.Invoke(); + return Task.CompletedTask; + } protected override void Update() { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2f11964f6a..ed4bd21e93 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1054,6 +1054,7 @@ namespace osu.Game Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); + Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay }; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index f652e88f5a..9de458b5c6 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }, new Header(ScreenTitle, screenStack), RoomManager, - ongoingOperationTracker + ongoingOperationTracker, } }; } @@ -79,10 +79,7 @@ namespace osu.Game.Screens.OnlinePlay private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { if (state.NewValue != APIState.Online) - { - Logger.Log("API connection was lost, can't continue with online play", LoggingTarget.Network, LogLevel.Important); Schedule(forcefullyExit); - } }); protected override void LoadComplete() diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index ce2eee8aa4..5aef85fa13 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -181,10 +181,10 @@ namespace osu.Game.Tests.Visual.Spectator }); } - protected override Task DisconnectInternal() + protected override async Task DisconnectInternal() { + await base.DisconnectInternal().ConfigureAwait(false); isConnected.Value = false; - return Task.CompletedTask; } } }