From 373162df02421cf835f2ad7ef25d98b092910cbd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 19:56:12 +0900 Subject: [PATCH] Add support for vote-to-skip in multiplayer --- .../Online/Multiplayer/MultiplayerClient.cs | 34 +++++++++++++++++-- .../Multiplayer/OnlineMultiplayerClient.cs | 12 +++++-- .../Multiplayer/MultiplayerPlayer.cs | 14 +++++++- osu.Game/Screens/Play/Player.cs | 9 +++-- .../Multiplayer/TestMultiplayerClient.cs | 2 +- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 04162f6b6f..9d97f0e830 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,6 +131,9 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; + public event Action? UserVotedToSkip; + public event Action? VoteToSkipPassed; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -521,6 +524,12 @@ namespace osu.Game.Online.Multiplayer break; } + if (state == MultiplayerRoomState.Playing) + { + foreach (var user in Room.Users) + user.VotedToSkip = false; + } + RoomUpdated?.Invoke(); }); @@ -920,12 +929,33 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.UserVotedToSkip(int userId) { - throw new NotImplementedException(); + handleRoomRequest(() => + { + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); + + // TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713. + if (user == null) + return; + + user.VotedToSkip = true; + + UserVotedToSkip?.Invoke(userId); + }); + + return Task.CompletedTask; } Task IMultiplayerClient.VoteToSkipPassed() { - throw new NotImplementedException(); + handleRoomRequest(() => + { + Debug.Assert(Room != null); + VoteToSkipPassed?.Invoke(); + }); + + return Task.CompletedTask; } /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index e496aea7a2..54811c5794 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -70,7 +70,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); - connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); + connection.On(nameof(IMultiplayerClient.UserVotedToSkip), ((IMultiplayerClient)this).UserVotedToSkip); + connection.On(nameof(IMultiplayerClient.VoteToSkipPassed), ((IMultiplayerClient)this).VoteToSkipPassed); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); @@ -80,6 +81,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged); connection.On(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected); connection.On(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected); + + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -314,7 +317,12 @@ namespace osu.Game.Online.Multiplayer public override Task VoteToSkip() { - throw new NotImplementedException(); + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkip)); } public override Task DisconnectInternal() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 56120120d5..41b8f5f146 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -56,7 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AllowPause = false, AllowRestart = false, - AllowSkipping = room.AutoSkip, AutomaticallySkipIntro = room.AutoSkip, ShowLeaderboard = true, }) @@ -121,6 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; + client.VoteToSkipPassed += onVoteToSkipPassed; ScoreProcessor.HasCompleted.BindValueChanged(_ => { @@ -219,6 +219,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false); } + protected override void RequestIntroSkip() + { + // No base call because we aren't skipping yet. + client.VoteToSkip(); + } + + private void onVoteToSkipPassed() + { + Schedule(PerformIntroSkip); + } + protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID != null); @@ -242,6 +253,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; + client.VoteToSkipPassed -= onVoteToSkipPassed; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 22fb8a3463..9f40fc97da 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -502,7 +502,7 @@ namespace osu.Game.Screens.Play DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { - RequestSkip = performUserRequestedSkip + RequestSkip = RequestIntroSkip }, skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0) { @@ -701,7 +701,12 @@ namespace osu.Game.Screens.Play return true; } - private void performUserRequestedSkip() + protected virtual void RequestIntroSkip() + { + PerformIntroSkip(); + } + + protected void PerformIntroSkip() { // user requested skip // disable sample playback to stop currently playing samples and perform skip diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a899912225..242bbe3083 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -563,7 +563,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task VoteToSkip() { - throw new NotImplementedException(); + return Task.CompletedTask; } protected override Task CreateRoomInternal(MultiplayerRoom room)