From 398c9d01c1071ad6d11ab37fac3693f51756ecec Mon Sep 17 00:00:00 2001 From: anth64 Date: Sun, 21 Jun 2026 12:18:19 +0200 Subject: [PATCH] docs(CommandHookModSystem): add XML doc comments to Register/Unregister and internal comments to Rebuild, FireBefore, FireAfter, CommandListEquals --- CommandHook/CommandHookModSystem.cs | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/CommandHook/CommandHookModSystem.cs b/CommandHook/CommandHookModSystem.cs index 6d16cb3..001f7e7 100644 --- a/CommandHook/CommandHookModSystem.cs +++ b/CommandHook/CommandHookModSystem.cs @@ -7,6 +7,20 @@ using Vintagestory.API.Server; namespace CommandHook; +/// +/// Server-side event bus for chat commands. Other mods register an +/// here to get Before/After callbacks on +/// specific command names, namespaced by +/// so multiple mods can watch the same command without stepping on each other. +/// +/// +/// Patches ChatCommandApi.Execute via Harmony. Server only, see +/// . A listener's +/// and are read at registration +/// time and cached in a +/// for fast lookup on the command dispatch path. If you change which commands a +/// listener watches, call again with the updated list. +/// public class CommandHookModSystem : ModSystem { internal static CommandHookModSystem? Instance; @@ -38,6 +52,17 @@ public class CommandHookModSystem : ModSystem Instance = null; } + /// + /// Registers a listener for the commands it returns from . + /// + /// + /// Calling this again for a that is already + /// registered replaces its previous registration. If the new + /// list is identical to the existing one this is a no-op, no rebuild happens. + /// If has a null or empty + /// list, this just calls instead. + /// + /// The listener to register or update. public void Register(ICommandHookListener listener) { var commands = listener.Commands; @@ -66,6 +91,11 @@ public class CommandHookModSystem : ModSystem Rebuild(); } + /// + /// Removes a listener's registration entirely. Safe to call on a listener + /// that was never registered, this is a no-op in that case. + /// + /// The listener to remove, matched by . public void Unregister(ICommandHookListener listener) { int index = listeners.FindIndex(l => l.ModId == listener.ModId); @@ -77,6 +107,14 @@ public class CommandHookModSystem : ModSystem Rebuild(); } + // Rebuilds the dispatch table from listeners. Runs unconditionally and + // immediately whenever Register/Unregister actually changes something, + // no batching or startup-phase awareness. Command list sizes here are + // small (a handful of mods, a handful of commands each), so the realloc + // on every change is cheap and not worth the complexity of an incremental + // update. The two-level structure (command -> ModId -> registration) is + // what FireBefore/FireAfter walk on the hot path, so it's built once here + // instead of being derived per-dispatch. private void Rebuild() { var builder = new Dictionary>(); @@ -108,6 +146,13 @@ public class CommandHookModSystem : ModSystem registrations = pruned.ToFrozenDictionary(); } + // Called from ChatCommandApiPatch.Prefix before the real command executes. + // CommandData is passed by ref the whole way down, no allocation here. + // Each listener's Before is isolated in its own try/catch so one mod + // throwing doesn't stop the rest from running or break command dispatch + // for the server. If a listener sets data.Cancel, we stop walking the + // rest of the listeners immediately rather than letting them all run + // against an already-cancelled command. internal bool FireBefore(string commandName, ref CommandData data) { if (registrations.TryGetValue(commandName, out var mods)) @@ -131,6 +176,11 @@ public class CommandHookModSystem : ModSystem return data.Cancel; } + // Called from the wrapped onCommandComplete in ChatCommandApiPatch.Prefix, + // only on the path where the command actually ran (Before didn't cancel). + // Same per-listener try/catch as FireBefore, but there's no early-out here + // since cancelling after the fact doesn't mean anything, the command + // already ran. internal void FireAfter(string commandName, ref CommandData data, TextCommandResult result) { if (registrations.TryGetValue(commandName, out var mods)) @@ -149,6 +199,10 @@ public class CommandHookModSystem : ModSystem } } + // Order-sensitive on purpose. Register only treats an update as a no-op + // if the list is identical in order and content, a reordered list still + // triggers a rebuild. Cheap to check (just an index walk, no allocation) + // and correctness here matters more than being lenient about ordering. private static bool CommandListEquals(IReadOnlyList a, IReadOnlyList b) { if (a.Count != b.Count)