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

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:

{
  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)

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:

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

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

Description
Hax's Tools � Foundry VTT hook normalization library (battle-focus slice 1)
Readme 464 KiB
Languages
JavaScript 100%