diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs
index af4e88a05c..f3bcced630 100644
--- a/osu.Game/Online/EndpointConfiguration.cs
+++ b/osu.Game/Online/EndpointConfiguration.cs
@@ -39,5 +39,10 @@ namespace osu.Game.Online
/// The endpoint for the SignalR multiplayer server.
///
public string MultiplayerEndpointUrl { get; set; }
+
+ ///
+ /// The endpoint for the SignalR metadata server.
+ ///
+ public string MetadataEndpointUrl { get; set; }
}
}
diff --git a/osu.Game/Online/Metadata/BeatmapUpdates.cs b/osu.Game/Online/Metadata/BeatmapUpdates.cs
new file mode 100644
index 0000000000..8814e35e1f
--- /dev/null
+++ b/osu.Game/Online/Metadata/BeatmapUpdates.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.Online.Metadata
+{
+ ///
+ /// Describes a set of beatmaps which have been updated in some way.
+ ///
+ [MessagePackObject]
+ [Serializable]
+ public class BeatmapUpdates
+ {
+ [Key(0)]
+ public int[] BeatmapSetIDs { get; set; }
+
+ [Key(1)]
+ public uint LastProcessedQueueID { get; set; }
+
+ public BeatmapUpdates(int[] beatmapSetIDs, uint lastProcessedQueueID)
+ {
+ BeatmapSetIDs = beatmapSetIDs;
+ LastProcessedQueueID = lastProcessedQueueID;
+ }
+ }
+}
diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs
new file mode 100644
index 0000000000..ad1e7ebbaf
--- /dev/null
+++ b/osu.Game/Online/Metadata/IMetadataClient.cs
@@ -0,0 +1,12 @@
+// 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.Metadata
+{
+ public interface IMetadataClient
+ {
+ Task BeatmapSetsUpdated(BeatmapUpdates updates);
+ }
+}
diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs
new file mode 100644
index 0000000000..edf288b6cf
--- /dev/null
+++ b/osu.Game/Online/Metadata/IMetadataServer.cs
@@ -0,0 +1,21 @@
+// 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.Metadata
+{
+ ///
+ /// Metadata server is responsible for keeping the osu! client up-to-date with any changes.
+ ///
+ public interface IMetadataServer
+ {
+ ///
+ /// Get any changes since a specific point in the queue.
+ /// Should be used to allow the client to catch up with any changes after being closed or disconnected.
+ ///
+ /// The last processed queue ID.
+ ///
+ Task GetChangesSince(uint queueId);
+ }
+}
diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs
new file mode 100644
index 0000000000..65817b3152
--- /dev/null
+++ b/osu.Game/Online/Metadata/MetadataClient.cs
@@ -0,0 +1,15 @@
+// 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;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Online.Metadata
+{
+ public abstract class MetadataClient : Component, IMetadataClient, IMetadataServer
+ {
+ public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
+
+ public abstract Task GetChangesSince(uint queueId);
+ }
+}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
new file mode 100644
index 0000000000..cfcebf73d3
--- /dev/null
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -0,0 +1,61 @@
+// 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 System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online.Metadata
+{
+ public class OnlineMetadataClient : MetadataClient
+ {
+ private readonly string endpoint;
+
+ private IHubClientConnector? connector;
+
+ private HubConnection? connection => connector?.CurrentConnection;
+
+ public OnlineMetadataClient(EndpointConfiguration endpoints)
+ {
+ endpoint = endpoints.MetadataEndpointUrl;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IAPIProvider api)
+ {
+ // 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);
+
+ if (connector != null)
+ {
+ connector.ConfigureConnection = connection =>
+ {
+ // this is kind of SILLY
+ // https://github.com/dotnet/aspnetcore/issues/15198
+ connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
+ };
+ }
+ }
+
+ public override Task BeatmapSetsUpdated(BeatmapUpdates updates)
+ {
+ Logger.Log($"Received beatmap updates {updates.BeatmapSetIDs.Length} updates with last id {updates.LastProcessedQueueID}");
+ return Task.CompletedTask;
+ }
+
+ public override Task GetChangesSince(uint queueId)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ connector?.Dispose();
+ }
+ }
+}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index ead3eeb0dc..bf3cc16728 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -41,6 +40,7 @@ using osu.Game.IO;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
+using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@@ -52,6 +52,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
+using File = System.IO.File;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
@@ -180,6 +181,8 @@ namespace osu.Game
private MultiplayerClient multiplayerClient;
+ private MetadataClient metadataClient;
+
private RealmAccess realm;
protected override Container Content => content;
@@ -265,6 +268,7 @@ namespace osu.Game
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
+ dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
@@ -316,8 +320,10 @@ namespace osu.Game
// add api components to hierarchy.
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
+
AddInternal(spectatorClient);
AddInternal(multiplayerClient);
+ AddInternal(metadataClient);
AddInternal(rulesetConfigCache);