From e05b8ff1bea6086dbe8474d4a847e8e72a2fa412 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Thu, 25 Sep 2025 00:55:43 +0100 Subject: [PATCH 1/3] add ability to mix fm and required mods --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 153 ++++++++++-------- 1 file changed, 85 insertions(+), 68 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 1244ea6e5f..a0a463f68a 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -466,93 +466,94 @@ private void processChatCommands(string[] parts) List requiredMods = new List(); List allowedMods = new List(); - if (mods.Length == 1 && mods[0].Equals(@"fm", StringComparison.OrdinalIgnoreCase)) + foreach (string mod in mods) { - // hardcode freemod to allow all mods, leaves requiredMods empty - var newAllowedMods = availableMods.Value - .SelectMany(pair => pair.Value) - .SelectMany(ModUtils.FlattenMod) - .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, Client.Room?.Settings.MatchType ?? MatchType.TeamVersus)); + if (mod.Length < 2) + { + Logger.Log($@"[!mp mods] Unknown mod '{mod}', ignoring", LoggingTarget.Runtime, LogLevel.Important); + continue; + } - allowedMods.AddRange(newAllowedMods); - } - else // handle regular mods - { - foreach (string mod in mods) + string modAcronym = mod[..2]; + + if (modAcronym.Equals(@"fm", StringComparison.OrdinalIgnoreCase)) { - if (mod.Length < 2) - { - Logger.Log($@"[!mp mods] Unknown mod '{mod}', ignoring", LoggingTarget.Runtime, LogLevel.Important); - continue; - } + // hardcode freemod to allow all mods, leaves requiredMods empty + var newAllowedMods = availableMods.Value + .SelectMany(pair => pair.Value) + .SelectMany(ModUtils.FlattenMod) + .Where(availableMod => + ModUtils.IsValidFreeModForMatchType(availableMod, Client.Room?.Settings.MatchType ?? MatchType.TeamVersus)); + + allowedMods.AddRange(newAllowedMods); + continue; + } - string modAcronym = mod[..2]; - var rulesetInstance = RulesetStore.GetRuleset(itemToEdit.RulesetID)?.CreateInstance(); + var rulesetInstance = RulesetStore.GetRuleset(itemToEdit.RulesetID)?.CreateInstance(); - if (rulesetInstance == null) - { - Logger.Log($@"[!mp mods] Couldn't create ruleset instance with ruleset ID {itemToEdit.RulesetID}, ignoring mod '{mod}'", - LoggingTarget.Runtime, LogLevel.Important); - continue; - } + if (rulesetInstance == null) + { + Logger.Log($@"[!mp mods] Couldn't create ruleset instance with ruleset ID {itemToEdit.RulesetID}, ignoring mod '{mod}'", + LoggingTarget.Runtime, LogLevel.Important); + continue; + } + + Mod? modInstance; + + // mod with no params + if (mod.Length == 2) + { + modInstance = ParseMod(rulesetInstance, modAcronym, Array.Empty()); + if (modInstance != null) + requiredMods.Add(modInstance); + continue; + } - Mod? modInstance; + // mod has parameters + { + JsonNode? modParamsNode; - // mod with no params - if (mod.Length == 2) + try { - modInstance = ParseMod(rulesetInstance, modAcronym, Array.Empty()); - if (modInstance != null) - requiredMods.Add(modInstance); - continue; + modParamsNode = JsonNode.Parse(mod[2..]); + } + catch (JsonException) + { + modParamsNode = null; } - // mod has parameters + if (modParamsNode is JsonArray modParams) { - JsonNode? modParamsNode; + List parsedParamsList = new List(); - try - { - modParamsNode = JsonNode.Parse(mod[2..]); - } - catch (JsonException) + foreach (JsonNode? node in modParams) { - modParamsNode = null; - } + if (node?.GetValueKind() is not (JsonValueKind.Number or JsonValueKind.False or JsonValueKind.True)) + continue; - if (modParamsNode is JsonArray modParams) - { - List parsedParamsList = new List(); + if (node.AsValue().TryGetValue(out int parsedInt)) + { + parsedParamsList.Add(parsedInt); + continue; + } - foreach (JsonNode? node in modParams) + if (node.AsValue().TryGetValue(out double parsedDouble)) { - if (node?.GetValueKind() is not (JsonValueKind.Number or JsonValueKind.False or JsonValueKind.True)) - continue; - - if (node.AsValue().TryGetValue(out int parsedInt)) - { - parsedParamsList.Add(parsedInt); - continue; - } - - if (node.AsValue().TryGetValue(out double parsedDouble)) - { - parsedParamsList.Add(parsedDouble); - continue; - } - - if (node.AsValue().TryGetValue(out bool parsedBool)) - parsedParamsList.Add(parsedBool); + parsedParamsList.Add(parsedDouble); + continue; } - modInstance = ParseMod(rulesetInstance, modAcronym, parsedParamsList); - if (modInstance != null) - requiredMods.Add(modInstance); - } - else - { - Logger.Log($@"[!mp mods] Couldn't parse mod parameter(s) '{mod[2..]}', ignoring", LoggingTarget.Runtime, LogLevel.Important); + if (node.AsValue().TryGetValue(out bool parsedBool)) + parsedParamsList.Add(parsedBool); } + + modInstance = ParseMod(rulesetInstance, modAcronym, parsedParamsList); + if (modInstance != null) + requiredMods.Add(modInstance); + } + else + { + Logger.Log($@"[!mp mods] Couldn't parse mod parameter(s) '{mod[2..]}', ignoring", LoggingTarget.Runtime, LogLevel.Important); } } } @@ -563,6 +564,22 @@ private void processChatCommands(string[] parts) break; } + // remove duplicate required mods from allowed mods (if present) + // don't use Mod.Equals, as it will return false if parameters differ + allowedMods.RemoveAll(mod => requiredMods.Any(rMod => rMod.Acronym == mod.Acronym)); + + // ensure allowed mods are compatible with required mods + // inspired by https://github.com/ppy/osu-server-spectator/blob/156923578a34b4559582df9ccd49fda499b4fa0a/osu.Server.Spectator/Extensions/MultiplayerPlaylistItemExtensions.cs#L88-L92 + List modsToRemoveFromAllowed = new List(); + + foreach (var allowedMod in allowedMods) + { + if (!ModUtils.CheckCompatibleSet(requiredMods.Concat(new[] { allowedMod }), out var invalidMods)) + modsToRemoveFromAllowed.AddRange(invalidMods); + } + + allowedMods.RemoveAll(modsToRemoveFromAllowed.Contains); + // get playlist item to edit: beatmapLookupCache.GetBeatmapAsync(itemToEdit.BeatmapID).ContinueWith(task => Schedule(() => { From dc97275cff058e8143a91c64df7994b8fc2e3a75 Mon Sep 17 00:00:00 2001 From: Dao Heng Liu Date: Thu, 25 Sep 2025 01:06:05 +0100 Subject: [PATCH 2/3] filter invalidMods to only mods contained in allowedMods Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index a0a463f68a..aaf79e318d 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -575,7 +575,7 @@ private void processChatCommands(string[] parts) foreach (var allowedMod in allowedMods) { if (!ModUtils.CheckCompatibleSet(requiredMods.Concat(new[] { allowedMod }), out var invalidMods)) - modsToRemoveFromAllowed.AddRange(invalidMods); + modsToRemoveFromAllowed.AddRange(invalidMods.Where(m => allowedMods.Contains(m))); } allowedMods.RemoveAll(modsToRemoveFromAllowed.Contains); From 4700fa7423bdcc0e0db2655b1e782dfcad8b48a2 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Thu, 25 Sep 2025 01:07:55 +0100 Subject: [PATCH 3/3] use hashset for mods to remove from allowedMods --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index aaf79e318d..ee8f9d1fad 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -570,12 +570,12 @@ private void processChatCommands(string[] parts) // ensure allowed mods are compatible with required mods // inspired by https://github.com/ppy/osu-server-spectator/blob/156923578a34b4559582df9ccd49fda499b4fa0a/osu.Server.Spectator/Extensions/MultiplayerPlaylistItemExtensions.cs#L88-L92 - List modsToRemoveFromAllowed = new List(); + HashSet modsToRemoveFromAllowed = new HashSet(); foreach (var allowedMod in allowedMods) { if (!ModUtils.CheckCompatibleSet(requiredMods.Concat(new[] { allowedMod }), out var invalidMods)) - modsToRemoveFromAllowed.AddRange(invalidMods.Where(m => allowedMods.Contains(m))); + modsToRemoveFromAllowed.UnionWith(invalidMods.Where(m => allowedMods.Contains(m))); } allowedMods.RemoveAll(modsToRemoveFromAllowed.Contains);