From 3f5c113394e9c07505d3bc83e95753a2dddf68ca Mon Sep 17 00:00:00 2001 From: triacontakai <31161627+triacontakai@users.noreply.github.com> Date: Thu, 7 May 2026 02:59:54 -0400 Subject: [PATCH] Fix non-default mod settings allowing for duplicate freestyle mod selection in multiplayer (#37646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a bug that allowed for selecting the same mod twice in multiplayer if the playlist entry has non-default settings for a required mod. This happens due to a strict equality mod check in the mod set compatibility check function, which only considers mods duplicates if their settings are exactly the same. Replacing with a more lenient `Type` check fixes this. Adds a regression test for this behavior Fixes https://github.com/ppy/osu/issues/37625. --------- Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Mods/ModUtilsTest.cs | 19 +++++++++++++ .../TestSceneMultiplayerMatchSubScreen.cs | 27 +++++++++++++++++++ osu.Game/Utils/ModUtils.cs | 2 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index b05b4c00b9..f0f08e5a31 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -7,8 +7,10 @@ using System.Linq; using Moq; using NUnit.Framework; using NUnit.Framework.Legacy; +using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -33,6 +35,17 @@ namespace osu.Game.Tests.Mods Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object })); } + [Test] + public void TestModIsNotCompatibleWithItselfEvenIfSettingsDiffer() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + mod2.Setup(m => m.Setting).Returns(new BindableBool(true)); + + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }, out var invalid), Is.False); + Assert.That(invalid, Is.EquivalentTo(new[] { mod2.Object })); + } + [Test] public void TestModIsCompatibleByItself() { @@ -397,6 +410,12 @@ namespace osu.Game.Tests.Mods { } + public abstract class CustomMod3 : Mod, IModCompatibilitySpecification + { + [SettingSource("Setting")] + public virtual BindableBool Setting { get; } = new BindableBool(); + } + private class InvalidMultiplayerMod : Mod { public override string Name => string.Empty; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 379a589ca9..7a72178645 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -330,6 +330,33 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } + [Test] + public void TestModSelectOverlayNonDefaultSettings() + { + AddStep("add playlist item", () => + { + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = + [ + new APIMod(new OsuModSuddenDeath { FailOnSliderTail = { Value = true } }), + ], + AllowedMods = [], + Freestyle = true + } + ]; + }); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + AddAssert("sudden death not visible", () => this.ChildrenOfType().Single().ChildrenOfType().Single(m => m.Mod is ModSuddenDeath).Visible == false); + } + [Test] public void TestChangeSettingsButtonVisibleForHost() { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index e944b188f1..01b0aab46b 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -63,7 +63,7 @@ namespace osu.Game.Utils { var m = mods[j]; - if (candidate.Equals(m)) + if (candidate.GetType() == m.GetType()) { invalidMods ??= new List(); invalidMods.Add(m);