Files
hooks-lib/README.md
Kaysser Kayyali 2fabb5e98f v0.4.1: drop renderChatMessage, register renderChatMessageHTML
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).
2026-06-20 22:49:32 -04:00

178 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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