Files
hooks-lib/scripts/internal/registered-hooks.js
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
9.2 KiB
JavaScript

// scripts/internal/registered-hooks.js
//
// The registered hook set (HOOK_CONTRACT.md §6) and the dispatch-mode
// table (HOOK_CONTRACT.md §8). Each entry declares the Foundry hook
// name, its normalized envelope `hook` value (after anti-corruption
// mapping §9), and whether consumer callbacks dispatch synchronously
// or asynchronously.
//
// Async dispatch (microtask, default): consumer callbacks do not block
// Foundry's hook chain. Used for events where consumers only observe,
// not cancel.
//
// Sync dispatch: consumer callbacks fire inline with Foundry's hook.
// Used for hooks where the consumer's return value matters
// (pre-* cancellation) or where the consumer needs to mutate state
// before the next event in the same tick (combat*, applyActiveEffect,
// get*Context).
//
// The hook name in REGISTRY is the *normalized* envelope name. The
// ANTI_CORRUPTION map below (per §9) lists the raw Foundry names
// that produce each normalized envelope.
const MODULE_ID = "foundry-hooks-lib";
// mode: "sync" | "async"
export const HOOK_REGISTRY = [
// --- Lifecycle (always-on, sync — observable but not cancellable) ---
{ envelope: "init", mode: "sync", raw: ["init"] },
{ envelope: "setup", mode: "sync", raw: ["setup"] },
{ envelope: "ready", mode: "sync", raw: ["ready"] },
{ envelope: "pauseGame", mode: "async", raw: ["pauseGame"] },
// --- Document CRUD (async — observe only) ---
{ envelope: "createActor", mode: "async", raw: ["createActor"] },
{ envelope: "updateActor", mode: "async", raw: ["updateActor"] },
{ envelope: "deleteActor", mode: "async", raw: ["deleteActor"] },
{ envelope: "preCreateActor", mode: "sync", raw: ["preCreateActor"] },
{ envelope: "preUpdateActor", mode: "sync", raw: ["preUpdateActor"] },
{ envelope: "preDeleteActor", mode: "sync", raw: ["preDeleteActor"] },
{ envelope: "createToken", mode: "async", raw: ["createToken"] },
{ envelope: "updateToken", mode: "async", raw: ["updateToken"] },
{ envelope: "deleteToken", mode: "async", raw: ["deleteToken"] },
{ envelope: "preCreateToken", mode: "sync", raw: ["preCreateToken"] },
{ envelope: "preUpdateToken", mode: "sync", raw: ["preUpdateToken"] },
{ envelope: "preDeleteToken", mode: "sync", raw: ["preDeleteToken"] },
{ envelope: "createItem", mode: "async", raw: ["createItem"] },
{ envelope: "updateItem", mode: "async", raw: ["updateItem"] },
{ envelope: "deleteItem", mode: "async", raw: ["deleteItem"] },
{ envelope: "preCreateItem", mode: "sync", raw: ["preCreateItem"] },
{ envelope: "preUpdateItem", mode: "sync", raw: ["preUpdateItem"] },
{ envelope: "preDeleteItem", mode: "sync", raw: ["preDeleteItem"] },
{ envelope: "createScene", mode: "async", raw: ["createScene"] },
{ envelope: "updateScene", mode: "async", raw: ["updateScene"] },
{ envelope: "deleteScene", mode: "async", raw: ["deleteScene"] },
{ envelope: "createJournalEntry", mode: "async", raw: ["createJournalEntry"] },
{ envelope: "updateJournalEntry", mode: "async", raw: ["updateJournalEntry"] },
{ envelope: "deleteJournalEntry", mode: "async", raw: ["deleteJournalEntry"] },
{ envelope: "createActiveEffect", mode: "async", raw: ["createActiveEffect"] },
{ envelope: "updateActiveEffect", mode: "async", raw: ["updateActiveEffect"] },
{ envelope: "deleteActiveEffect", mode: "async", raw: ["deleteActiveEffect"] },
{ envelope: "preCreateActiveEffect", mode: "sync", raw: ["preCreateActiveEffect"] },
{ envelope: "preUpdateActiveEffect", mode: "sync", raw: ["preUpdateActiveEffect"] },
{ envelope: "preDeleteActiveEffect", mode: "sync", raw: ["preDeleteActiveEffect"] },
{ envelope: "createCombat", mode: "async", raw: ["createCombat"] },
{ envelope: "updateCombat", mode: "async", raw: ["updateCombat"] },
{ envelope: "deleteCombat", mode: "async", raw: ["deleteCombat"] },
{ envelope: "preCreateCombat", mode: "sync", raw: ["preCreateCombat"] },
{ envelope: "preUpdateCombat", mode: "sync", raw: ["preUpdateCombat"] },
{ envelope: "preDeleteCombat", mode: "sync", raw: ["preDeleteCombat"] },
{ envelope: "createCombatant", mode: "async", raw: ["createCombatant"] },
{ envelope: "updateCombatant", mode: "async", raw: ["updateCombatant"] },
{ envelope: "deleteCombatant", mode: "async", raw: ["deleteCombatant"] },
{ envelope: "preCreateCombatant", mode: "sync", raw: ["preCreateCombatant"] },
{ envelope: "preUpdateCombatant", mode: "sync", raw: ["preUpdateCombatant"] },
{ envelope: "preDeleteCombatant", mode: "sync", raw: ["preDeleteCombatant"] },
// --- Combat lifecycle (sync — consumers may need to mutate before next event) ---
{ envelope: "combatStart", mode: "sync", raw: ["combatStart"] },
{ envelope: "combatEnd", mode: "sync", raw: ["combatEnd"] },
{ envelope: "combatTurn", mode: "sync", raw: ["combatTurn"] },
{ envelope: "combatRound", mode: "sync", raw: ["combatRound"] },
// combatInactive is a synthetic event synthesized from updateCombat
// when active flips true→false (§9 anti-corruption). The raw hook
// it watches is updateCombat.
{ envelope: "combatInactive", mode: "sync", raw: ["updateCombat"], synthesized: true },
// --- Chat & rolls ---
// Note: renderChatMessage (the v13 jQuery-wrapper hook) is
// intentionally NOT registered. Foundry v13 deprecated it in favor
// of renderChatMessageHTML (HTMLElement). Consumers wanting chat
// message rendering events should subscribe to renderChatMessageHTML
// (or renderJournalPageSheet for journal pages). Subscribing to the
// deprecated hook here would re-emit Foundry's compatibility warning
// on every chat render in worlds still running v13.351.
{ envelope: "createChatMessage", mode: "async", raw: ["createChatMessage"] },
{ envelope: "renderChatMessageHTML", mode: "sync", raw: ["renderChatMessageHTML"] },
{ envelope: "renderChatInput", mode: "sync", raw: ["renderChatInput"] },
{ envelope: "dnd5e.rollAttackV2", mode: "async", raw: ["dnd5e.rollAttackV2"] },
{ envelope: "dnd5e.rollDamageV2", mode: "async", raw: ["dnd5e.rollDamageV2"] },
// --- Canvas / scene / UI ---
{ envelope: "canvasInit", mode: "async", raw: ["canvasInit"] },
{ envelope: "canvasReady", mode: "sync", raw: ["canvasReady"] },
{ envelope: "canvasPan", mode: "async", raw: ["canvasPan"] },
{ envelope: "controlToken", mode: "sync", raw: ["controlToken"] },
{ envelope: "hoverToken", mode: "sync", raw: ["hoverToken"] },
{ envelope: "targetToken", mode: "sync", raw: ["targetToken"] },
{ envelope: "lightingRefresh", mode: "async", raw: ["lightingRefresh"] },
{ envelope: "sightRefresh", mode: "async", raw: ["sightRefresh"] },
{ envelope: "collapseSidebar", mode: "sync", raw: ["collapseSidebar"] },
{ envelope: "changeSidebarTab", mode: "sync", raw: ["changeSidebarTab"] },
{ envelope: "getSceneControlButtons", mode: "sync", raw: ["getSceneControlButtons"] },
{ envelope: "collapseSceneNavigation", mode: "sync", raw: ["collapseSceneNavigation"] },
{ envelope: "renderJournalPageSheet", mode: "sync", raw: ["renderJournalPageSheet"] },
{ envelope: "initializePointSourceShaders", mode: "sync", raw: ["initializePointSourceShaders"] },
{ envelope: "rtcSettingsChanged", mode: "async", raw: ["rtcSettingsChanged"] },
];
// Lookup table: raw Foundry hook name -> registry entry.
// Multiple raw names can map to the same envelope (anti-corruption §9).
// When the same raw name is associated with both a regular entry and a
// synthesized entry (e.g. updateCombat → updateCombat AND updateCombat
// → combatInactive via synthesis), the regular entry wins; the
// synthesized entry is only consulted for synthesizing envelopes.
const RAW_TO_ENTRY = new Map();
const SYNTHESIZING_RAW_NAMES = new Set();
for (const entry of HOOK_REGISTRY) {
if (entry.synthesized) {
for (const rawName of entry.raw) {
SYNTHESIZING_RAW_NAMES.add(rawName);
}
continue;
}
for (const rawName of entry.raw) {
RAW_TO_ENTRY.set(rawName, entry);
}
}
export function getEntryForRawName(rawName) {
return RAW_TO_ENTRY.get(rawName) ?? null;
}
export function getEntryForEnvelope(envelopeName) {
return HOOK_REGISTRY.find((e) => e.envelope === envelopeName) ?? null;
}
export function isSynthesizingRawName(rawName) {
return SYNTHESIZING_RAW_NAMES.has(rawName);
}
// All raw Foundry names we register a Hooks.on for. The init hook
// uses this to register every listener exactly once.
export function allRawHookNames() {
const set = new Set();
for (const entry of HOOK_REGISTRY) {
for (const raw of entry.raw) set.add(raw);
}
return [...set];
}
// Names consumers can subscribe to (the normalized envelope names).
export const REGISTERED_HOOKS = HOOK_REGISTRY.map((e) => e.envelope);
// The set of synthesized envelopes (anti-corruption). combatInactive
// is the only one in v0.2.0; future syntheses add to this set.
export const SYNTHESIZED_ENVELOPES = HOOK_REGISTRY
.filter((e) => e.synthesized)
.map((e) => e.envelope);
export { MODULE_ID };