Hooks
Hooks are bindings run backwards: the host fires them, scripts handle them. Bindings let scripts call the host; hooks let the host call scripts. Put the two together and you have a bidirectional channel between the application and its mods.
This document covers hook declaration, lifecycle phases, registration, invocation, and the runtime conventions that govern hook behavior. Those conventions are the semantics of the event-slot type: everything below applies whether an event is declared as a slot or, for back-compat, in the deprecated standalone hooks section.
Declaration
Section titled “Declaration”A host declares an event as a slot whose accepts is the event-handler type:
{ "slots": [ { "id": "playerDamage", "accepts": ["application/x-xript-hook"] } ]}A mod fills that slot by naming a handler export in its mod manifest’s fills surface, keyed by the slot id:
{ "fills": { "playerDamage": [{ "handler": "onPlayerDamage" }] }}The deprecated standalone form declares the event in the manifest’s hooks section instead, carrying its params and other properties inline:
{ "hooks": { "playerDamage": { "description": "Fired when the player takes damage.", "params": [ { "name": "amount", "type": "number", "description": "Damage amount." }, { "name": "source", "type": "string", "description": "What caused the damage." } ] } }}Either form requires a description. Same rule as bindings: if a modder can’t tell what a hook does, it may as well not exist. The script-side registration helpers below are the runtime’s convenience over the { "handler": "..." } wiring.
Lifecycle Phases
Section titled “Lifecycle Phases”Hooks can optionally declare lifecycle phases. Phases let scripts intervene at specific points in a host operation:
{ "hooks": { "save": { "description": "Fired during the save lifecycle.", "phases": ["pre", "post", "done", "error"], "params": [ { "name": "data", "type": "SaveData", "description": "The save payload." } ] } }}The four standard phases are:
| Phase | When | Typical Use |
|---|---|---|
pre | Before the operation | Validation, modification, cancellation |
post | After the operation, can modify | Result transformation, interception |
done | After all post-processing, sealed | Logging, notifications, observation |
error | When the operation fails | Error recovery, fallback behavior |
Phases are optional. A hook without phases is a plain notification: it fires, handlers run, done. The host controls which phases it declares and the order it fires them in.
Registration (Script Side)
Section titled “Registration (Script Side)”Scripts register handlers via the hooks global object injected by the runtime.
Simple hooks (no phases)
Section titled “Simple hooks (no phases)”hooks.playerDamage((amount, source) => { log(`Player took ${amount} damage from ${source}`);});Phased hooks
Section titled “Phased hooks”hooks.save.pre((data) => { log("About to save: " + data.filename);});
hooks.save.post((data) => { log("Save complete: " + data.filename);});Multiple scripts can register handlers for the same hook (or phase). Handlers run in registration order.
Invocation (Host Side)
Section titled “Invocation (Host Side)”The host fires hooks through the runtime’s fireHook method:
const results = runtime.fireHook("playerDamage", { amount: 25, source: "trap" });
const preResults = runtime.fireHook("save", { phase: "pre", data: savePayload });const postResults = runtime.fireHook("save", { phase: "post", data: savePayload });fireHook returns an array of results from all registered handlers, in registration order. If no handlers are registered, it returns an empty array.
Capability Gating
Section titled “Capability Gating”Hooks can require capabilities, the same model as bindings:
{ "hooks": { "save": { "description": "Fired during the save lifecycle.", "phases": ["pre", "post", "done", "error"], "capability": "persistence" } }}A script without the persistence capability cannot register handlers for this hook. Attempting to register throws a CapabilityDeniedError.
Async Hooks
Section titled “Async Hooks”Hooks can be declared async, controlled by the host:
{ "hooks": { "dataSync": { "description": "Fired when data synchronization occurs.", "async": true } }}When async is true, handlers can use await and fireHook returns a Promise. The host must use the async runtime variant (initXriptAsync / async-capable createRuntime) for async hooks.
Execution Limits
Section titled “Execution Limits”Each hook handler invocation gets its own execution budget by default. Hooks can override the manifest-level limits:
{ "hooks": { "frameTick": { "description": "Fired every frame.", "limits": { "timeout_ms": 5 } } }}Per-hook limits are useful when different hooks have different performance requirements. A frame tick handler needs a tight timeout; a save handler can take longer.
Error Handling
Section titled “Error Handling”Handler Errors
Section titled “Handler Errors”When a hook handler throws an error, the runtime:
- Catches the error
- Wraps it in a
HookErrorwith the hook name and phase - Logs it via the host console
- Continues executing remaining handlers
One handler’s error does not prevent other handlers from running. The fireHook return value includes results from successful handlers; failed handlers contribute undefined.
Error Types
Section titled “Error Types”| Error | When Thrown |
|---|---|
HookError | A handler failed during execution |
CapabilityDeniedError | Registration attempted without the required capability |
Hook Granularity
Section titled “Hook Granularity”Filtering and scoping are the host’s responsibility, not the hook system’s. Instead of one save hook with filter parameters, hosts should expose granular hooks when distinction matters:
{ "hooks": { "save": { "description": "Any save operation.", "phases": ["pre", "on", "post", "error"] }, "autosave": { "description": "Automatic background saves.", "phases": ["pre", "post", "done"] }, "manualSave": { "description": "Player-initiated saves.", "phases": ["pre", "on", "post", "error"] } }}Scripts register for what they care about. The host controls granularity by how many hooks it exposes.
Naming Conventions
Section titled “Naming Conventions”Hook names should follow the same conventions as binding names:
- camelCase (
playerDamage,onSave,frameTick) - Noun or event-oriented (
playerDamage,levelComplete,inventoryChange) - Not verbs unless describing an action (
save,loadare fine as they describe the operation the hook wraps)
Manifest-to-TypeScript Mapping
Section titled “Manifest-to-TypeScript Mapping”The typegen tool generates handler registration types from hooks:
Simple hook
Section titled “Simple hook”declare namespace hooks { function playerDamage(handler: (amount: number, source: string) => void): void;}Phased hook
Section titled “Phased hook”declare namespace hooks { namespace save { function pre(handler: (data: SaveData) => void): void; function done(handler: (data: SaveData) => void): void; function post(handler: (data: SaveData) => void): void; function error(handler: (data: SaveData) => void): void; }}Async hook
Section titled “Async hook”Async hooks wrap the handler return type in Promise:
declare namespace hooks { function dataSync(handler: (payload: SyncPayload) => Promise<void>): void;}