User directive: 'update the plan. v14 only'. Implementation: **scope change (Foundry v14 only):** - registered-hooks.js: renderChatInput entry drops the v13 renderChatLog name. Single subscription to the v14 name. - anti-corruption.js: combatRound arg-normalization no longer detects the v13 round-num position. v14's updateOptions.round is the only path. Removed the v13 comments from the other arg shapes (combatEnd, combatTurn). - module.json: compatibility.minimum is now 14 (was 13). verified stays 14. - package.json: version 0.3.0 -> 0.4.0 (semver-breaking: dropping v13 support is a breaking change for v13 consumers). - package.json description: 'Foundry VTT v14-only module' prefix. **test plan:** - tests/PLAN.md: v14-only scope documented at the top of the file and in Section E. Status line bumps 554 to 546 assertions (v13-only assertions dropped). Test files table re-scoped to v0.4.0. - tests/verify-hooks-lib.mjs: dropped the v13-only assertions (E.2 'renderChatLog in installed', E.3's 'both v13 and v14 produce' check, E.4's v13 round shape). Kept the v14-only assertions + added an inverse assertion: 'renderChatLog is NOT in installed raw hooks' to lock the v14-only scope. - tests/verify-hooks-lib.mjs: stub table at line ~520 drops renderChatLog (dead in production now). **doc updates:** - README.md: new 'v0.4.0 — Foundry v14 only' section explaining the change + migration note for v13 consumers. - docs/HOOK_CONTRACT.md: v0.3.0 header. §9 marks the v13 column as historical. §10 example uses the v14 shape only. **artifact:** - foundry-hooks-lib-0.4.0.zip built (82KB, 46 entries, inner version 0.4.0, inner compatibility.minimum 14). **verified:** - npm test: 546/546 assertions passed - npm run test:foundry: 30/30 assertions passed - npm run test:perf: 6/6 assertions passed (median 0.0003ms/fire) - battle-focus E2E (consumer): 125/125 still green - its-achievable smoke (consumer): 75/75 still green **consumer follow-up (separate commits in their own repos):** - battle-focus/module.json: relationships.requires[0].version bumped to ^0.4.0 - its-achievable/module.json: relationships.requires[0].minimum bumped to 0.4.0
171 lines
8.7 KiB
JavaScript
171 lines
8.7 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"] },
|
|
{ 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 }; |