diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 312281ac18..e05580fed6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); - AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted()); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted()); } [Test] diff --git a/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs new file mode 100644 index 0000000000..4ec5019a07 --- /dev/null +++ b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.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 MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// A started by the server when clients being to load. + /// Indicates how long until gameplay will forcefully start, excluding any users which have not completed loading, + /// and forcing progression of any clients that are blocking load due to user interaction. + /// + [MessagePackObject] + public class ForceGameplayStartCountdown : MultiplayerCountdown + { + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 3e6821b1cd..2f454ea835 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -93,14 +93,20 @@ namespace osu.Game.Online.Multiplayer Task UserModsChanged(int userId, IEnumerable mods); /// - /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. + /// Signals that the match is starting and the loading of gameplay should be started. This will *only* be sent to clients which are to begin loading at this point. /// Task LoadRequested(); /// - /// Signals that a match has started. All users in the state should begin gameplay as soon as possible. + /// Signals that loading of gameplay is to be aborted. /// - Task MatchStarted(); + Task LoadAborted(); + + /// + /// Signals that gameplay has started. + /// All users in the or states should begin gameplay as soon as possible. + /// + Task GameplayStarted(); /// /// Signals that the match has ended, all players have finished and results are ready to be displayed. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 967220abbf..cae675b406 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -69,10 +69,15 @@ namespace osu.Game.Online.Multiplayer /// public virtual event Action? LoadRequested; + /// + /// Invoked when the multiplayer server requests loading of play to be aborted. + /// + public event Action? LoadAborted; + /// /// Invoked when the multiplayer server requests gameplay to be started. /// - public event Action? MatchStarted; + public event Action? GameplayStarted; /// /// Invoked when the multiplayer server has finished collating results. @@ -604,14 +609,27 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.MatchStarted() + Task IMultiplayerClient.LoadAborted() { Scheduler.Add(() => { if (Room == null) return; - MatchStarted?.Invoke(); + LoadAborted?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.GameplayStarted() + { + Scheduler.Add(() => + { + if (Room == null) + return; + + GameplayStarted?.Invoke(); }, false); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index 81190e64c9..dbf2ab667b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -14,6 +14,7 @@ namespace osu.Game.Online.Multiplayer /// [MessagePackObject] [Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(1, typeof(ForceGameplayStartCountdown))] public abstract class MultiplayerCountdown { /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f0b7dcbff8..50e539e8a6 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -65,5 +65,21 @@ namespace osu.Game.Online.Multiplayer } public override int GetHashCode() => UserID.GetHashCode(); + + /// + /// Whether this user has finished loading and can start gameplay. + /// + public bool CanStartGameplay() + { + switch (State) + { + case MultiplayerUserState.Loaded: + case MultiplayerUserState.ReadyForGameplay: + return true; + + default: + return false; + } + } } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs index c467ff84bb..d1369a7970 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs @@ -29,10 +29,16 @@ namespace osu.Game.Online.Multiplayer WaitingForLoad, /// - /// The user's client has marked itself as loaded and ready to begin gameplay. + /// The user has marked itself as loaded, but may still be adjusting settings prior to being ready for gameplay. + /// Players remaining in this state for an extended period of time will be automatically transitioned to the state by the server. /// Loaded, + /// + /// The user has finished adjusting settings and is ready to start gameplay. + /// + ReadyForGameplay, + /// /// The user is currently playing in a game. This is a reserved state, and is set by the server. /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 7e62908ecd..4dc23d8b85 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -54,7 +54,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); - connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); + connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 156f916cef..d1f0ba725f 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -24,7 +24,8 @@ namespace osu.Game.Online (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), (typeof(TeamVersusUserState), typeof(MatchUserState)), - (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)) + (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)), + (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)) }; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index c84fcff11e..1a51aebb76 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onRoomUpdated() => Scheduler.AddOnce(() => { - bool countdownActive = multiplayerClient.Room?.Countdown != null; + bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown; if (countdownActive) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 22a0243f8f..a7e18622dc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -55,7 +55,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onRoomUpdated() => Scheduler.AddOnce(() => { - if (countdown != room?.Countdown) + MultiplayerCountdown newCountdown; + + switch (room?.Countdown) + { + case MatchStartCountdown _: + newCountdown = room.Countdown; + break; + + // Clear the countdown with any other (including non-null) countdown values. + default: + newCountdown = null; + break; + } + + if (newCountdown != countdown) { countdown = room?.Countdown; countdownChangeTime = Time.Current; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 66f6935bcc..53d081a108 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; @@ -20,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); client.RoomUpdated += onRoomUpdated; + client.LoadAborted += onLoadAborted; onRoomUpdated(); } @@ -35,6 +37,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer transitionFromResults(); } + private void onLoadAborted() + { + // If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens. + if (!this.IsCurrentScreen()) + { + Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + this.MakeCurrent(); + } + } + public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); @@ -42,9 +54,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) return; + Debug.Assert(client.LocalUser != null); + if (!(e.Last is MultiplayerPlayerLoader playerLoader)) return; + // Nothing needs to be done if already in the idle state (e.g. via load being aborted by the server). + if (client.LocalUser.State == MultiplayerUserState.Idle) + return; + // If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay. if (!playerLoader.GameplayPassed) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 70f8f1b752..02ff040a94 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!ValidForResume) return; // token retrieval may have failed. - client.MatchStarted += onMatchStarted; + client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; ScoreProcessor.HasCompleted.BindValueChanged(completed => @@ -144,10 +144,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void StartGameplay() { - // block base call, but let the server know we are ready to start. - loadingDisplay.Show(); - - client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + if (client.LocalUser?.State == MultiplayerUserState.Loaded) + { + // block base call, but let the server know we are ready to start. + loadingDisplay.Show(); + client.ChangeState(MultiplayerUserState.ReadyForGameplay); + } } private void failAndBail(string message = null) @@ -175,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); } - private void onMatchStarted() => Scheduler.Add(() => + private void onGameplayStarted() => Scheduler.Add(() => { if (!this.IsCurrentScreen()) return; @@ -223,7 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) { - client.MatchStarted -= onMatchStarted; + client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 53dea83f18..7f01bd64ab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -11,6 +15,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public bool GameplayPassed => player?.GameplayState.HasPassed == true; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + private Player player; public MultiplayerPlayerLoader(Func createPlayer) @@ -18,6 +25,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { } + protected override bool ReadyForGameplay => + base.ReadyForGameplay + // The server is forcefully starting gameplay. + || multiplayerClient.LocalUser?.State == MultiplayerUserState.Playing; + + protected override void OnPlayerLoaded() + { + base.OnPlayerLoaded(); + + multiplayerClient.ChangeState(MultiplayerUserState.Loaded) + .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + } + + private void failAndBail(string message = null) + { + if (!string.IsNullOrEmpty(message)) + Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important); + + Schedule(() => + { + if (this.IsCurrentScreen()) + this.Exit(); + }); + } + public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index 2616b07c1f..658fc43e8d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -112,6 +112,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants break; case MultiplayerUserState.Loaded: + case MultiplayerUserState.ReadyForGameplay: text.Text = "loaded"; icon.Icon = FontAwesome.Solid.DotCircle; icon.Colour = colours.YellowLight; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 494ab51a10..d75466764d 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -92,11 +92,15 @@ namespace osu.Game.Screens.Play !playerConsumed // don't push unless the player is completely loaded && CurrentPlayer?.LoadState == LoadState.Ready - // don't push if the user is hovering one of the panes, unless they are idle. - && (IsHovered || idleTracker.IsIdle.Value) - // don't push if the user is dragging a slider or otherwise. + // don't push unless the player is ready to start gameplay + && ReadyForGameplay; + + protected virtual bool ReadyForGameplay => + // not ready if the user is hovering one of the panes, unless they are idle. + (IsHovered || idleTracker.IsIdle.Value) + // not ready if the user is dragging a slider or otherwise. && inputManager.DraggedDrawable == null - // don't push if a focused overlay is visible, like settings. + // not ready if a focused overlay is visible, like settings. && inputManager.FocusedDrawable == null; private readonly Func createPlayer; @@ -364,7 +368,15 @@ namespace osu.Game.Screens.Play CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; - LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false); + LoadTask = LoadComponentAsync(CurrentPlayer, _ => + { + MetadataInfo.Loading = false; + OnPlayerLoaded(); + }); + } + + protected virtual void OnPlayerLoaded() + { } private void restartRequested() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 21774b73a0..725499d0e5 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) ChangeUserState(u.UserID, MultiplayerUserState.Playing); - ((IMultiplayerClient)this).MatchStarted(); + ((IMultiplayerClient)this).GameplayStarted(); ChangeRoomState(MultiplayerRoomState.Playing); }