diff --git a/osu.Android.props b/osu.Android.props
index f89994cd56..04c543750e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 3563869d8b..8f6ba6375f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -871,6 +871,53 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0);
}
+ [Test]
+ public void TestGameplayStartsWhileInSpectatorScreen()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("join other user and make host", () =>
+ {
+ client.AddUser(new APIUser { Id = 1234 });
+ client.TransferHost(1234);
+ });
+
+ AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
+ runGameplay();
+
+ AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle));
+ AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open);
+
+ runGameplay();
+
+ void runGameplay()
+ {
+ AddStep("start match by other user", () =>
+ {
+ client.ChangeUserState(1234, MultiplayerUserState.Ready);
+ client.StartMatch();
+ });
+
+ AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
+ AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing);
+ AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen);
+ }
+ }
+
private void enterGameplay()
{
pressReadyButton();
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 4bd68f2034..a397493bab 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -457,6 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
+ // The beatmap is queried asynchronously when the selected item changes.
+ // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks
+ // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback.
+ // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()).
+ if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault))
+ return;
+
StartPlay();
readyClickOperation?.Dispose();
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 50cef71b26..83c3593edb 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 9ec0f1c0a0..b0c056ea21 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,7 +60,7 @@
-
+
@@ -83,7 +83,7 @@
-
+