diff --git a/CommandHook/ChatCommandApiPatch.cs b/CommandHook/ChatCommandApiPatch.cs index 21e2581..7051922 100644 --- a/CommandHook/ChatCommandApiPatch.cs +++ b/CommandHook/ChatCommandApiPatch.cs @@ -1,7 +1,6 @@ using System; using HarmonyLib; using Vintagestory.API.Common; -using Vintagestory.API.Server; using Vintagestory.Common; namespace CommandHook; @@ -24,11 +23,12 @@ public static class ChatCommandApiPatch /// 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 a Before listener returns a non-null , + /// this returns false to stop the original Execute from running at + /// all, and invokes directly with that + /// result so the caller sees whatever the listener intended (a real error + /// message, a silent , etc). After + /// listeners are not fired in this case. /// /// /// If nothing cancels, this returns true and lets the original Execute @@ -39,7 +39,10 @@ public static class ChatCommandApiPatch /// /// /// The command name being executed, without the leading slash. - /// The calling args supplied by the game, used here to resolve the sender. + /// + /// The calling args supplied by the game. Passed straight through to listeners, + /// live and unmodified, the engine's own object for this invocation. + /// /// /// The completion callback supplied by the game. Replaced with a wrapper that /// fires After listeners after invoking the original. @@ -66,19 +69,10 @@ public static class ChatCommandApiPatch 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)) + var cancelResult = system.FireBefore(commandName, args); + if (cancelResult != null) { - // 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 } - ); + onCommandComplete?.Invoke(cancelResult); return false; } @@ -89,11 +83,7 @@ public static class ChatCommandApiPatch 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); + system.FireAfter(commandName, args, result); }; return true; diff --git a/CommandHook/CommandData.cs b/CommandHook/CommandData.cs deleted file mode 100644 index 4c44adc..0000000 --- a/CommandHook/CommandData.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Vintagestory.API.Server; - -namespace CommandHook; - -/// -/// The data passed to Before/After listeners for a single command invocation. -/// Passed by ref through the hot path, no allocation per command. -/// -public struct CommandData -{ - /// - /// The player who ran the command, or null if it came from the server console. - /// - public readonly IServerPlayer? Sender; - - /// - /// The full command text as typed, including the leading slash. - /// - public readonly string FullCommand; - - /// - /// The command name only, without the leading slash (matches what listeners - /// register in ). - /// - public readonly string CommandName; - private byte flags; - - private const byte FlagIsPlayerCommand = 1 << 0; - private const byte FlagCancel = 1 << 1; - - /// - /// True if a player ran this command, false if it came from the server console. - /// Equivalent to Sender != null. - /// - public bool IsPlayerCommand => (flags & FlagIsPlayerCommand) != 0; - - /// - /// Set this to true in a Before listener to stop the command from executing. - /// Stops any remaining Before listeners from running too, and skips After entirely. - /// - public bool Cancel - { - get => (flags & FlagCancel) != 0; - set => flags = value ? (byte)(flags | FlagCancel) : (byte)(flags & ~FlagCancel); - } - - /// - /// Creates the command data for a single invocation. - /// - /// The player who ran the command, or null for console. - /// The command name without the leading slash. - /// The full command text as typed, including the leading slash. - public CommandData(IServerPlayer? sender, string commandName, string fullCommand) - { - Sender = sender; - CommandName = commandName; - FullCommand = fullCommand; - flags = sender != null ? FlagIsPlayerCommand : (byte)0; - } -} diff --git a/CommandHook/CommandDelegate.cs b/CommandHook/CommandDelegate.cs index 20ada7f..a93b423 100644 --- a/CommandHook/CommandDelegate.cs +++ b/CommandHook/CommandDelegate.cs @@ -3,15 +3,28 @@ using Vintagestory.API.Common; namespace CommandHook; /// -/// Invoked before a watched command runs. Set data.Cancel = true to stop -/// the command from executing. +/// Invoked before a watched command runs. Return a non-null +/// to cancel the command and report that +/// result to the caller (e.g. +/// for a visible reason, or to cancel +/// silently). Return null to let the command run normally. /// -/// The command data, passed by ref so you can read or cancel it. -public delegate void CommandBeforeDelegate(ref CommandData data); +/// +/// The live calling args for this invocation, supplied directly by the engine. +/// +public delegate TextCommandResult? CommandBeforeDelegate(TextCommandCallingArgs callingArgs); /// -/// Invoked after a watched command has run. Not invoked if a Before listener cancelled it. +/// Invoked after a watched command has run. Not invoked if a Before listener +/// (for this command, any mod) cancelled it. /// -/// The command data, passed by ref for consistency with . +/// +/// The same live calling args object passed to Before. By now the real +/// command handler has had full access to it, and what's safe to read +/// depends on how that command is implemented internally. +/// /// The result the command produced. -public delegate void CommandAfterDelegate(ref CommandData data, TextCommandResult result); +public delegate void CommandAfterDelegate( + TextCommandCallingArgs callingArgs, + TextCommandResult result +); diff --git a/CommandHook/CommandHookModSystem.cs b/CommandHook/CommandHookModSystem.cs index 001f7e7..9d92b85 100644 --- a/CommandHook/CommandHookModSystem.cs +++ b/CommandHook/CommandHookModSystem.cs @@ -147,41 +147,44 @@ public class CommandHookModSystem : ModSystem } // 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) + // callingArgs is the engine's own live object, passed straight through, + // 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. The first listener to return a + // non-null result wins, we stop walking immediately rather than letting + // later listeners run against an already-cancelled command. + internal TextCommandResult? FireBefore(string commandName, TextCommandCallingArgs callingArgs) { if (registrations.TryGetValue(commandName, out var mods)) { foreach (var (modId, reg) in mods) { + TextCommandResult? result = null; try { - reg.Before?.Invoke(ref data); + result = reg.Before?.Invoke(callingArgs); } catch (Exception ex) { Mod.Logger.Error("[{0}] Before /{1} threw: {2}", modId, commandName, ex); } - if (data.Cancel) - break; + if (result != null) + return result; } } - return data.Cancel; + return null; } // 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) + // only on the path where the command actually ran (no Before listener + // cancelled it). Same per-listener try/catch as FireBefore. + internal void FireAfter( + string commandName, + TextCommandCallingArgs callingArgs, + TextCommandResult result + ) { if (registrations.TryGetValue(commandName, out var mods)) { @@ -189,7 +192,7 @@ public class CommandHookModSystem : ModSystem { try { - reg.After?.Invoke(ref data, result); + reg.After?.Invoke(callingArgs, result); } catch (Exception ex) {