2025-12-02 21:21:13 +01:00
2025-12-02 21:21:13 +01:00
2025-12-02 21:21:13 +01:00
2025-12-02 21:43:46 +01:00

CommandHook

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 the same command without colliding.

Usage

Install it like any other mod. It has no effect unless something else depends on it.

For mod development

Set the COMMANDHOOK environment variable to the folder containing CommandHook.dll (i.e. wherever you've built CommandHook), the same way VINTAGE_STORY points at your game install.

Linux/macOS (add to ~/.bashrc, ~/.zshrc, etc.):

export COMMANDHOOK="/path/to/CommandHook/CommandHook/bin/Release"

Windows (PowerShell):

[Environment]::SetEnvironmentVariable("COMMANDHOOK", "C:\path\to\CommandHook\CommandHook\bin\Release", "User")

Then add a Reference to your .csproj next to your other game references:

<Reference Include="CommandHook">
  <HintPath>$(COMMANDHOOK)/CommandHook.dll</HintPath>
  <Private>false</Private>
</Reference>

Add it as a dependency in your modinfo.json:

"dependencies": {
    "game": "1.22.3",
    "commandhook": "2.0.0"
}

CommandHook patches ChatCommandApi.Execute and dispatches to whoever has registered. Your mod doesn't patch anything itself, it just implements ICommandHookListener and calls CommandHookModSystem.Register.

A listener provides:

  • ModId, your mod's id. Used to namespace your registration so it doesn't collide with another mod watching the same command
  • Commands, the command names you want to watch, without the leading slash
  • Registration, a Before delegate, an After delegate, or both

Before receives the engine's own TextCommandCallingArgs for the invocation, live and unmodified. Return a non-null TextCommandResult to cancel the command and report that result to the caller, use the engine's 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 receives the same TextCommandCallingArgs object plus the TextCommandResult the command actually produced. It only fires on the path where the command ran.

A note on TextCommandCallingArgs at Before vs After

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

public class MyListener : ICommandHookListener
{
    // Your mod's id, used to namespace this registration.
    public string ModId => "mymod";

    // Commands you want to watch, no leading slash.
    public IReadOnlyList<string> Commands => new[] { "tp" };

    // Wire up Before, After, or both.
    public CommandRegistration Registration => new(Before, After);

    private TextCommandResult? Before(TextCommandCallingArgs args)
    {
        // args.Caller.Player is null for console invocations.
        if (args.Caller.Player is IServerPlayer player && !IsAllowed(player))
        {
            // Cancels the command, skips any remaining Before listeners,
            // and After never fires for this invocation. The caller sees
            // this exact result.
            return TextCommandResult.Error("You're not allowed to do that", "notallowed");
        }

        return null;
    }

    private void After(TextCommandCallingArgs args, TextCommandResult result)
    {
        // Only runs if nothing cancelled. result is whatever the command
        // actually produced, check result.Status for success/error/deferred.
        if (result.Status != EnumCommandStatus.Success)
            Logger.Warn($"A watched command failed: {result.StatusMessage}");
    }
}

Register it once your mod starts, unregister it on dispose:

public override void StartServerSide(ICoreServerAPI api)
{
    // CommandHookModSystem.Instance is null if CommandHook isn't loaded,
    // hence the ?. Register your listener once, here.
    CommandHookModSystem.Instance?.Register(myListener);
}

public override void Dispose()
{
    // Always unregister on dispose, otherwise a stale listener stays in
    // the dispatch table after your mod is gone.
    CommandHookModSystem.Instance?.Unregister(myListener);
}

Calling Register again with the same ModId replaces your existing registration. If your Commands list hasn't changed, it's a no-op.

S
Description
A server-side API for routing and overriding vanilla commands.
Readme MPL-2.0
134 KiB
Languages
C# 99.3%
PowerShell 0.4%
Shell 0.3%