Foundry v13 deprecated renderChatMessage in favor of
renderChatMessageHTML (which passes an HTMLElement, not a jQuery
wrapper). Subscribing to the deprecated hook re-emits Foundry's
compatibility warning on every chat render in worlds still running
v13.351 (the foundry-hooks-lib module's tests run against such a
world).
v0.3.0 already narrowed scope to Foundry v14 only (HOOK_CONTRACT.md
section 9), but the registered hook set still included
renderChatMessage as a legacy fallback. There is no Foundry v14
hook by that name, so the entry was dead weight — and worse, any
v13.351 world running the v14-only library would still see the
deprecation warning every chat render.
Changes:
- registered-hooks.js: replace renderChatMessage entry with
renderChatMessageHTML. Update arg shape (HTML passes HTMLElement,
not jQuery). Add comment explaining the deprecation.
- README.md / HOOK_CONTRACT.md section 6: list renderChatMessageHTML
instead of renderChatMessage.
- tests/verify-hooks-lib.mjs: update stub arg shape from
[{id}, {}, {}] to [{id}, {}] (v14 signature).
Verification:
- node tests/verify-hooks-lib.mjs: 546/546 (unchanged)
- node tests/perf.mjs: 6/6, median 0.0003ms/fire (well under
the 0.1ms budget in HOOK_CONTRACT.md section 7)
- node --check on all scripts + tests: clean
Push: Gitea only.
Note: battle-focus's own main.js line 144 still has a
Hooks.on('renderChatMessage', ...) listener for its 'Open in
Journal' button wiring. That listener fires the deprecation warning
on the user's console. Fixing it is a battle-focus change, out of
scope for this turn (hooks-lib only).
178 lines
6.6 KiB
Markdown
178 lines
6.6 KiB
Markdown
# Foundry Hooks Lib (`foundry-hooks-lib`)
|
||
|
||
Foundry VTT module (id: `foundry-hooks-lib`) that turns Foundry's hook soup
|
||
(dnd5e, combat, token updates, canvas/UI) into a clean, normalized event
|
||
stream. **Library-only** — no UI, no settings, no chat output. Designed to
|
||
be consumed by any module that wants Foundry events in a stable shape.
|
||
|
||
Part of the **Foundry module split** (battle-focus + its-achievable + this lib).
|
||
Consumers today: `battle-focus` (encounter + journal + summary) and
|
||
`its-achievable` (achievements, rewards, wall, HUD). System-specific knowledge
|
||
(dnd5e rolls, PF2e, etc.) lives in separate adapter repos that declare
|
||
Foundry + system version ranges they support.
|
||
|
||
## v0.4.0 — Foundry v14 only
|
||
|
||
v0.3.0 renamed the module id (hax-hooks-lib → foundry-hooks-lib) and
|
||
shipped the Playwright test against a live Foundry v14. v0.4.0 narrows
|
||
the scope to **Foundry v14 only** — the v0.2.0-era anti-corruption
|
||
layer that absorbed both v13 and v14 hook renames is reduced to
|
||
v14-only normalization. Specifically:
|
||
|
||
- The `renderChatInput` registered-hooks entry no longer subscribes
|
||
to the v13 `renderChatLog` name.
|
||
- The `combatRound` arg-normalization no longer detects the v13
|
||
round-num position; v14's `updateOptions.round` is the only path.
|
||
- `compatibility.minimum` in `module.json` is now `14` (was `13`).
|
||
- The smoke test dropped 8 v13-only assertions (E.2's
|
||
`renderChatLog is in installed`, E.3's "both produce" check, E.4's
|
||
v13 round-shape). 546/546 remaining assertions pass.
|
||
|
||
`tests/PLAN.md` documents the v14-only scope at the top of the file
|
||
and in Section E.
|
||
|
||
**Migration note for v13 consumers:** upgrade to Foundry v14. The
|
||
contract still documents the v13 → v14 hook name mapping in §9 for
|
||
historical reference, but the library no longer subscribes to v13
|
||
names.
|
||
|
||
---
|
||
|
||
## v0.3.0 — module id renamed (hax-hooks-lib → foundry-hooks-lib)
|
||
|
||
v0.2.0 is a complete rewrite. v0.1.0 shipped as a curated-event catalog
|
||
(a list of hand-written handlers for 18 specific Foundry events). v0.2.0
|
||
replaces that with a **generic facade**:
|
||
|
||
- Subscribes to every relevant Foundry hook (combat lifecycle, all
|
||
document CRUD, canvas/UI, dnd5e v2 roll hooks, etc.).
|
||
- Emits a uniform envelope — `{ts, hook, args}` — with no domain
|
||
interpretation.
|
||
- Consumers write queries against the envelope. "When bob takes damage"
|
||
is a consumer-side query, not a hook name the library knows about.
|
||
- System-specific derived events (e.g. "dnd5e attack roll") live in
|
||
separate adapter repos. Adapters register a manifest with Foundry +
|
||
system version ranges; the library evaluates at ready and loads
|
||
matching adapters.
|
||
|
||
**Why this shape:** Foundry's hook names and arities change between
|
||
versions. The library absorbs that churn (§9 + §10 of the contract) so
|
||
consumers don't have to rewrite every Foundry upgrade.
|
||
|
||
## Status
|
||
|
||
v0.2.0 — generic facade per `docs/HOOK_CONTRACT.md`. Smoke test: 554/554
|
||
assertions. Perf test: median 0.0003ms/fire (333× under the 0.1ms budget).
|
||
|
||
## Envelope shape
|
||
|
||
Every fire produces exactly one envelope:
|
||
|
||
```js
|
||
{
|
||
ts: 1719000000000, // epoch ms when Foundry fired
|
||
hook: "updateActor", // the Foundry hook name (normalized; see §9)
|
||
args: [doc, change, options, userId] // positional args, verbatim
|
||
}
|
||
```
|
||
|
||
That's it. No `kind`, no normalized fields, no consumer metadata.
|
||
Consumers that want `{kind, actorId, delta}` build that themselves from
|
||
`args`. This is intentional: the library is the boundary that absorbs
|
||
Foundry version churn.
|
||
|
||
## Public API (on `game.modules.get("foundry-hooks-lib").api`)
|
||
|
||
```js
|
||
import { subscribe, subscribeMany, subscribeAll } from
|
||
game.modules.get("foundry-hooks-lib").api;
|
||
|
||
// Single hook:
|
||
const unsub = subscribe("updateActor", (envelope) => {
|
||
const [actor, change] = envelope.args;
|
||
// ...
|
||
});
|
||
|
||
// Batch subscribe (atomic):
|
||
subscribeMany({
|
||
updateActor: handleActorUpdate,
|
||
createToken: handleTokenCreate,
|
||
});
|
||
|
||
// Every hook (for audit logs):
|
||
subscribeAll((envelope) => log(envelope));
|
||
|
||
// One-shot cleanup:
|
||
unsub(); // or unsubscribeAll() to purge everything
|
||
```
|
||
|
||
### System adapters
|
||
|
||
A system adapter is a separate Foundry module. At its `init`, it calls:
|
||
|
||
```js
|
||
hooksLib.api.registerSystemAdapter({
|
||
id: "foundry-hooks-dnd5e",
|
||
moduleId: "foundry-hooks-dnd5e",
|
||
system: { id: "dnd5e", versions: ">=5.2.0 <5.3.0" },
|
||
foundryVersions: ">=13 <15",
|
||
factory: () => [ /* derived-event registrations */ ],
|
||
});
|
||
```
|
||
|
||
The library evaluates the manifest against `game.system.id` + version
|
||
and `game.version` at `ready`. Matching adapter factories are called
|
||
once. Non-matching adapters log a warning naming the version mismatch
|
||
(or silently skip for non-matching system).
|
||
|
||
## Subscribed hook set (v0.2.0)
|
||
|
||
Lifecycle, document CRUD (Actor/Token/Item/Scene/JournalEntry/
|
||
ActiveEffect/Combat/Combatant), combat lifecycle, chat & rolls (incl.
|
||
dnd5e v2 roll hooks), canvas/scene/UI (canvasInit/Ready/Pan, controlToken,
|
||
hoverToken, targetToken, lighting/sightRefresh, collapseSidebar,
|
||
changeSidebarTab, getSceneControlButtons, renderChatMessageHTML,
|
||
renderChatInput, renderJournalPageSheet, rtcSettingsChanged), and more.
|
||
Full list: `scripts/internal/registered-hooks.js`.
|
||
|
||
## Error containment
|
||
|
||
If a consumer callback throws, the library catches it, logs via
|
||
`console.error` with the `[foundry-hooks-lib]` prefix and the hook name,
|
||
and continues dispatching to subsequent callbacks. Errors never
|
||
propagate to Foundry's hook chain.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
npm test # 554 assertions in <2s, no Foundry needed (smoke)
|
||
npm run test:foundry # 30 assertions in <15s, Playwright + live Foundry
|
||
npm run test:perf # median 0.0003ms/fire, heap delta check
|
||
npm run test:all # all three
|
||
```
|
||
|
||
See `tests/PLAN.md` for what we test and what we don't. The
|
||
`test:foundry` runner connects to `FOUNDRY_URL` (defaults to
|
||
`http://localhost:30000`), signs in as Gamemaster, waits for the
|
||
library's `mod.api.isReady() === true`, and exercises the live
|
||
envelope dispatch chain end-to-end.
|
||
|
||
## Architecture notes
|
||
|
||
- **One envelope shape, one dispatcher.** Adding a new Foundry hook is
|
||
one entry in `scripts/internal/registered-hooks.js`. No new file.
|
||
- **System adapters are repos, not files.** Each system's derived
|
||
knowledge is its own module with its own version cadence.
|
||
- **Anti-corruption (§9-§10):** library subscribes to BOTH v13 and v14
|
||
hook names where applicable; consumers see stable envelope names.
|
||
- **Async dispatch (microtask) by default.** pre-* + combat* +
|
||
applyActiveEffect + get*Context dispatch sync because their return
|
||
values matter.
|
||
|
||
## Dependencies
|
||
|
||
None. Library-only; system adapters depend on this, not the other way
|
||
around.
|
||
|
||
## Maintained by Kaysser Taylor + Hermes
|