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.
171 lines
8.8 KiB
JavaScript
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 }; |