From a435dfe93ed4d6971c8cf329a9777ad0d7a2045c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 22:42:12 +0900 Subject: [PATCH 01/12] Add interop models --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 10 ++++++++++ .../Online/Multiplayer/IMultiplayerRoomServer.cs | 5 +++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 12 ++++++++++++ osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 6 ++++++ .../Online/Multiplayer/OnlineMultiplayerClient.cs | 5 +++++ .../Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++++ 6 files changed, 43 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index adb9b92614..340fb04731 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -149,5 +149,15 @@ namespace osu.Game.Online.Multiplayer /// /// The changed item. Task PlaylistItemChanged(MultiplayerPlaylistItem item); + + /// + /// Signals that a user has requested to skip the beatmap intro. + /// + Task UserVotedToSkip(int userId); + + /// + /// Signals that the vote to skip the beatmap intro has passed. + /// + Task VoteToSkipPassed(); } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 490973faa2..d7834427d0 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -112,6 +112,11 @@ namespace osu.Game.Online.Multiplayer /// The item to remove. Task RemovePlaylistItem(long playlistItemId); + /// + /// Votes to skip the beatmap intro. + /// + Task VoteToSkip(); + /// /// Invites a player to the current room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 44cbbafe72..04162f6b6f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -493,6 +493,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task RemovePlaylistItem(long playlistItemId); + public abstract Task VoteToSkip(); + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { handleRoomRequest(() => @@ -916,6 +918,16 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + Task IMultiplayerClient.UserVotedToSkip(int userId) + { + throw new NotImplementedException(); + } + + Task IMultiplayerClient.VoteToSkipPassed() + { + throw new NotImplementedException(); + } + /// /// Populates the for a given collection of s. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 499e84ce80..365a25778b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -49,6 +49,12 @@ namespace osu.Game.Online.Multiplayer [Key(6)] public int? BeatmapId; + /// + /// Whether this user voted to skip the beatmap intro. + /// + [Key(7)] + public bool VotedToSkip; + [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 0decff7ab3..e496aea7a2 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -312,6 +312,11 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + public override Task VoteToSkip() + { + throw new NotImplementedException(); + } + public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5b2876a989..a899912225 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -561,6 +561,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); + public override Task VoteToSkip() + { + throw new NotImplementedException(); + } + protected override Task CreateRoomInternal(MultiplayerRoom room) { Room apiRoom = new Room(room) From 373162df02421cf835f2ad7ef25d98b092910cbd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 19:56:12 +0900 Subject: [PATCH 02/12] 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) From d0ce74063d21329f5feeaec26c1a87f6c19f12f3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 20:49:27 +0900 Subject: [PATCH 03/12] Skip full intro length --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 41b8f5f146..406f38ea72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -227,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onVoteToSkipPassed() { - Schedule(PerformIntroSkip); + Schedule(() => PerformIntroSkip(true)); } protected override ResultsScreen CreateResults(ScoreInfo score) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 07ecb5a5fb..abf157df43 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -115,14 +115,14 @@ namespace osu.Game.Screens.Play /// /// Skip forward to the next valid skip point. /// - public void Skip() + public void Skip(bool fullLength = false) { if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) return; double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME; - if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) + if (!fullLength && StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9f40fc97da..2927d8a720 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -706,13 +706,13 @@ namespace osu.Game.Screens.Play PerformIntroSkip(); } - protected void PerformIntroSkip() + protected void PerformIntroSkip(bool fullLength = false) { // user requested skip // disable sample playback to stop currently playing samples and perform skip samplePlaybackDisabled.Value = true; - (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(); + (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(fullLength); // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state updateSampleDisabledState(); From b20a41c1e8377271bd96fd345df1479d20fac3c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:39:33 +0900 Subject: [PATCH 04/12] Add simple multiplayer skip overlay --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- .../TestSceneMultiplayerSkipOverlay.cs | 73 +++++++++++ .../Multiplayer/MultiplayerPlayer.cs | 2 + .../Multiplayer/MultiplayerSkipOverlay.cs | 114 ++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 14 ++- osu.Game/Screens/Play/SkipOverlay.cs | 17 +-- .../Multiplayer/TestMultiplayerClient.cs | 7 +- 7 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 276a0c3410..946b625608 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Drawable OverlayContent => InternalChild; - public Drawable FadingContent => (OverlayContent as Container)?.Child; + public new Drawable FadingContent => (OverlayContent as Container)?.Child; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..a1b28e2544 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerSkipOverlay : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add skip overlay", () => + { + GameplayClockContainer gameplayClockContainer; + + var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new MultiplayerSkipOverlay(120000) + }, + }; + + gameplayClockContainer.Start(); + }); + + AddStep("set playing state", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Playing)); + } + + [Test] + public void TestSkip() + { + for (int i = 0; i < 4; i++) + { + int i2 = i; + + AddStep($"join user {i2}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = i2, + Username = $"User {i2}" + }); + + MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing); + }); + } + + AddStep("local user votes", () => MultiplayerClient.VoteToSkip().WaitSafely()); + + for (int i = 0; i < 4; i++) + { + int i2 = i; + AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkip(i2).WaitSafely()); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 406f38ea72..24dfa59ed3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -148,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } + protected override SkipOverlay CreateSkipOverlay(double startTime) => new MultiplayerSkipOverlay(startTime); + protected override void StartGameplay() { // We can enter this screen one of two ways: diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..ccda0e8690 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -0,0 +1,114 @@ +// 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 System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerSkipOverlay : SkipOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Drawable votedIcon = null!; + private OsuSpriteText countText = null!; + + public MultiplayerSkipOverlay(double startTime) + : base(startTime) + { + } + + [BackgroundDependencyLoader] + private void load() + { + FadingContent.AddRange( + [ + votedIcon = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(50, 0), + Size = new Vector2(20), + Alpha = 0, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Green + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Icon = FontAwesome.Solid.Check + } + } + }, + countText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + Position = new Vector2(0.75f, 0), + Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold) + } + ]); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.UserStateChanged += onUserStateChanged; + client.UserVotedToSkip += onUserVotedToSkip; + + updateText(); + } + + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) + { + Schedule(updateText); + } + + private void onUserVotedToSkip(int userId) => Schedule(() => + { + updateText(); + + countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); + + if (userId == client.LocalUser?.UserID) + { + votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); + votedIcon.FadeInFromZero(100); + } + }); + + private void updateText() + { + if (client.Room == null) + return; + + int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); + int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkip); + int countRequired = countTotal / 2 + 1; + + countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2927d8a720..b712a451c5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play private BreakTracker breakTracker; - private SkipOverlay skipIntroOverlay; + protected SkipOverlay SkipIntroOverlay { get; private set; } private SkipOverlay skipOutroOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } @@ -500,10 +500,10 @@ namespace osu.Game.Screens.Play }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) + SkipIntroOverlay = CreateSkipOverlay(DrawableRuleset.GameplayStartTime).With(o => { - RequestSkip = RequestIntroSkip - }, + o.RequestSkip = RequestIntroSkip; + }), skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0) { RequestSkip = () => progressToResults(false), @@ -522,13 +522,15 @@ namespace osu.Game.Screens.Play if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { - skipIntroOverlay.Expire(); + SkipIntroOverlay.Expire(); skipOutroOverlay.Expire(); } return container; } + protected virtual SkipOverlay CreateSkipOverlay(double startTime) => new SkipOverlay(startTime); + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); @@ -1158,7 +1160,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Reset(startClock: true); if (Configuration.AutomaticallySkipIntro) - skipIntroOverlay.SkipWhenReady(); + SkipIntroOverlay.SkipWhenReady(); } public override void OnSuspending(ScreenTransitionEvent e) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index be8517d9a0..700ea2e532 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -38,20 +38,21 @@ namespace osu.Game.Screens.Play private readonly double startTime; public Action RequestSkip; + + protected FadeContainer FadingContent { get; private set; } + private Button button; private ButtonContainer buttonContainer; private Circle remainingTimeBox; - private FadeContainer fadeContainer; private double displayTime; - private bool isClickable; private bool skipQueued; [Resolved] private IGameplayClock gameplayClock { get; set; } - internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; + internal bool IsButtonVisible => FadingContent.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -77,7 +78,7 @@ namespace osu.Game.Screens.Play InternalChild = buttonContainer = new ButtonContainer { RelativeSizeAxes = Axes.Both, - Child = fadeContainer = new FadeContainer + Child = FadingContent = new FadeContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -107,13 +108,13 @@ namespace osu.Game.Screens.Play public override void Hide() { base.Hide(); - fadeContainer.Hide(); + FadingContent.Hide(); } public override void Show() { base.Show(); - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } protected override void LoadComplete() @@ -136,7 +137,7 @@ namespace osu.Game.Screens.Play RequestSkip?.Invoke(); }; - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } /// @@ -183,7 +184,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { if (isClickable && !e.HasAnyButtonPressed) - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); return base.OnMouseMove(e); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 242bbe3083..fcee7e5b44 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -563,7 +563,12 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task VoteToSkip() { - return Task.CompletedTask; + return UserVoteToSkip(api.LocalUser.Value.OnlineID); + } + + public async Task UserVoteToSkip(int userId) + { + await ((IMultiplayerClient)this).UserVotedToSkip(userId); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 6f94b1ab6d21e0dba43c78a801624611c838981b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:40:40 +0900 Subject: [PATCH 05/12] Move property reset into GameplayStarted() --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 9d97f0e830..3df12e16ea 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -524,12 +524,6 @@ namespace osu.Game.Online.Multiplayer break; } - if (state == MultiplayerRoomState.Playing) - { - foreach (var user in Room.Users) - user.VotedToSkip = false; - } - RoomUpdated?.Invoke(); }); @@ -857,6 +851,10 @@ namespace osu.Game.Online.Multiplayer handleRoomRequest(() => { Debug.Assert(Room != null); + + foreach (var user in Room.Users) + user.VotedToSkip = false; + GameplayStarted?.Invoke(); }); From bdcc0ee937113dd5a80ae6e7920688dd7a6de028 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:42:29 +0900 Subject: [PATCH 06/12] Apply suggestions from review --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 1 + osu.Game/Screens/Play/Player.cs | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 24dfa59ed3..214a7d6403 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { // No base call because we aren't skipping yet. - client.VoteToSkip(); + client.VoteToSkip().FireAndForget(); } private void onVoteToSkipPassed() diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index abf157df43..c9db6009d0 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -115,6 +115,7 @@ namespace osu.Game.Screens.Play /// /// Skip forward to the next valid skip point. /// + /// true to skip as close to gameplay as possible, or false to skip only to the next valid skip point. public void Skip(bool fullLength = false) { if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b712a451c5..6158118c78 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -708,6 +708,10 @@ namespace osu.Game.Screens.Play PerformIntroSkip(); } + /// + /// Skip forward to the next valid skip point. + /// + /// true to skip as close to gameplay as possible, or false to skip only to the next valid skip point. protected void PerformIntroSkip(bool fullLength = false) { // user requested skip From a9ca4634fc583ececc0fd3c3bbfa224c8a8daf59 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:48:24 +0900 Subject: [PATCH 07/12] Resolve CI inspections --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index fcee7e5b44..83b2da000f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public async Task UserVoteToSkip(int userId) { - await ((IMultiplayerClient)this).UserVotedToSkip(userId); + await ((IMultiplayerClient)this).UserVotedToSkip(userId).ConfigureAwait(false); } protected override Task CreateRoomInternal(MultiplayerRoom room) From f4049c7ec18fa22904e7c57ce8c725f7824136bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 22:48:49 +0900 Subject: [PATCH 08/12] Suffix introp methods with "Intro" --- .../Multiplayer/TestSceneMultiplayerSkipOverlay.cs | 4 ++-- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 4 ++-- .../Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 14 +++++++------- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- .../Online/Multiplayer/OnlineMultiplayerClient.cs | 8 ++++---- .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 8 ++++---- .../Multiplayer/MultiplayerSkipOverlay.cs | 2 +- .../Visual/Multiplayer/TestMultiplayerClient.cs | 8 ++++---- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs index a1b28e2544..059af2484d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - AddStep("local user votes", () => MultiplayerClient.VoteToSkip().WaitSafely()); + AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely()); for (int i = 0; i < 4; i++) { int i2 = i; - AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkip(i2).WaitSafely()); + AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely()); } } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 340fb04731..c91128401d 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -153,11 +153,11 @@ namespace osu.Game.Online.Multiplayer /// /// Signals that a user has requested to skip the beatmap intro. /// - Task UserVotedToSkip(int userId); + Task UserVotedToSkipIntro(int userId); /// /// Signals that the vote to skip the beatmap intro has passed. /// - Task VoteToSkipPassed(); + Task VoteToSkipIntroPassed(); } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index d7834427d0..169d5d1b83 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -115,7 +115,7 @@ namespace osu.Game.Online.Multiplayer /// /// Votes to skip the beatmap intro. /// - Task VoteToSkip(); + Task VoteToSkipIntro(); /// /// Invites a player to the current room. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 3df12e16ea..af2655f0f4 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -132,7 +132,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchRoomStateChanged; public event Action? UserVotedToSkip; - public event Action? VoteToSkipPassed; + public event Action? VoteToSkipIntroPassed; /// /// Whether the is currently connected. @@ -496,7 +496,7 @@ namespace osu.Game.Online.Multiplayer public abstract Task RemovePlaylistItem(long playlistItemId); - public abstract Task VoteToSkip(); + public abstract Task VoteToSkipIntro(); Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { @@ -853,7 +853,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); foreach (var user in Room.Users) - user.VotedToSkip = false; + user.VotedToSkipIntro = false; GameplayStarted?.Invoke(); }); @@ -925,7 +925,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.UserVotedToSkip(int userId) + Task IMultiplayerClient.UserVotedToSkipIntro(int userId) { handleRoomRequest(() => { @@ -937,7 +937,7 @@ namespace osu.Game.Online.Multiplayer if (user == null) return; - user.VotedToSkip = true; + user.VotedToSkipIntro = true; UserVotedToSkip?.Invoke(userId); }); @@ -945,12 +945,12 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.VoteToSkipPassed() + Task IMultiplayerClient.VoteToSkipIntroPassed() { handleRoomRequest(() => { Debug.Assert(Room != null); - VoteToSkipPassed?.Invoke(); + VoteToSkipIntroPassed?.Invoke(); }); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 365a25778b..d19386c98d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.Multiplayer /// Whether this user voted to skip the beatmap intro. /// [Key(7)] - public bool VotedToSkip; + public bool VotedToSkipIntro; [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 54811c5794..1319578c06 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -70,8 +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(IMultiplayerClient.UserVotedToSkip), ((IMultiplayerClient)this).UserVotedToSkip); - connection.On(nameof(IMultiplayerClient.VoteToSkipPassed), ((IMultiplayerClient)this).VoteToSkipPassed); + connection.On(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro); + connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); @@ -315,14 +315,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - public override Task VoteToSkip() + public override Task VoteToSkipIntro() { if (!IsConnected.Value) return Task.CompletedTask; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkip)); + return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro)); } public override Task DisconnectInternal() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 214a7d6403..26535f269c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; - client.VoteToSkipPassed += onVoteToSkipPassed; + client.VoteToSkipIntroPassed += onVoteToSkipIntroPassed; ScoreProcessor.HasCompleted.BindValueChanged(_ => { @@ -224,10 +224,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { // No base call because we aren't skipping yet. - client.VoteToSkip().FireAndForget(); + client.VoteToSkipIntro().FireAndForget(); } - private void onVoteToSkipPassed() + private void onVoteToSkipIntroPassed() { Schedule(() => PerformIntroSkip(true)); } @@ -255,7 +255,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; - client.VoteToSkipPassed -= onVoteToSkipPassed; + client.VoteToSkipIntroPassed -= onVoteToSkipIntroPassed; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index ccda0e8690..68c6fbe7c5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); - int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkip); + int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro); int countRequired = countTotal / 2 + 1; countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 83b2da000f..38070d953e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -561,14 +561,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); - public override Task VoteToSkip() + public override Task VoteToSkipIntro() { - return UserVoteToSkip(api.LocalUser.Value.OnlineID); + return UserVoteToSkipIntro(api.LocalUser.Value.OnlineID); } - public async Task UserVoteToSkip(int userId) + public async Task UserVoteToSkipIntro(int userId) { - await ((IMultiplayerClient)this).UserVotedToSkip(userId).ConfigureAwait(false); + await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId).ConfigureAwait(false); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 4c81d661aa472d69b74af75c539a96ea66d1bee7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:03:45 +0900 Subject: [PATCH 09/12] Bypass vote for auto-skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 7 +++++++ .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 26535f269c..4cc6f3469d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -223,6 +223,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { + // If the room is set up such that the intro is automatically skipped, there's no need to vote on it. + if (Configuration.AutomaticallySkipIntro) + { + base.RequestIntroSkip(); + return; + } + // No base call because we aren't skipping yet. client.VoteToSkipIntro().FireAndForget(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 68c6fbe7c5..747384b220 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateText() { - if (client.Room == null) + if (client.Room == null || client.Room.Settings.AutoSkip) return; int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); From c44f701abe08bae1439a3319654df2c9eb991c58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:06:57 +0900 Subject: [PATCH 10/12] Also update text when users leave --- .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 747384b220..927d303988 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -75,12 +75,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); + client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; client.UserVotedToSkip += onUserVotedToSkip; updateText(); } + private void onUserLeft(MultiplayerRoomUser user) + { + Schedule(updateText); + } + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) { Schedule(updateText); From 4d706b12ac3b0cc13e44ce6efd8af2d971055195 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:07:18 +0900 Subject: [PATCH 11/12] Fix missing disposal --- .../Multiplayer/MultiplayerSkipOverlay.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 927d303988..f9d394c2b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -116,5 +117,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.UserLeft -= onUserLeft; + client.UserStateChanged -= onUserStateChanged; + client.UserVotedToSkip -= onUserVotedToSkip; + } + } } } From f8331e0b2859d0d849cbf9f39dfa722f7def33c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Nov 2025 12:56:03 +0100 Subject: [PATCH 12/12] Apply one more missed rename --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index af2655f0f4..6f98264d23 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,7 +131,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; - public event Action? UserVotedToSkip; + public event Action? UserVotedToSkipIntro; public event Action? VoteToSkipIntroPassed; /// @@ -939,7 +939,7 @@ namespace osu.Game.Online.Multiplayer user.VotedToSkipIntro = true; - UserVotedToSkip?.Invoke(userId); + UserVotedToSkipIntro?.Invoke(userId); }); return Task.CompletedTask; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index f9d394c2b5..35e85c3273 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; - client.UserVotedToSkip += onUserVotedToSkip; + client.UserVotedToSkipIntro += onUserVotedToSkipIntro; updateText(); } @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Schedule(updateText); } - private void onUserVotedToSkip(int userId) => Schedule(() => + private void onUserVotedToSkipIntro(int userId) => Schedule(() => { updateText(); @@ -126,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.UserLeft -= onUserLeft; client.UserStateChanged -= onUserStateChanged; - client.UserVotedToSkip -= onUserVotedToSkip; + client.UserVotedToSkipIntro -= onUserVotedToSkipIntro; } } }