diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index b35dfa11cb..f7a3f4602f 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -257,8 +257,8 @@ namespace osu.Game.Online.API
this.password = password;
}
- public IHubClientConnector GetHubConnector(string clientName, string endpoint) =>
- new HubClientConnector(clientName, endpoint, this, versionHash);
+ public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
+ new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 52f2365165..1ba31db9fa 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Online.API
state.Value = APIState.Offline;
}
- public IHubClientConnector GetHubConnector(string clientName, string endpoint) => null;
+ public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index 3a77b9cfee..5ad5367924 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -102,7 +102,8 @@ namespace osu.Game.Online.API
///
/// The name of the client this connector connects for, used for logging.
/// The endpoint to the hub.
- IHubClientConnector? GetHubConnector(string clientName, string endpoint);
+ /// Whether to use MessagePack for serialisation if available on this platform.
+ IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
///
/// Create a new user account. This is a blocking operation.
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 90049a6501..d2dba8a402 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -26,6 +26,7 @@ namespace osu.Game.Online
private readonly string clientName;
private readonly string endpoint;
private readonly string versionHash;
+ private readonly bool preferMessagePack;
private readonly IAPIProvider api;
///
@@ -51,12 +52,14 @@ namespace osu.Game.Online
/// The endpoint to the hub.
/// An API provider used to react to connection state changes.
/// The hash representing the current game version, used for verification purposes.
- public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash)
+ /// Whether to use MessagePack for serialisation if available on this platform.
+ public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
{
this.clientName = clientName;
this.endpoint = endpoint;
this.api = api;
this.versionHash = versionHash;
+ this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
apiState.BindValueChanged(state =>
@@ -144,13 +147,19 @@ namespace osu.Game.Online
options.Headers.Add("OsuVersionHash", versionHash);
});
- if (RuntimeInfo.SupportsJIT)
+ if (RuntimeInfo.SupportsJIT && preferMessagePack)
builder.AddMessagePackProtocol();
else
{
// eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
- builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
+ builder.AddNewtonsoftJsonProtocol(options =>
+ {
+ options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
+ // TODO: This should only be required to be `TypeNameHandling.Auto`.
+ // See usage in osu-server-spectator for further documentation as to why this is required.
+ options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All;
+ });
}
var newConnection = builder.Build();
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
index 6d7b9d24d6..064065ab00 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
@@ -50,6 +50,25 @@ namespace osu.Game.Online.Multiplayer
/// The new state of the user.
Task UserStateChanged(int userId, MultiplayerUserState state);
+ ///
+ /// Signals that the match type state has changed for a user in this room.
+ ///
+ /// The ID of the user performing a state change.
+ /// The new state of the user.
+ Task MatchUserStateChanged(int userId, MatchUserState state);
+
+ ///
+ /// Signals that the match type state has changed for this room.
+ ///
+ /// The new state of the room.
+ Task MatchRoomStateChanged(MatchRoomState state);
+
+ ///
+ /// Send a match type specific request.
+ ///
+ /// The event to handle.
+ Task MatchEvent(MatchServerEvent e);
+
///
/// Signals that a user in this room changed their beatmap availability state.
///
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
index 3527ce6314..b26c4d8201 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
@@ -55,6 +55,12 @@ namespace osu.Game.Online.Multiplayer
/// The proposed new mods, excluding any required by the room itself.
Task ChangeUserMods(IEnumerable newMods);
+ ///
+ /// Send a match type specific request.
+ ///
+ /// The request to send.
+ Task SendMatchRequest(MatchUserRequest request);
+
///
/// As the host of a room, start the match.
///
diff --git a/osu.Game/Online/Multiplayer/MatchRoomState.cs b/osu.Game/Online/Multiplayer/MatchRoomState.cs
new file mode 100644
index 0000000000..5b662af100
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchRoomState.cs
@@ -0,0 +1,23 @@
+// 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;
+using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// Room-wide state for the current match type.
+ /// Can be used to contain any state which should be used before or during match gameplay.
+ ///
+ [Serializable]
+ [MessagePackObject]
+ [Union(0, typeof(TeamVersusRoomState))]
+ // TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation.
+ public class MatchRoomState
+ {
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs
new file mode 100644
index 0000000000..891fb2cc3b
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs
@@ -0,0 +1,17 @@
+// 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.Multiplayer
+{
+ ///
+ /// An event from the server to allow clients to update gameplay to an expected state.
+ ///
+ [Serializable]
+ [MessagePackObject]
+ public abstract class MatchServerEvent
+ {
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs
new file mode 100644
index 0000000000..9c3b07049c
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.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 MessagePack;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
+{
+ public class ChangeTeamRequest : MatchUserRequest
+ {
+ [Key(0)]
+ public int TeamID { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/MultiplayerTeam.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/MultiplayerTeam.cs
new file mode 100644
index 0000000000..f952dbc1b5
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/MultiplayerTeam.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;
+using MessagePack;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
+{
+ [Serializable]
+ [MessagePackObject]
+ public class MultiplayerTeam
+ {
+ [Key(0)]
+ public int ID { get; set; }
+
+ [Key(1)]
+ public string Name { get; set; } = string.Empty;
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusRoomState.cs
new file mode 100644
index 0000000000..91d1aa43d4
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusRoomState.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using MessagePack;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
+{
+ [MessagePackObject]
+ public class TeamVersusRoomState : MatchRoomState
+ {
+ [Key(0)]
+ public List Teams { get; set; } = new List();
+
+ public static TeamVersusRoomState CreateDefault() =>
+ new TeamVersusRoomState
+ {
+ Teams =
+ {
+ new MultiplayerTeam { ID = 0, Name = "Team Red" },
+ new MultiplayerTeam { ID = 1, Name = "Team Blue" },
+ }
+ };
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs
new file mode 100644
index 0000000000..96a4e2ea99
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.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 MessagePack;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
+{
+ public class TeamVersusUserState : MatchUserState
+ {
+ [Key(0)]
+ public int TeamID { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs
new file mode 100644
index 0000000000..15c3ad0776
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs
@@ -0,0 +1,17 @@
+// 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.Multiplayer
+{
+ ///
+ /// A request from a user to perform an action specific to the current match type.
+ ///
+ [Serializable]
+ [MessagePackObject]
+ public abstract class MatchUserRequest
+ {
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchUserState.cs b/osu.Game/Online/Multiplayer/MatchUserState.cs
new file mode 100644
index 0000000000..f457191bb5
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MatchUserState.cs
@@ -0,0 +1,23 @@
+// 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;
+using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
+
+#nullable enable
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// User specific state for the current match type.
+ /// Can be used to contain any state which should be used before or during match gameplay.
+ ///
+ [Serializable]
+ [MessagePackObject]
+ [Union(0, typeof(TeamVersusUserState))]
+ // TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation.
+ public class MatchUserState
+ {
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 9972d7e88d..873be7f49c 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -293,6 +293,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task ChangeUserMods(IEnumerable newMods);
+ public abstract Task SendMatchRequest(MatchUserRequest request);
+
public abstract Task StartMatch();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
@@ -420,6 +422,46 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
+ Task IMultiplayerClient.MatchUserStateChanged(int userId, MatchUserState state)
+ {
+ if (Room == null)
+ return Task.CompletedTask;
+
+ Scheduler.Add(() =>
+ {
+ if (Room == null)
+ return;
+
+ Room.Users.Single(u => u.UserID == userId).MatchState = state;
+ RoomUpdated?.Invoke();
+ }, false);
+
+ return Task.CompletedTask;
+ }
+
+ Task IMultiplayerClient.MatchRoomStateChanged(MatchRoomState state)
+ {
+ if (Room == null)
+ return Task.CompletedTask;
+
+ Scheduler.Add(() =>
+ {
+ if (Room == null)
+ return;
+
+ Room.MatchState = state;
+ RoomUpdated?.Invoke();
+ }, false);
+
+ return Task.CompletedTask;
+ }
+
+ public Task MatchEvent(MatchServerEvent e)
+ {
+ // not used by any match types just yet.
+ return Task.CompletedTask;
+ }
+
Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability)
{
if (Room == null)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
index c5fa6253ed..175c0e0e27 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Online.Multiplayer
/// All users currently in this room.
///
[Key(3)]
- public List Users { get; set; } = new List();
+ public IList Users { get; set; } = new List();
///
/// The host of this room, in control of changing room settings.
@@ -47,6 +47,9 @@ namespace osu.Game.Online.Multiplayer
[Key(4)]
public MultiplayerRoomUser? Host { get; set; }
+ [Key(5)]
+ public MatchRoomState? MatchState { get; set; }
+
[JsonConstructor]
[SerializationConstructor]
public MultiplayerRoom(long roomId)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
index 4e94c5982f..706bc750d3 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
namespace osu.Game.Online.Multiplayer
{
@@ -39,6 +40,9 @@ namespace osu.Game.Online.Multiplayer
[Key(7)]
public string Password { get; set; } = string.Empty;
+ [Key(8)]
+ public MatchType MatchType { get; set; }
+
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
@@ -47,7 +51,8 @@ namespace osu.Game.Online.Multiplayer
&& RulesetID == other.RulesetID
&& Password.Equals(other.Password, StringComparison.Ordinal)
&& Name.Equals(other.Name, StringComparison.Ordinal)
- && PlaylistItemId == other.PlaylistItemId;
+ && PlaylistItemId == other.PlaylistItemId
+ && MatchType == other.MatchType;
public override string ToString() => $"Name:{Name}"
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
@@ -55,6 +60,7 @@ namespace osu.Game.Online.Multiplayer
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}"
+ $" Ruleset:{RulesetID}"
+ + $" Type:{MatchType}"
+ $" Item:{PlaylistItemId}";
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index a49a8f083c..5d11e2921a 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Online.Multiplayer
[Key(1)]
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
+ [Key(4)]
+ public MatchUserState? MatchState { get; set; }
+
///
/// The availability state of the current beatmap.
///
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 726e26ebe1..8b8d10ce4f 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -37,7 +37,9 @@ namespace osu.Game.Online.Multiplayer
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
- connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint);
+ // 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(OnlineMultiplayerClient), endpoint, false);
if (connector != null)
{
@@ -56,6 +58,9 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
+ connection.On(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged);
+ connection.On(nameof(IMultiplayerClient.MatchUserStateChanged), ((IMultiplayerClient)this).MatchUserStateChanged);
+ connection.On(nameof(IMultiplayerClient.MatchEvent), ((IMultiplayerClient)this).MatchEvent);
};
IsConnected.BindTo(connector.IsConnected);
@@ -118,6 +123,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
}
+ public override Task SendMatchRequest(MatchUserRequest request)
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.SendMatchRequest), request);
+ }
+
public override Task StartMatch()
{
if (!IsConnected.Value)
diff --git a/osu.Game/Online/Rooms/MatchType.cs b/osu.Game/Online/Rooms/MatchType.cs
index cafa147a61..36f0dc0c81 100644
--- a/osu.Game/Online/Rooms/MatchType.cs
+++ b/osu.Game/Online/Rooms/MatchType.cs
@@ -7,6 +7,8 @@ namespace osu.Game.Online.Rooms
{
public enum MatchType
{
+ // used for osu-web deserialization so names shouldn't be changed.
+
Playlists,
[Description("Head to head")]
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index fe7455d964..4bd5b1a788 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -64,6 +64,15 @@ namespace osu.Game.Online.Rooms
[JsonIgnore]
public readonly Bindable Type = new Bindable();
+ // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106)
+ [JsonConverter(typeof(SnakeCaseStringEnumConverter))]
+ [JsonProperty("type")]
+ private MatchType type
+ {
+ get => Type.Value;
+ set => Type.Value = value;
+ }
+
[Cached]
[JsonIgnore]
public readonly Bindable MaxParticipants = new Bindable();
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
index baf9570209..2a40a61257 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
- bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
+ bool enableButton = Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 3349d670c8..7deecdfa28 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -192,6 +192,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
+ public override Task SendMatchRequest(MatchUserRequest request) => Task.CompletedTask;
+
public override Task StartMatch()
{
Debug.Assert(Room != null);