From d21c8ba1b41393c0baf42a30a8f97d6619fac411 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Wed, 24 Sep 2025 03:42:08 +0100 Subject: [PATCH 1/3] add `!mp mods fm` --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 167 +++++++++++------- 1 file changed, 105 insertions(+), 62 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 50606aca41..74ba3720c2 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -33,6 +33,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Mods; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -166,8 +167,12 @@ public StandAloneChatDisplay(bool postingTextBox = false) Channel.BindValueChanged(channelChanged); } + private readonly Bindable>> availableMods = new Bindable>>(); + + private Bindable>> availableModsState { get; } = new Bindable>>(new Dictionary>()); + [BackgroundDependencyLoader(true)] - private void load(ChannelManager manager, BeatmapModelDownloader beatmaps, BeatmapLookupCache beatmapsCache, OsuConfigManager config) + private void load(ChannelManager manager, BeatmapModelDownloader beatmaps, BeatmapLookupCache beatmapsCache, OsuConfigManager config, OsuGameBase game) { channelManager ??= manager; beatmapsDownloader = beatmaps; @@ -179,6 +184,29 @@ private void load(ChannelManager manager, BeatmapModelDownloader beatmaps, Beatm Scheduler.Add(() => broadcastServer.Add(chatBroadcaster)); Scheduler.Add(processMessageQueue); + + availableMods.BindTo(game.AvailableMods); + + populateModStates(); + } + + /// + /// yoinked most of the code from ModSelectOverlay.createLocalMods + /// + private void populateModStates() + { + var newLocalAvailableMods = new Dictionary>(); + + foreach (var (modType, mods) in availableMods.Value) + { + var modStates = mods.SelectMany(ModUtils.FlattenMod) + .Select(mod => new ModState(mod.DeepClone())) + .ToArray(); + + newLocalAvailableMods[modType] = modStates; + } + + availableModsState.Value = newLocalAvailableMods; } protected override void LoadComplete() @@ -459,89 +487,104 @@ private void processChatCommands(string[] parts) break; string[] mods = parts[2].Split("+"); - List modInstances = new List(); + List requiredMods = new List(); + List allowedMods = new List(); - foreach (string mod in mods) + if (mods.Length == 1 && mods[0].Equals(@"fm", StringComparison.OrdinalIgnoreCase)) { - if (mod.Length < 2) - { - Logger.Log($@"[!mp mods] Unknown mod '{mod}', ignoring", LoggingTarget.Runtime, LogLevel.Important); - continue; - } - - string modAcronym = mod[..2]; - 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; - } - - Mod? modInstance; - - // mod with no params - if (mod.Length == 2) + // hardcode freemod to allow all mods, leaves requiredMods empty + var newAllowedMods = availableModsState.Value + .SelectMany(pair => pair.Value) + .Where(state => state.ValidForSelection.Value) + .Select(state => state.Mod) + .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, MatchType.TeamVersus)); + + allowedMods.AddRange(newAllowedMods); + } + else // handle regular mods + { + foreach (string mod in mods) { - modInstance = ParseMod(rulesetInstance, modAcronym, Array.Empty()); - if (modInstance != null) - modInstances.Add(modInstance); - continue; - } + if (mod.Length < 2) + { + Logger.Log($@"[!mp mods] Unknown mod '{mod}', ignoring", LoggingTarget.Runtime, LogLevel.Important); + continue; + } - // mod has parameters - { - JsonNode? modParamsNode; + string modAcronym = mod[..2]; + var rulesetInstance = RulesetStore.GetRuleset(itemToEdit.RulesetID)?.CreateInstance(); - try + if (rulesetInstance == null) { - modParamsNode = JsonNode.Parse(mod[2..]); + Logger.Log($@"[!mp mods] Couldn't create ruleset instance with ruleset ID {itemToEdit.RulesetID}, ignoring mod '{mod}'", + LoggingTarget.Runtime, LogLevel.Important); + continue; } - catch (JsonException) + + Mod? modInstance; + + // mod with no params + if (mod.Length == 2) { - modParamsNode = null; + modInstance = ParseMod(rulesetInstance, modAcronym, Array.Empty()); + if (modInstance != null) + requiredMods.Add(modInstance); + continue; } - if (modParamsNode is JsonArray modParams) + // mod has parameters { - List parsedParamsList = new List(); + JsonNode? modParamsNode; - foreach (JsonNode? node in modParams) + try + { + modParamsNode = JsonNode.Parse(mod[2..]); + } + catch (JsonException) { - if (node?.GetValueKind() is not (JsonValueKind.Number or JsonValueKind.False or JsonValueKind.True)) - continue; + modParamsNode = null; + } - if (node.AsValue().TryGetValue(out int parsedInt)) - { - parsedParamsList.Add(parsedInt); - continue; - } + if (modParamsNode is JsonArray modParams) + { + List parsedParamsList = new List(); - if (node.AsValue().TryGetValue(out double parsedDouble)) + foreach (JsonNode? node in modParams) { - parsedParamsList.Add(parsedDouble); - continue; + 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); } - 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); } - - modInstance = ParseMod(rulesetInstance, modAcronym, parsedParamsList); - if (modInstance != null) - modInstances.Add(modInstance); - } - else - { - Logger.Log($@"[!mp mods] Couldn't parse mod parameter(s) '{mod[2..]}', ignoring", LoggingTarget.Runtime, LogLevel.Important); } } } - if (!ModUtils.CheckCompatibleSet(modInstances)) + if (!ModUtils.CheckCompatibleSet(requiredMods)) { - Logger.Log($@"[!mp mods] Mods {string.Join(", ", modInstances.Select(mod => mod.Acronym))} are not compatible together", LoggingTarget.Runtime, LogLevel.Important); + Logger.Log($@"[!mp mods] Mods {string.Join(", ", requiredMods.Select(mod => mod.Acronym))} are not compatible together", LoggingTarget.Runtime, LogLevel.Important); break; } @@ -562,8 +605,8 @@ private void processChatCommands(string[] parts) BeatmapID = beatmapInfo.OnlineID, BeatmapChecksum = beatmapInfo.MD5Hash, RulesetID = itemToEdit.RulesetID, - RequiredMods = modInstances.Select(mod => new APIMod(mod)).ToArray(), - AllowedMods = Array.Empty() + RequiredMods = requiredMods.Select(mod => new APIMod(mod)).ToArray(), + AllowedMods = allowedMods.Select(mod => new APIMod(mod)).ToArray(), }; selectionOperation = operationTracker.BeginOperation(); From 6c6e8f078463c232a89a53fab69dd9316c1b40b5 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Wed, 24 Sep 2025 03:47:14 +0100 Subject: [PATCH 2/3] remove modselect-specific logic from fm mp cmd --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 74ba3720c2..6fb154d2b8 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -33,7 +33,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Chat; -using osu.Game.Overlays.Mods; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -169,8 +168,6 @@ public StandAloneChatDisplay(bool postingTextBox = false) private readonly Bindable>> availableMods = new Bindable>>(); - private Bindable>> availableModsState { get; } = new Bindable>>(new Dictionary>()); - [BackgroundDependencyLoader(true)] private void load(ChannelManager manager, BeatmapModelDownloader beatmaps, BeatmapLookupCache beatmapsCache, OsuConfigManager config, OsuGameBase game) { @@ -186,27 +183,6 @@ private void load(ChannelManager manager, BeatmapModelDownloader beatmaps, Beatm Scheduler.Add(processMessageQueue); availableMods.BindTo(game.AvailableMods); - - populateModStates(); - } - - /// - /// yoinked most of the code from ModSelectOverlay.createLocalMods - /// - private void populateModStates() - { - var newLocalAvailableMods = new Dictionary>(); - - foreach (var (modType, mods) in availableMods.Value) - { - var modStates = mods.SelectMany(ModUtils.FlattenMod) - .Select(mod => new ModState(mod.DeepClone())) - .ToArray(); - - newLocalAvailableMods[modType] = modStates; - } - - availableModsState.Value = newLocalAvailableMods; } protected override void LoadComplete() @@ -493,11 +469,10 @@ private void processChatCommands(string[] parts) if (mods.Length == 1 && mods[0].Equals(@"fm", StringComparison.OrdinalIgnoreCase)) { // hardcode freemod to allow all mods, leaves requiredMods empty - var newAllowedMods = availableModsState.Value - .SelectMany(pair => pair.Value) - .Where(state => state.ValidForSelection.Value) - .Select(state => state.Mod) - .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, MatchType.TeamVersus)); + var newAllowedMods = availableMods.Value + .SelectMany(pair => pair.Value) + .SelectMany(ModUtils.FlattenMod) + .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, MatchType.TeamVersus)); allowedMods.AddRange(newAllowedMods); } From a74163606aedb930567a4a431ee7a5a429cfc3a0 Mon Sep 17 00:00:00 2001 From: Dao Heng Liu Date: Thu, 25 Sep 2025 00:11:42 +0100 Subject: [PATCH 3/3] use match type from current room if possible 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 6fb154d2b8..1244ea6e5f 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -472,7 +472,7 @@ private void processChatCommands(string[] parts) var newAllowedMods = availableMods.Value .SelectMany(pair => pair.Value) .SelectMany(ModUtils.FlattenMod) - .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, MatchType.TeamVersus)); + .Where(mod => ModUtils.IsValidFreeModForMatchType(mod, Client.Room?.Settings.MatchType ?? MatchType.TeamVersus)); allowedMods.AddRange(newAllowedMods); }