diff --git a/osu.Android.props b/osu.Android.props
index 5cf59decec..1131203a95 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index bc2902480d..710855a605 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -30,6 +30,8 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
@@ -594,9 +596,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- pressReadyButton();
- pressReadyButton();
- AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
+ enterGameplay();
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
@@ -656,23 +656,172 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
+ [Test]
+ public void TestSpectatingStateResetOnBackButtonDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
+
+ pressReadyButton(1234);
+ AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
+
+ AddStep("press back button and exit", () =>
+ {
+ multiplayerScreenStack.OnBackButton();
+ multiplayerScreenStack.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen());
+ AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
+ }
+
+ [Test]
+ public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
+
+ pressReadyButton(1234);
+ AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
+ AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
+ AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay));
+
+ AddStep("press back button and exit", () =>
+ {
+ multiplayerScreenStack.OnBackButton();
+ multiplayerScreenStack.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen());
+ AddWaitStep("wait for possible state change", 5);
+ AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+ }
+
+ [Test]
+ public void TestItemAddedByOtherUserDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ enterGameplay();
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+ })));
+
+ AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
+
+ AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2);
+ }
+
+ [Test]
+ public void TestItemAddedAndDeletedByOtherUserDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ enterGameplay();
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+ })));
+
+ AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
+
+ AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2));
+ AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1);
+
+ AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0);
+ }
+
+ private void enterGameplay()
+ {
+ pressReadyButton();
+ pressReadyButton();
+ AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
+ }
+
private ReadyButton readyButton => this.ChildrenOfType().Single();
- private void pressReadyButton()
+ private void pressReadyButton(int? playingUserId = null)
{
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
MultiplayerUserState lastState = MultiplayerUserState.Idle;
+ MultiplayerRoomUser user = null;
AddStep("click ready button", () =>
{
- lastState = client.LocalUser?.State ?? MultiplayerUserState.Idle;
+ user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId);
+ lastState = user?.State ?? MultiplayerUserState.Idle;
InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
});
- AddUntilStep("wait for state change", () => client.LocalUser?.State != lastState);
+ AddUntilStep("wait for state change", () => user?.State != lastState);
}
private void createRoom(Func room)
diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
index 7f1171db1f..370f3bd0ae 100644
--- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual
((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
- public override bool OnBackButton() => multiplayerScreen.OnBackButton();
+ public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton();
public override bool OnExiting(IScreen next)
{
diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
index 36288c745a..3d565a4464 100644
--- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
@@ -1,10 +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 osu.Framework.Extensions;
+
namespace osu.Game.Graphics.UserInterface
{
public class OsuNumberBox : OsuTextBox
{
- protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+ protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
}
}
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
index 73fda78d00..073d512f90 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
@@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer
/// If an attempt to start the game occurs when the game's (or users') state disallows it.
Task StartMatch();
+ ///
+ /// Aborts an ongoing gameplay load.
+ ///
+ Task AbortGameplay();
+
///
/// Adds an item to the playlist.
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index f366de557f..903aaa89e3 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -327,6 +327,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task StartMatch();
+ public abstract Task AbortGameplay();
+
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index f911ef3121..3794bec228 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -154,6 +154,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
+ public override Task AbortGameplay()
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay));
+ }
+
public override Task AddPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index cbe9f7fc64..cc4446033a 100644
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings
private class OutlinedNumberBox : OutlinedTextBox
{
- protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+ protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
public new void NotifyInputError() => base.NotifyInputError();
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
index 4971489769..7b90532cce 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
@@ -121,7 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
private void addItemToLists(MultiplayerPlaylistItem item)
{
- var apiItem = Playlist.Single(i => i.ID == item.ID);
+ var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
+
+ // Item could have been removed from the playlist while the local player was in gameplay.
+ if (apiItem == null)
+ return;
if (item.Expired)
historyList.Items.Add(apiItem);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index 58b5b7bbeb..e136627d43 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
@@ -18,8 +19,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.OnResuming(last);
- if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating)
- client.ChangeState(MultiplayerUserState.Idle);
+ if (client.Room == null)
+ return;
+
+ Debug.Assert(client.LocalUser != null);
+
+ switch (client.LocalUser.State)
+ {
+ case MultiplayerUserState.Spectating:
+ break;
+
+ case MultiplayerUserState.WaitingForLoad:
+ case MultiplayerUserState.Loaded:
+ case MultiplayerUserState.Playing:
+ client.AbortGameplay();
+ break;
+
+ default:
+ client.ChangeState(MultiplayerUserState.Idle);
+ break;
+ }
}
protected override string ScreenTitle => "Multiplayer";
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index 9ac64add9a..7350408eba 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@@ -226,8 +227,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool OnBackButton()
{
- // On a manual exit, set the player state back to idle.
- multiplayerClient.ChangeState(MultiplayerUserState.Idle);
+ Debug.Assert(multiplayerClient.Room != null);
+
+ // On a manual exit, set the player back to idle unless gameplay has finished.
+ if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
+ multiplayerClient.ChangeState(MultiplayerUserState.Idle);
+
return base.OnBackButton();
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index d20d6b1d37..4e0cfe405e 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -128,6 +128,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
case MultiplayerRoomState.WaitingForLoad:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
+ var loadedUsers = Room.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
+
+ if (loadedUsers.Length == 0)
+ {
+ // all users have bailed from the load sequence. cancel the game start.
+ ChangeRoomState(MultiplayerRoomState.Open);
+ return;
+ }
+
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
@@ -143,8 +152,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
- ChangeRoomState(MultiplayerRoomState.Open);
+ ChangeRoomState(MultiplayerRoomState.Open);
((IMultiplayerClient)this).ResultsReady();
FinishCurrentItem().Wait();
@@ -242,6 +251,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
+ Debug.Assert(Room != null);
+
+ if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad)
+ return Task.CompletedTask;
+
ChangeUserState(api.LocalUser.Value.Id, newState);
return Task.CompletedTask;
}
@@ -303,6 +317,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
return ((IMultiplayerClient)this).LoadRequested();
}
+ public override Task AbortGameplay()
+ {
+ Debug.Assert(Room != null);
+ Debug.Assert(LocalUser != null);
+
+ ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle);
+
+ return Task.CompletedTask;
+ }
+
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
Debug.Assert(Room != null);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 46064e320b..feae990df7 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index fdb63a19d3..27ac1bf647 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,8 +60,8 @@
-
-
+
+
@@ -83,7 +83,7 @@
-
+