# 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