diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs
index ad1e7ebbaf..7102554ae9 100644
--- a/osu.Game/Online/Metadata/IMetadataClient.cs
+++ b/osu.Game/Online/Metadata/IMetadataClient.cs
@@ -2,11 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
+using osu.Game.Users;
namespace osu.Game.Online.Metadata
{
- public interface IMetadataClient
+ ///
+ /// Interface for metadata-related remote procedure calls to be executed on the client side.
+ ///
+ public interface IMetadataClient : IStatefulUserHubClient
{
+ ///
+ /// Delivers the set of requested to the client.
+ ///
Task BeatmapSetsUpdated(BeatmapUpdates updates);
+
+ ///
+ /// Delivers an update of the of the user with the supplied .
+ ///
+ Task UserPresenceUpdated(int userId, UserPresence? status);
}
}
diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs
index 994f60f877..9780045333 100644
--- a/osu.Game/Online/Metadata/IMetadataServer.cs
+++ b/osu.Game/Online/Metadata/IMetadataServer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
+using osu.Game.Users;
namespace osu.Game.Online.Metadata
{
@@ -17,5 +18,25 @@ namespace osu.Game.Online.Metadata
/// The last processed queue ID.
///
Task GetChangesSince(int queueId);
+
+ ///
+ /// Signals to the server that the current user's has changed.
+ ///
+ Task UpdateActivity(UserActivity? activity);
+
+ ///
+ /// Signals to the server that the current user's has changed.
+ ///
+ Task UpdateStatus(UserStatus? status);
+
+ ///
+ /// Signals to the server that the current user would like to begin receiving updates on other users' online presence.
+ ///
+ Task BeginWatchingUserPresence();
+
+ ///
+ /// Signals to the server that the current user would like to stop receiving updates on other users' online presence.
+ ///
+ Task EndWatchingUserPresence();
}
}
diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs
index d4e7540fe7..8e99a9b2cb 100644
--- a/osu.Game/Online/Metadata/MetadataClient.cs
+++ b/osu.Game/Online/Metadata/MetadataClient.cs
@@ -4,22 +4,71 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Users;
namespace osu.Game.Online.Metadata
{
public abstract partial class MetadataClient : Component, IMetadataClient, IMetadataServer
{
- public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
+ public abstract IBindable IsConnected { get; }
+
+ #region Beatmap metadata updates
public abstract Task GetChangesSince(int queueId);
- public Action? ChangedBeatmapSetsArrived;
+ public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
+
+ public event Action? ChangedBeatmapSetsArrived;
protected Task ProcessChanges(int[] beatmapSetIDs)
{
ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray());
return Task.CompletedTask;
}
+
+ #endregion
+
+ #region User presence updates
+
+ ///
+ /// Whether the client is currently receiving user presence updates from the server.
+ ///
+ public abstract IBindable IsWatchingUserPresence { get; }
+
+ ///
+ /// Dictionary keyed by user ID containing all of the information about currently online users received from the server.
+ ///
+ public abstract IBindableDictionary UserStates { get; }
+
+ ///
+ public abstract Task UpdateActivity(UserActivity? activity);
+
+ ///
+ public abstract Task UpdateStatus(UserStatus? status);
+
+ ///
+ public abstract Task BeginWatchingUserPresence();
+
+ ///
+ public abstract Task EndWatchingUserPresence();
+
+ ///
+ public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
+
+ #endregion
+
+ #region Disconnection handling
+
+ public event Action? Disconnecting;
+
+ public virtual Task DisconnectRequested()
+ {
+ Schedule(() => Disconnecting?.Invoke());
+ return Task.CompletedTask;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
index 57311419f7..27093d7961 100644
--- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
@@ -10,17 +11,32 @@ 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.Users;
namespace osu.Game.Online.Metadata
{
public partial class OnlineMetadataClient : MetadataClient
{
+ public override IBindable IsConnected { get; } = new Bindable();
+
+ public override IBindable IsWatchingUserPresence => isWatchingUserPresence;
+ private readonly BindableBool isWatchingUserPresence = new BindableBool();
+
+ // ReSharper disable once InconsistentlySynchronizedField
+ public override IBindableDictionary UserStates => userStates;
+ private readonly BindableDictionary userStates = new BindableDictionary();
+
private readonly string endpoint;
private IHubClientConnector? connector;
private Bindable lastQueueId = null!;
+ private IBindable localUser = null!;
+ private IBindable userActivity = null!;
+ private IBindable? userStatus;
+
private HubConnection? connection => connector?.CurrentConnection;
public OnlineMetadataClient(EndpointConfiguration endpoints)
@@ -33,7 +49,7 @@ namespace osu.Game.Online.Metadata
{
// 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);
+ connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint, false);
if (connector != null)
{
@@ -42,12 +58,37 @@ namespace osu.Game.Online.Metadata
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
+ connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
};
- connector.IsConnected.BindValueChanged(isConnectedChanged, true);
+ IsConnected.BindTo(connector.IsConnected);
+ IsConnected.BindValueChanged(isConnectedChanged, true);
}
lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId);
+
+ localUser = api.LocalUser.GetBoundCopy();
+ userActivity = api.Activity.GetBoundCopy()!;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ localUser.BindValueChanged(_ =>
+ {
+ if (localUser.Value is not GuestUser)
+ {
+ userStatus = localUser.Value.Status.GetBoundCopy();
+ userStatus.BindValueChanged(status => UpdateStatus(status.NewValue), true);
+ }
+ else
+ userStatus = null;
+ }, true);
+ userActivity.BindValueChanged(activity =>
+ {
+ if (localUser.Value is not GuestUser)
+ UpdateActivity(activity.NewValue);
+ }, true);
}
private bool catchingUp;
@@ -55,7 +96,17 @@ namespace osu.Game.Online.Metadata
private void isConnectedChanged(ValueChangedEvent connected)
{
if (!connected.NewValue)
+ {
+ isWatchingUserPresence.Value = false;
+ userStates.Clear();
return;
+ }
+
+ if (localUser.Value is not GuestUser)
+ {
+ UpdateActivity(userActivity.Value);
+ UpdateStatus(userStatus?.Value);
+ }
if (lastQueueId.Value >= 0)
{
@@ -116,6 +167,71 @@ namespace osu.Game.Online.Metadata
return connection.InvokeAsync(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);
+ }
+
+ public override Task UserPresenceUpdated(int userId, UserPresence? presence)
+ {
+ lock (userStates)
+ {
+ if (presence != null)
+ userStates[userId] = presence.Value;
+ else
+ userStates.Remove(userId);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public override async Task BeginWatchingUserPresence()
+ {
+ if (connector?.IsConnected.Value != true)
+ throw new OperationCanceledException();
+
+ Debug.Assert(connection != null);
+ await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
+ isWatchingUserPresence.Value = true;
+ }
+
+ public override async Task EndWatchingUserPresence()
+ {
+ try
+ {
+ if (connector?.IsConnected.Value != true)
+ throw new OperationCanceledException();
+
+ // must happen synchronously before any remote calls to avoid misordering.
+ userStates.Clear();
+ Debug.Assert(connection != null);
+ await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
+ }
+ finally
+ {
+ isWatchingUserPresence.Value = false;
+ }
+ }
+
+ public override async Task DisconnectRequested()
+ {
+ await base.DisconnectRequested().ConfigureAwait(false);
+ await EndWatchingUserPresence().ConfigureAwait(false);
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs
index 0d846f7d27..c36e4ab894 100644
--- a/osu.Game/Online/OnlineStatusNotifier.cs
+++ b/osu.Game/Online/OnlineStatusNotifier.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Online.API;
+using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@@ -30,6 +31,9 @@ namespace osu.Game.Online
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
+ [Resolved]
+ private MetadataClient metadataClient { get; set; } = null!;
+
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
@@ -56,6 +60,7 @@ namespace osu.Game.Online
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
+ metadataClient.Disconnecting += notifyAboutForcedDisconnection;
}
protected override void LoadComplete()
@@ -131,6 +136,9 @@ namespace osu.Game.Online
if (multiplayerClient.IsNotNull())
multiplayerClient.Disconnecting -= notifyAboutForcedDisconnection;
+
+ if (metadataClient.IsNotNull())
+ metadataClient.Disconnecting -= notifyAboutForcedDisconnection;
}
}
}
diff --git a/osu.Game/Users/UserPresence.cs b/osu.Game/Users/UserPresence.cs
new file mode 100644
index 0000000000..dff40a9889
--- /dev/null
+++ b/osu.Game/Users/UserPresence.cs
@@ -0,0 +1,28 @@
+// 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 MessagePack;
+
+namespace osu.Game.Users
+{
+ ///
+ /// Structure containing all relevant information about a user's online presence.
+ ///
+ [Serializable]
+ [MessagePackObject]
+ public struct UserPresence
+ {
+ ///
+ /// The user's current activity.
+ ///
+ [Key(0)]
+ public UserActivity? Activity { get; set; }
+
+ ///
+ /// The user's current status.
+ ///
+ [Key(1)]
+ public UserStatus? Status { get; set; }
+ }
+}