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;
}
}
}