From 8de96201566175e3cc65dd9db3d61e66d2bf4285 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 14:41:17 +0900 Subject: [PATCH 1/8] Isolate operation of multiplayer mod overlay --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 +---- .../Match/MultiplayerUserModSelectOverlay.cs | 108 ++++++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 36 ------ 3 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 57e8aff151..c73a36617d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; @@ -27,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; @@ -62,11 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match private Sample? sampleStart; - /// - /// Any mods applied by/to the local user. - /// - protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -245,12 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay - { - SelectedItem = { BindTarget = SelectedItem }, - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - }); + LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay()); } protected override void LoadComplete() @@ -258,7 +248,6 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => updateSpecifics()); - UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -441,11 +430,6 @@ namespace osu.Game.Screens.OnlinePlay.Match ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - // Remove any user mods that are no longer allowed. - Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = newUserMods; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); @@ -456,15 +440,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); if (allowedMods.Length > 0) - { UserModsSection.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } else { UserModsSection.Hide(); UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; } if (item.Freestyle) @@ -488,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection.Hide(); } - protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods; protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs new file mode 100644 index 0000000000..e5c447f038 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Utils; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerUserModSelectOverlay : RoomModSelectOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private ModSettingChangeTracker? modSettingChangeTracker; + private ScheduledDelegate? debouncedModSettingsUpdate; + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsValidMod = _ => false; + + client.RoomUpdated += onRoomUpdated; + + SelectedItem.BindValueChanged(_ => updateSpecifics()); + SelectedMods.BindValueChanged(_ => updateSpecifics()); + SelectedMods.BindValueChanged(onSelectedModsChanged); + } + + private void onRoomUpdated() + { + if (client.Room == null) + return; + + SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); + } + + private void onSelectedModsChanged(ValueChangedEvent> mods) + { + modSettingChangeTracker?.Dispose(); + + if (client.Room == null) + return; + + client.ChangeUserMods(mods.NewValue).FireAndForget(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += _ => + { + // Debounce changes to mod settings so as to not thrash the network. + debouncedModSettingsUpdate?.Cancel(); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => + { + if (client.Room == null) + return; + + client.ChangeUserMods(SelectedMods.Value).FireAndForget(); + }, 500); + }; + } + + private void updateSpecifics() + { + if (client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + Mod[] allowedMods = currentItem.Freestyle + ? ruleset.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray() + : currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray(); + + // Update the mod panels to reflect the ones which are valid for selection. + IsValidMod = allowedMods.Length > 0 + ? m => allowedMods.Any(a => a.GetType() == m.GetType()) + : _ => false; + + // Remove any mods that are no longer allowed. + Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + if (!newUserMods.SequenceEqual(SelectedMods.Value)) + SelectedMods.Value = newUserMods; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + + modSettingChangeTracker?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 08a469fa03..0cc033907f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -11,9 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; using osu.Game.Online.API; @@ -23,7 +20,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -64,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); - UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; @@ -306,35 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void PartRoom() => client.LeaveRoom(); - private ModSettingChangeTracker? modSettingChangeTracker; - private ScheduledDelegate? debouncedModSettingsUpdate; - - private void onUserModsChanged(ValueChangedEvent> mods) - { - modSettingChangeTracker?.Dispose(); - - if (client.Room == null) - return; - - client.ChangeUserMods(mods.NewValue).FireAndForget(); - - modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - modSettingChangeTracker.SettingChanged += onModSettingsChanged; - } - - private void onModSettingsChanged(Mod mod) - { - // Debounce changes to mod settings so as to not thrash the network. - debouncedModSettingsUpdate?.Cancel(); - debouncedModSettingsUpdate = Scheduler.AddDelayed(() => - { - if (client.Room == null) - return; - - client.ChangeUserMods(UserMods.Value).FireAndForget(); - }, 500); - } - private void updateBeatmapAvailability(ValueChangedEvent availability) { if (client.Room == null) @@ -462,8 +428,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.RoomUpdated -= onRoomUpdated; client.LoadRequested -= onLoadRequested; } - - modSettingChangeTracker?.Dispose(); } public partial class AddItemButton : PurpleRoundedButton From 72efbbad2dec4565fa46003056a60d26b1d82c9f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 16:45:25 +0900 Subject: [PATCH 2/8] Remove inheritance on `RoomModSelectOverlay` --- .../Match/MultiplayerUserModSelectOverlay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index e5c447f038..1ddcccc02c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -10,14 +10,15 @@ using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerUserModSelectOverlay : RoomModSelectOverlay + public class MultiplayerUserModSelectOverlay : UserModSelectOverlay { [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -28,26 +29,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private ModSettingChangeTracker? modSettingChangeTracker; private ScheduledDelegate? debouncedModSettingsUpdate; + public MultiplayerUserModSelectOverlay() + : base(OverlayColourScheme.Plum) + { + } + protected override void LoadComplete() { base.LoadComplete(); - IsValidMod = _ => false; - client.RoomUpdated += onRoomUpdated; - - SelectedItem.BindValueChanged(_ => updateSpecifics()); - SelectedMods.BindValueChanged(_ => updateSpecifics()); SelectedMods.BindValueChanged(onSelectedModsChanged); + + updateValidMods(); } - private void onRoomUpdated() - { - if (client.Room == null) - return; - - SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); - } + private void onRoomUpdated() => Scheduler.AddOnce(updateValidMods); private void onSelectedModsChanged(ValueChangedEvent> mods) { @@ -73,7 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }; } - private void updateSpecifics() + private void updateValidMods() { if (client.Room == null || client.LocalUser == null) return; @@ -95,6 +92,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match SelectedMods.Value = newUserMods; } + protected override IReadOnlyList ComputeActiveMods() + { + if (client.Room == null || client.LocalUser == null) + return []; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + return currentItem.RequiredMods.Select(m => m.ToMod(ruleset)).Concat(base.ComputeActiveMods()).ToArray(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From fbc8469fc402c57e4fb146e13d162fab8030c119 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 17:10:47 +0900 Subject: [PATCH 3/8] Partial class --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 1ddcccc02c..075b664028 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -18,7 +18,7 @@ using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerUserModSelectOverlay : UserModSelectOverlay + public partial class MultiplayerUserModSelectOverlay : UserModSelectOverlay { [Resolved] private MultiplayerClient client { get; set; } = null!; From 452f36d77ab337f5150f84ac735a365000124def Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 17:55:19 +0900 Subject: [PATCH 4/8] Fix active mods not updated --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 075b664028..c66e1a906c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -90,6 +90,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(SelectedMods.Value)) SelectedMods.Value = newUserMods; + + ActiveMods.Value = ComputeActiveMods(); } protected override IReadOnlyList ComputeActiveMods() From aa58fa58cb6f2d5a3679a60e5f8fb5f44fd7a8e0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 17:59:52 +0900 Subject: [PATCH 5/8] Add deduping for active mods, add documentation --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index c66e1a906c..692ef0fd2f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -91,7 +91,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!newUserMods.SequenceEqual(SelectedMods.Value)) SelectedMods.Value = newUserMods; - ActiveMods.Value = ComputeActiveMods(); + // The active mods include the playlist item's required mods which change separately from the selected mods. + IReadOnlyList newActiveMods = ComputeActiveMods(); + if (!newActiveMods.SequenceEqual(ActiveMods.Value)) + ActiveMods.Value = ComputeActiveMods(); } protected override IReadOnlyList ComputeActiveMods() From d0de8e908d4cc64f8d9ebd4b155f37f9fdaf6214 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 18:27:16 +0900 Subject: [PATCH 6/8] Fix duplicate `ComputeActiveMods()` call --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 692ef0fd2f..dc443f595b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // The active mods include the playlist item's required mods which change separately from the selected mods. IReadOnlyList newActiveMods = ComputeActiveMods(); if (!newActiveMods.SequenceEqual(ActiveMods.Value)) - ActiveMods.Value = ComputeActiveMods(); + ActiveMods.Value = newActiveMods; } protected override IReadOnlyList ComputeActiveMods() From 56169d7ac4cb641e22d3f3abfb6bfcca7e10e100 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 19:58:31 +0900 Subject: [PATCH 7/8] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e51ea12e83..14e6a67d3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.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; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -336,6 +337,61 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0)); } + [Test] + public void TestUserModSelectUpdatesWhenNotVisible() + { + AddStep("add playlist item", () => + { + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = [new APIMod(new OsuModFlashlight())] + } + ]; + }); + + ClickButtonWhenEnabled(); + AddUntilStep("wait for join", () => RoomJoined); + + // 1. Open the mod select overlay and enable flashlight + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddStep("click flashlight panel", () => + { + ModPanel panel = this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods. + + AddStep("close mod select overlay", () => this.ChildrenOfType().Single().Hide()); + AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [] + }))); + // This would normally be done as part of the above operation with an actual server. + AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty())); + AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [new APIMod(new OsuModFlashlight())] + }))); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 3. Open the mod select overlay, check that the flashlight mod panel is deactivated. + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight).Active.Value); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] From f120684b145f5f6b9892eca1566f22b13ba6f210 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 20:02:39 +0900 Subject: [PATCH 8/8] Fix skipped updates leading to incorrect validation --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index dc443f595b..8463a4720c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -44,7 +44,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateValidMods(); } - private void onRoomUpdated() => Scheduler.AddOnce(updateValidMods); + private void onRoomUpdated() + { + // Importantly, this is not scheduled because the client must not skip intermediate server states to validate the allowed mods. + updateValidMods(); + } private void onSelectedModsChanged(ValueChangedEvent> mods) {