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); };