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)