tm.hooks
Hook pipelines. A resource fires named events through a pipeline, and its own modules and other resources extend or veto behavior by registering hooks, so a new feature is a hook instead of an edit to the owner. Shared (both sides).
local hooks = tm.hooks.pipeline('placeables', { indexKey = 'name' }) -- once per resource
hooks:expose() -- optional: other resources may register
local ok, reason = hooks:check('canPlace', { name = item, source = source }) -- a hook can veto
if not ok then return false, reason end
hooks:emit('placed', { name = item, id = id }) -- observe only
hooks:on('canPlace', function(payload) return false, 'Not here.' end, { filter = { name = 'door' }, priority = -1 })
tm.exports.get('placeables'):registerHook('placed', fn, { filter = { name = { 'door', 'gate' } } })Pipeline
tm.hooks.pipeline(name, options?) creates the pipeline for this resource, or returns the existing one with that name.
| Option | Meaning |
|---|---|
indexKey | The payload field most filters target. Default name. Hooks filtered on it are bucketed, so an event for a payload nobody hooked costs one table lookup. |
publicPayload(event, payload) | Builds the small plain-data table hooks from other resources receive instead of the payload (no live objects, nothing large). Filters and in-resource hooks still see the full payload. If it errors, an empty table is passed and the error is logged. |
| Method | Notes |
|---|---|
pipeline:on(event, fn, options?) | Registers a hook and returns its id. |
pipeline:off(id) | Removes one hook. Returns whether it existed. |
pipeline:removeResource(resource, id?) | Removes every hook a resource registered (or only id). Done automatically when a resource stops. |
pipeline:has(event) | True when anything is registered. Lets a hot path skip building a payload. |
pipeline:check(event, payload) | Runs a vetoable event. Returns ok, reason. |
pipeline:emit(event, payload) | Runs an observe-only event. Return values are ignored. |
pipeline:replay(event, source) | Names where existing items are, so late hooks can catch up (below). |
pipeline:expose(names?) | Lets other resources register hooks through exports (below). |
Hook behavior
- Return
false(orfalse, 'reason') from acheckhook to veto and stop the chain. Any other return, or an error (logged, never a veto), lets the chain continue.emitignores returns. Only a string reason is passed on. priority: lower runs first, ties run in registration order. Default 0.filtermaps payload fields to accepted values (one value or an array). A table field such astagsmatches when any entry is accepted. All fields must match.filteronly compares exact values. For anything else (Lua patterns, either of two fields, a live lookup table) passwhen, a predicate that gets the payload and returns true to run the hook:{ when = function(p) return tostring(p.inventoryId):find('^stash_') ~= nil end }. It runs afterfilter, so keepfilterfor cheap exact checks. Awhenthat errors is logged and skips the hook, never a veto. Awhenfrom another resource is a function reference, so it costs a round trip per event: preferfilterthere.- A hook that takes 50 ms or more logs a warning.
resource(option) is the owner used for cleanup. It defaults to the calling resource.
Other resources
pipeline:expose() registers two exports in the owning resource at runtime (no manifest entry needed): registerHook(event, fn, options) (returns the hook id, or false for a bad call) and removeHooks(id?) (removes the caller's hooks, or one by id). Pass { register = 'name', remove = 'name' } to rename them, for example when a resource has two pipelines. Consumers call them through a handle:
tm.exports.onStart('placeables', function()
tm.exports.get('placeables'):registerHook('placed', function(view) ... end, { filter = { name = 'door' } })
end)Hooks registered this way receive a serialized copy of the payload (the publicPayload table when the pipeline defines one), so they cannot mutate it, and a veto may lose its reason string. They are removed when their resource stops. Register them from onStart, because the owner forgets them when it restarts.
What tm_lib itself must provide for this to work: nothing. expose uses plain exports(...) in the owning resource and the consumer uses tm.exports (so the import needs only tm.log).
Catching up
A hook registered after items already exist can catch up: name where they are, then register with replay = true.
hooks:replay('spawned', function(emit)
for _, item in pairs(items) do emit(payloadOf(item)) end -- may Wait between items
end)
hooks:on('spawned', fn, { replay = true })The replay runs in its own thread after the register call returns, runs only that hook, respects its filter and when, and stops if the hook is removed. A hook can see an item twice if it appears during the replay, so keep replaying hooks idempotent.