Compare commits
6
Commits
2a7cdb4fad
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5256476c | ||
|
|
f261848734 | ||
|
|
c43872565c | ||
|
|
ecc796031a | ||
|
|
d93e56c1d3 | ||
|
|
de74d71f0f |
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using Vintagestory.API.Common;
|
using Vintagestory.API.Common;
|
||||||
using Vintagestory.API.Server;
|
|
||||||
using Vintagestory.Common;
|
using Vintagestory.Common;
|
||||||
|
|
||||||
namespace CommandHook;
|
namespace CommandHook;
|
||||||
@@ -24,11 +23,12 @@ public static class ChatCommandApiPatch
|
|||||||
/// typing in chat, since both go through <c>Execute</c> the same way
|
/// typing in chat, since both go through <c>Execute</c> the same way
|
||||||
/// internally.
|
/// internally.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// If a Before listener sets <see cref="CommandData.Cancel"/>, this returns
|
/// If a Before listener returns a non-null <see cref="TextCommandResult"/>,
|
||||||
/// false to stop the original <c>Execute</c> from running at all, and
|
/// this returns false to stop the original <c>Execute</c> from running at
|
||||||
/// invokes <paramref name="onCommandComplete"/> directly with a
|
/// all, and invokes <paramref name="onCommandComplete"/> directly with that
|
||||||
/// <see cref="EnumCommandStatus.Deferred"/> result so the caller doesn't see
|
/// result so the caller sees whatever the listener intended (a real error
|
||||||
/// a generic failure. After listeners are not fired in this case.
|
/// message, a silent <see cref="TextCommandResult.Deferred"/>, etc). After
|
||||||
|
/// listeners are not fired in this case.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// If nothing cancels, this returns true and lets the original <c>Execute</c>
|
/// If nothing cancels, this returns true and lets the original <c>Execute</c>
|
||||||
@@ -39,7 +39,10 @@ public static class ChatCommandApiPatch
|
|||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="commandName">The command name being executed, without the leading slash.</param>
|
/// <param name="commandName">The command name being executed, without the leading slash.</param>
|
||||||
/// <param name="args">The calling args supplied by the game, used here to resolve the sender.</param>
|
/// <param name="args">
|
||||||
|
/// The calling args supplied by the game. Passed straight through to listeners,
|
||||||
|
/// live and unmodified, the engine's own object for this invocation.
|
||||||
|
/// </param>
|
||||||
/// <param name="onCommandComplete">
|
/// <param name="onCommandComplete">
|
||||||
/// The completion callback supplied by the game. Replaced with a wrapper that
|
/// The completion callback supplied by the game. Replaced with a wrapper that
|
||||||
/// fires After listeners after invoking the original.
|
/// fires After listeners after invoking the original.
|
||||||
@@ -66,19 +69,10 @@ public static class ChatCommandApiPatch
|
|||||||
if (system == null)
|
if (system == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var sender = args.Caller.Player as IServerPlayer;
|
var cancelResult = system.FireBefore(commandName, args);
|
||||||
// Console invocations have no IServerPlayer, the cast just yields null,
|
if (cancelResult != 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
|
onCommandComplete?.Invoke(cancelResult);
|
||||||
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +83,7 @@ public static class ChatCommandApiPatch
|
|||||||
onCommandComplete = result =>
|
onCommandComplete = result =>
|
||||||
{
|
{
|
||||||
original?.Invoke(result);
|
original?.Invoke(result);
|
||||||
// data is captured by the closure, copy it so FireAfter gets its
|
system.FireAfter(commandName, args, result);
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
using Vintagestory.API.Server;
|
|
||||||
|
|
||||||
namespace CommandHook;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The data passed to Before/After listeners for a single command invocation.
|
|
||||||
/// Passed by ref through the hot path, no allocation per command.
|
|
||||||
/// </summary>
|
|
||||||
public struct CommandData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The player who ran the command, or null if it came from the server console.
|
|
||||||
/// </summary>
|
|
||||||
public readonly IServerPlayer? Sender;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The full command text as typed, including the leading slash.
|
|
||||||
/// </summary>
|
|
||||||
public readonly string FullCommand;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The command name only, without the leading slash (matches what listeners
|
|
||||||
/// register in <see cref="ICommandHookListener.Commands"/>).
|
|
||||||
/// </summary>
|
|
||||||
public readonly string CommandName;
|
|
||||||
private byte flags;
|
|
||||||
|
|
||||||
private const byte FlagIsPlayerCommand = 1 << 0;
|
|
||||||
private const byte FlagCancel = 1 << 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True if a player ran this command, false if it came from the server console.
|
|
||||||
/// Equivalent to <c>Sender != null</c>.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsPlayerCommand => (flags & FlagIsPlayerCommand) != 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
public bool Cancel
|
|
||||||
{
|
|
||||||
get => (flags & FlagCancel) != 0;
|
|
||||||
set => flags = value ? (byte)(flags | FlagCancel) : (byte)(flags & ~FlagCancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the command data for a single invocation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">The player who ran the command, or null for console.</param>
|
|
||||||
/// <param name="commandName">The command name without the leading slash.</param>
|
|
||||||
/// <param name="fullCommand">The full command text as typed, including the leading slash.</param>
|
|
||||||
public CommandData(IServerPlayer? sender, string commandName, string fullCommand)
|
|
||||||
{
|
|
||||||
Sender = sender;
|
|
||||||
CommandName = commandName;
|
|
||||||
FullCommand = fullCommand;
|
|
||||||
flags = sender != null ? FlagIsPlayerCommand : (byte)0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,15 +3,28 @@ using Vintagestory.API.Common;
|
|||||||
namespace CommandHook;
|
namespace CommandHook;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked before a watched command runs. Set <c>data.Cancel = true</c> to stop
|
/// Invoked before a watched command runs. Return a non-null
|
||||||
/// the command from executing.
|
/// <see cref="TextCommandResult"/> to cancel the command and report that
|
||||||
|
/// result to the caller (e.g. <see cref="TextCommandResult.Error(string, string)"/>
|
||||||
|
/// for a visible reason, or <see cref="TextCommandResult.Deferred"/> to cancel
|
||||||
|
/// silently). Return null to let the command run normally.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The command data, passed by ref so you can read or cancel it.</param>
|
/// <param name="callingArgs">
|
||||||
public delegate void CommandBeforeDelegate(ref CommandData data);
|
/// The live calling args for this invocation, supplied directly by the engine.
|
||||||
|
/// </param>
|
||||||
|
public delegate TextCommandResult? CommandBeforeDelegate(TextCommandCallingArgs callingArgs);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The command data, passed by ref for consistency with <see cref="CommandBeforeDelegate"/>.</param>
|
/// <param name="callingArgs">
|
||||||
|
/// 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.
|
||||||
|
/// </param>
|
||||||
/// <param name="result">The result the command produced.</param>
|
/// <param name="result">The result the command produced.</param>
|
||||||
public delegate void CommandAfterDelegate(ref CommandData data, TextCommandResult result);
|
public delegate void CommandAfterDelegate(
|
||||||
|
TextCommandCallingArgs callingArgs,
|
||||||
|
TextCommandResult result
|
||||||
|
);
|
||||||
|
|||||||
@@ -147,41 +147,44 @@ public class CommandHookModSystem : ModSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called from ChatCommandApiPatch.Prefix before the real command executes.
|
// Called from ChatCommandApiPatch.Prefix before the real command executes.
|
||||||
// CommandData is passed by ref the whole way down, no allocation here.
|
// callingArgs is the engine's own live object, passed straight through,
|
||||||
// Each listener's Before is isolated in its own try/catch so one mod
|
// no allocation here. Each listener's Before is isolated in its own
|
||||||
// throwing doesn't stop the rest from running or break command dispatch
|
// try/catch so one mod throwing doesn't stop the rest from running or
|
||||||
// for the server. If a listener sets data.Cancel, we stop walking the
|
// break command dispatch for the server. The first listener to return a
|
||||||
// rest of the listeners immediately rather than letting them all run
|
// non-null result wins, we stop walking immediately rather than letting
|
||||||
// against an already-cancelled command.
|
// later listeners run against an already-cancelled command.
|
||||||
internal bool FireBefore(string commandName, ref CommandData data)
|
internal TextCommandResult? FireBefore(string commandName, TextCommandCallingArgs callingArgs)
|
||||||
{
|
{
|
||||||
if (registrations.TryGetValue(commandName, out var mods))
|
if (registrations.TryGetValue(commandName, out var mods))
|
||||||
{
|
{
|
||||||
foreach (var (modId, reg) in mods)
|
foreach (var (modId, reg) in mods)
|
||||||
{
|
{
|
||||||
|
TextCommandResult? result = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
reg.Before?.Invoke(ref data);
|
result = reg.Before?.Invoke(callingArgs);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Mod.Logger.Error("[{0}] Before /{1} threw: {2}", modId, commandName, ex);
|
Mod.Logger.Error("[{0}] Before /{1} threw: {2}", modId, commandName, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.Cancel)
|
if (result != null)
|
||||||
break;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.Cancel;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from the wrapped onCommandComplete in ChatCommandApiPatch.Prefix,
|
// Called from the wrapped onCommandComplete in ChatCommandApiPatch.Prefix,
|
||||||
// only on the path where the command actually ran (Before didn't cancel).
|
// only on the path where the command actually ran (no Before listener
|
||||||
// Same per-listener try/catch as FireBefore, but there's no early-out here
|
// cancelled it). Same per-listener try/catch as FireBefore.
|
||||||
// since cancelling after the fact doesn't mean anything, the command
|
internal void FireAfter(
|
||||||
// already ran.
|
string commandName,
|
||||||
internal void FireAfter(string commandName, ref CommandData data, TextCommandResult result)
|
TextCommandCallingArgs callingArgs,
|
||||||
|
TextCommandResult result
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (registrations.TryGetValue(commandName, out var mods))
|
if (registrations.TryGetValue(commandName, out var mods))
|
||||||
{
|
{
|
||||||
@@ -189,7 +192,7 @@ public class CommandHookModSystem : ModSystem
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
reg.After?.Invoke(ref data, result);
|
reg.After?.Invoke(callingArgs, result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ namespace CommandHook;
|
|||||||
public struct CommandRegistration
|
public struct CommandRegistration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called before the command runs. Set <see cref="CommandData.Cancel"/> on
|
/// Called before the command runs. Return a non-null <see cref="TextCommandResult"/>
|
||||||
/// <paramref name="data"/> via the ref parameter inside your delegate to stop
|
/// to cancel the command, report that result to the caller, and skip any
|
||||||
/// the command from executing and skip any remaining Before listeners.
|
/// remaining Before listeners. Return null to let the command run.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly CommandBeforeDelegate? Before;
|
public readonly CommandBeforeDelegate? Before;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
{
|
{}
|
||||||
"hello": "hello world!"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"authors": [
|
"authors": [
|
||||||
"anth64"
|
"anth64"
|
||||||
],
|
],
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"side": "Server",
|
"side": "Server",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"game": "1.22.3"
|
"game": "1.22.3"
|
||||||
|
|||||||
@@ -4,7 +4,21 @@ Server-side mod for Vintage Story. Lets other mods hook into chat commands
|
|||||||
before and after they run, namespaced per mod so multiple mods can watch
|
before and after they run, namespaced per mod so multiple mods can watch
|
||||||
the same command without colliding.
|
the same command without colliding.
|
||||||
|
|
||||||
## How it works
|
## Usage
|
||||||
|
|
||||||
|
Install it like any other mod. It has no effect unless something else
|
||||||
|
depends on it.
|
||||||
|
|
||||||
|
## For mod development
|
||||||
|
|
||||||
|
Add it as a dependency in your `modinfo.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dependencies": {
|
||||||
|
"game": "1.22.3",
|
||||||
|
"commandhook": "2.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
CommandHook patches `ChatCommandApi.Execute` and dispatches to whoever
|
CommandHook patches `ChatCommandApi.Execute` and dispatches to whoever
|
||||||
has registered. Your mod doesn't patch anything itself, it just implements
|
has registered. Your mod doesn't patch anything itself, it just implements
|
||||||
@@ -17,18 +31,26 @@ A listener provides:
|
|||||||
- `Commands`, the command names you want to watch, without the leading slash
|
- `Commands`, the command names you want to watch, without the leading slash
|
||||||
- `Registration`, a `Before` delegate, an `After` delegate, or both
|
- `Registration`, a `Before` delegate, an `After` delegate, or both
|
||||||
|
|
||||||
`Before` runs before the command executes. Set `data.Cancel = true` inside it
|
`Before` receives the engine's own `TextCommandCallingArgs` for the
|
||||||
to stop the command from running. If you cancel, any remaining `Before`
|
invocation, live and unmodified. Return a non-null `TextCommandResult` to
|
||||||
listeners for that command are skipped, and `After` never fires for that
|
cancel the command and report that result to the caller, use the engine's
|
||||||
invocation.
|
own factories, e.g. `TextCommandResult.Error(...)` for a visible reason, or
|
||||||
|
`TextCommandResult.Deferred` to cancel silently (per its own doc comment,
|
||||||
|
this prints no output). Return `null` to let the command run normally. If
|
||||||
|
you cancel, any remaining `Before` listeners for that command are skipped,
|
||||||
|
and `After` never fires for that invocation.
|
||||||
|
|
||||||
`After` runs once the command has produced a result. It only fires on the
|
`After` receives the same `TextCommandCallingArgs` object plus the
|
||||||
path where the command actually ran.
|
`TextCommandResult` the command actually produced. It only fires on the
|
||||||
|
path where the command ran.
|
||||||
|
|
||||||
`CommandData` is passed by `ref` the whole way through, no allocation per
|
### A note on `TextCommandCallingArgs` at Before vs After
|
||||||
command.
|
|
||||||
|
|
||||||
## Example
|
This is the engine's own live object, not a copy, what's safe to read
|
||||||
|
depends on timing and on how the specific command you're watching is
|
||||||
|
implemented internally.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class MyListener : ICommandHookListener
|
public class MyListener : ICommandHookListener
|
||||||
@@ -42,23 +64,26 @@ public class MyListener : ICommandHookListener
|
|||||||
// Wire up Before, After, or both.
|
// Wire up Before, After, or both.
|
||||||
public CommandRegistration Registration => new(Before, After);
|
public CommandRegistration Registration => new(Before, After);
|
||||||
|
|
||||||
private void Before(ref CommandData data)
|
private TextCommandResult? Before(TextCommandCallingArgs args)
|
||||||
{
|
{
|
||||||
// data.Sender is null for console invocations.
|
// args.Caller.Player is null for console invocations.
|
||||||
if (data.Sender != null && !IsAllowed(data.Sender))
|
if (args.Caller.Player is IServerPlayer player && !IsAllowed(player))
|
||||||
{
|
{
|
||||||
// Stops the command from running and skips any remaining
|
// Cancels the command, skips any remaining Before listeners,
|
||||||
// Before listeners. After never fires for this invocation.
|
// and After never fires for this invocation. The caller sees
|
||||||
data.Cancel = true;
|
// this exact result.
|
||||||
|
return TextCommandResult.Error("You're not allowed to do that", "notallowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void After(ref CommandData data, TextCommandResult result)
|
private void After(TextCommandCallingArgs args, TextCommandResult result)
|
||||||
{
|
{
|
||||||
// Only runs if nothing cancelled. result is whatever the command
|
// Only runs if nothing cancelled. result is whatever the command
|
||||||
// actually produced, check result.Status for success/error/deferred.
|
// actually produced, check result.Status for success/error/deferred.
|
||||||
if (result.Status != EnumCommandStatus.Success)
|
if (result.Status != EnumCommandStatus.Success)
|
||||||
Logger.Warn($"/{data.CommandName} failed: {result.StatusMessage}");
|
Logger.Warn($"A watched command failed: {result.StatusMessage}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -83,4 +108,3 @@ public override void Dispose()
|
|||||||
|
|
||||||
Calling `Register` again with the same `ModId` replaces your existing
|
Calling `Register` again with the same `ModId` replaces your existing
|
||||||
registration. If your `Commands` list hasn't changed, it's a no-op.
|
registration. If your `Commands` list hasn't changed, it's a no-op.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user