diff --git a/CommandHook/ChatCommandApiPatch.cs b/CommandHook/ChatCommandApiPatch.cs
index 1104723..21e2581 100644
--- a/CommandHook/ChatCommandApiPatch.cs
+++ b/CommandHook/ChatCommandApiPatch.cs
@@ -6,9 +6,48 @@ using Vintagestory.Common;
namespace CommandHook;
+///
+/// Harmony prefix patch on ChatCommandApi.Execute. This is the only place
+/// CommandHook hooks into the game, everything else in the mod feeds into this.
+///
[HarmonyPatch(typeof(ChatCommandApi))]
public static class ChatCommandApiPatch
{
+ ///
+ /// Runs before the game's own command execution. Fires Before listeners for
+ /// and, if none of them cancel, lets the
+ /// original Execute run and wraps
+ /// so After listeners fire once a result exists.
+ ///
+ ///
+ /// Runs for both invocation paths, server console and a connected player
+ /// typing in chat, since both go through Execute the same way
+ /// internally.
+ ///
+ /// If a Before listener sets , this returns
+ /// false to stop the original Execute from running at all, and
+ /// invokes directly with a
+ /// result so the caller doesn't see
+ /// a generic failure. After listeners are not fired in this case.
+ ///
+ ///
+ /// If nothing cancels, this returns true and lets the original Execute
+ /// run, but first replaces with a wrapper
+ /// that calls the original callback, then fires After. The original callback
+ /// always runs first, so nothing about the vanilla command flow changes from
+ /// the caller's point of view.
+ ///
+ ///
+ /// The command name being executed, without the leading slash.
+ /// The calling args supplied by the game, used here to resolve the sender.
+ ///
+ /// The completion callback supplied by the game. Replaced with a wrapper that
+ /// fires After listeners after invoking the original.
+ ///
+ ///
+ /// False to skip the original Execute (a Before listener cancelled it),
+ /// true to let it run normally.
+ ///
[HarmonyPatch(
"Execute",
new[] { typeof(string), typeof(TextCommandCallingArgs), typeof(Action) }
@@ -21,24 +60,38 @@ public static class ChatCommandApiPatch
)
{
var system = CommandHookModSystem.Instance;
+ // Instance is null before StartServerSide runs or after Dispose. Harmony
+ // patches stay applied for the process lifetime, so this null check is
+ // what actually guards against firing into a torn-down mod.
if (system == null)
return true;
var sender = args.Caller.Player as IServerPlayer;
+ // Console invocations have no IServerPlayer, the cast just yields null,
+ // which is exactly what CommandData(sender, ...) expects to mean "console".
var data = new CommandData(sender, commandName, "/" + commandName);
if (system.FireBefore(commandName, ref data))
{
+ // Cancelled by a Before listener. Report Deferred rather than just
+ // swallowing it silently, so the original caller (console or player)
+ // sees something other than the command quietly doing nothing.
onCommandComplete?.Invoke(
new TextCommandResult { Status = EnumCommandStatus.Deferred }
);
return false;
}
+ // Not cancelled. Capture the game's own callback before replacing it,
+ // since we still need to call it, this patch only adds behavior around
+ // the vanilla flow, it never removes the vanilla completion handling.
var original = onCommandComplete;
onCommandComplete = result =>
{
original?.Invoke(result);
+ // data is captured by the closure, copy it so FireAfter gets its
+ // own ref target instead of sharing one with whatever else might
+ // still be holding the original local.
var afterData = data;
system.FireAfter(commandName, ref afterData, result);
};