109 lines
3.4 KiB
JavaScript
109 lines
3.4 KiB
JavaScript
/**
|
|
* 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, pushModuleInventory, pushSnapshot, testConnection } from "./api.js";
|
|
import { endSession, pushAllSnapshots } from "./macros.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, "snapshotDebounceSeconds") * 1000;
|
|
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, pushModuleInventory, testConnection, endSession, pushAllSnapshots };
|
|
mod.api = api;
|
|
globalThis.seitimeBridge = api;
|
|
|
|
console.log(`[${MODULE_ID}] ready. Call seitimeBridge.testConnection() to verify config, or seitimeBridge.endSession() from a macro.`);
|
|
|
|
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);
|
|
});
|