diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ee0f13 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# 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. + +## How it works + +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` runs before the command executes. Set `data.Cancel = true` inside it +to stop the command from running. 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 +path where the command actually ran. + +`CommandData` is passed by `ref` the whole way through, no allocation per +command. + +## Example + +```csharp +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 Commands => new[] { "tp" }; + + // Wire up Before, After, or both. + public CommandRegistration Registration => new(Before, After); + + private void Before(ref CommandData data) + { + // data.Sender is null for console invocations. + if (data.Sender != null && !IsAllowed(data.Sender)) + { + // Stops the command from running and skips any remaining + // Before listeners. After never fires for this invocation. + data.Cancel = true; + } + } + + private void After(ref CommandData data, 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($"/{data.CommandName} failed: {result.StatusMessage}"); + } +} +``` + +Register it once your mod starts, unregister it on dispose: + +```csharp +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. +