Files
hooks-lib/scripts/internal/registered-hooks.js
Kaysser Kayyali d038eb8c67 v0.3.0: rename module id hax-hooks-lib -> foundry-hooks-lib
User callout: 'Hax' is Kaysser's nickname. The module id should
not use it. Rename the Foundry module id from 'hax-hooks-lib' to
'foundry-hooks-lib'. Gitea repo name stays as 'hooks-lib' (kept
for the user-facing URL); the Gitea manifest URL is unchanged.

**Scope of rename:**
- module.json: id, title, version (0.2.0 -> 0.3.0), download URL
- package.json: name
- README.md, HOOK_CONTRACT.md, LICENSE: branding text
- All 6 production JS files: MODULE_ID constant + comments
- 4 active test files: console.log strings + test descriptions
- Rename of release zips in git: hooks-lib-X.Y.Z.zip ->
  foundry-hooks-lib-X.Y.Z.zip (preserves the v0.1.0 and v0.2.0
  zips as historical artifacts; the v0.3.0 zip is the new
  release artifact)
- .gitignore: glob + un-ignore lines updated to match

**Out of scope (deliberate):**
- Gitea repo name 'kaykayyali/hooks-lib' stays. Per the user's
  direction, only the module id is renamed; the Gitea URL path
  is preserved for the existing 'url', 'manifest', 'download'
  fields.
- scripts/_archive/v0.1.0/*: historical v0.1.0 code is left
  as-is. Those files tested 'hax-hooks-lib v0.1.0'; rewriting
  the history would be misleading.
- tests/_archive_v0.1.0_*.mjs: same reason, left untouched.
- .hermes/plans/* session-historian plans that reference
  'Hax's Tools split': session artifact, not a release asset.

**Verification:** 554/554 smoke assertions pass, 6/6 perf
assertions pass, median 0.0004ms/fire (well under 0.1ms
budget). No logic change; rename is string-only.

**Consumer action required:** battle-focus and its-achievable
both declare 'relationships.requires' pointing to
'hax-hooks-lib'. The next commits on those repos will update
their relationships to 'foundry-hooks-lib' + bump their
versions. Foundry instances with v0.2.0 of the old id
installed will need to be reinstalled as v0.3.0 of the new
id.
2026-06-20 16:53:37 -04:00

171 lines
8.8 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 ---
{ envelope: "createChatMessage", mode: "async", raw: ["createChatMessage"] },
{ envelope: "renderChatMessage", mode: "sync", raw: ["renderChatMessage"] },
{ envelope: "renderChatInput", mode: "sync", raw: ["renderChatInput", "renderChatLog"] },
{ 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 };