/** * Entry point for seitime-bridge. Registers settings, wires hooks, and * exposes the public API on `globalThis.seitimeBridge` for use from * Foundry macros and the F12 console. * * All push operations are gated to the primary connected GM * (`game.users.activeGM`) so that worlds with multiple GMs don't * duplicate webhook calls. */ import { MODULE_ID } from "./constants.js"; import { registerSettings } from "./settings.js"; import { pushManifest, pushSnapshot, testConnection } from "./api.js"; let manifestTimerId = null; const snapshotDebouncers = new Map(); function isPrimaryGM() { return game.user.isGM && game.users.activeGM?.id === game.user.id; } function safePushManifest(reason) { if (!isPrimaryGM()) return; pushManifest().catch((err) => { console.warn(`[${MODULE_ID}] manifest push (${reason}) failed:`, err); }); } /** * Schedule a debounced snapshot push for an actor. Each actor has its own * debouncer so that rapid edits to one PC don't delay pushes for others. * * Gated to the primary GM regardless of who made the change — Foundry * broadcasts updates to all clients, so the GM is always in a position * to push and we avoid duplicate pushes from co-GMs that way. */ function scheduleSnapshotPush(actor) { if (!isPrimaryGM()) return; if (!game.settings.get(MODULE_ID, "snapshotAutoSync")) return; let debouncer = snapshotDebouncers.get(actor.id); if (!debouncer) { const debounceMs = game.settings.get(MODULE_ID, "snapshotDebounceMs"); debouncer = foundry.utils.debounce((a) => { pushSnapshot(a).catch((err) => { console.warn(`[${MODULE_ID}] snapshot push for ${a.name} failed:`, err); }); }, debounceMs); snapshotDebouncers.set(actor.id, debouncer); } debouncer(actor); } function actorFromItem(item) { return item.parent?.documentName === "Actor" ? item.parent : null; } Hooks.once("init", () => { registerSettings(); }); Hooks.once("ready", () => { const mod = game.modules.get(MODULE_ID); const api = { pushManifest, pushSnapshot, testConnection }; mod.api = api; globalThis.seitimeBridge = api; console.log(`[${MODULE_ID}] ready. Call seitimeBridge.testConnection() to verify config.`); safePushManifest("ready"); const intervalMin = game.settings.get(MODULE_ID, "manifestSyncIntervalMinutes"); if (intervalMin > 0) { manifestTimerId = setInterval(() => safePushManifest("periodic"), intervalMin * 60 * 1000); } }); Hooks.on("createActor", (actor) => { if (actor.type !== "character") return; safePushManifest("createActor"); }); Hooks.on("deleteActor", (actor) => { if (actor.type !== "character") return; safePushManifest("deleteActor"); }); Hooks.on("updateActor", (actor) => { if (actor.type !== "character") return; scheduleSnapshotPush(actor); }); // Item edits don't fire updateActor in v12, so listen to the embedded // item hooks directly for inventory/feature/class changes. Hooks.on("createItem", (item) => { const actor = actorFromItem(item); if (actor?.type === "character") scheduleSnapshotPush(actor); }); Hooks.on("updateItem", (item) => { const actor = actorFromItem(item); if (actor?.type === "character") scheduleSnapshotPush(actor); }); Hooks.on("deleteItem", (item) => { const actor = actorFromItem(item); if (actor?.type === "character") scheduleSnapshotPush(actor); });