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)
{