import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const CONFIG_FILE_PATH = process.env.CONFIG_FILE_PATH ?? path.join(__dirname, "..", "config", "game.settings.json"); const EPOCH_FILE_PATH = process.env.EPOCH_FILE_PATH ?? path.join(path.dirname(CONFIG_FILE_PATH), ".timing_epoch"); let cached = { dailyActionQuota: 100, teamActionQuota: 100, actionsResetIntervalSeconds: 3600, databaseWipeoutIntervalCycles: 6, configReloadIntervalSeconds: 30, elementWorth: {}, resourceWorth: { common: {}, rare: {} }, militaryPower: {}, }; /** Raw JSON content from the last successful read (for change detection). */ let lastRawContent = ""; /** Unix seconds marking the origin of all timing slot calculations. */ let timingEpochSec = 0; /** Signature of timing-relevant values from the last successful load. */ let lastTimingSig = ""; function timingSignature() { return `${cached.actionsResetIntervalSeconds}:${cached.databaseWipeoutIntervalCycles}`; } function loadTimingEpoch() { try { const raw = fs.readFileSync(EPOCH_FILE_PATH, "utf8").trim(); const v = Number(raw); if (Number.isFinite(v) && v > 0) return v; } catch { /* ignore */ } return 0; } function saveTimingEpoch(sec) { try { fs.writeFileSync(EPOCH_FILE_PATH, String(sec), "utf8"); } catch (e) { console.error("[config] Could not persist timing epoch:", e.message); } } function parseBool(v) { if (typeof v === "boolean") return v; if (typeof v === "string") { const s = v.trim().toLowerCase(); if (s === "true") return true; if (s === "false") return false; } return null; } export function loadConfigFile() { try { // Always read the file — mtime is unreliable on Docker bind mounts (Windows). // The file is tiny and polled every ~30 s, so the cost is negligible. const raw = fs.readFileSync(CONFIG_FILE_PATH, "utf8"); if (raw === lastRawContent) { return cached; } lastRawContent = raw; const j = JSON.parse(raw); if (typeof j.dailyActionQuota === "number" && j.dailyActionQuota >= 1) { cached.dailyActionQuota = Math.floor(j.dailyActionQuota); } if (typeof j.teamActionQuota === "number" && j.teamActionQuota >= 1) { cached.teamActionQuota = Math.floor(j.teamActionQuota); } if (typeof j.actionsResetIntervalSeconds === "number" && j.actionsResetIntervalSeconds >= 1) { cached.actionsResetIntervalSeconds = Math.floor(j.actionsResetIntervalSeconds); } if (typeof j.databaseWipeoutIntervalCycles === "number" && j.databaseWipeoutIntervalCycles >= 1) { cached.databaseWipeoutIntervalCycles = Math.floor(j.databaseWipeoutIntervalCycles); } if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) { cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds; } if (j.elementWorth && typeof j.elementWorth === "object") { cached.elementWorth = j.elementWorth; } if (j.resourceWorth && typeof j.resourceWorth === "object") { cached.resourceWorth = j.resourceWorth; } if (j.militaryPower && typeof j.militaryPower === 'object') { cached.militaryPower = j.militaryPower; } // Detect timing changes and reset the epoch when they happen const newSig = timingSignature(); if (!timingEpochSec) { // First load: try to restore from disk timingEpochSec = loadTimingEpoch(); } if (!timingEpochSec || (lastTimingSig && newSig !== lastTimingSig)) { // No persisted epoch, or timing values changed → new epoch timingEpochSec = Math.floor(Date.now() / 1000); saveTimingEpoch(timingEpochSec); if (lastTimingSig) { console.log(`[config] Timing values changed (${lastTimingSig} → ${newSig}), epoch reset to ${timingEpochSec}`); } } lastTimingSig = newSig; } catch (e) { if (e.code === "ENOENT") { lastRawContent = ""; } else { console.error("[config]", e.message); } } return cached; } export function getConfig() { const c = { ...cached }; c.databaseWipeoutIntervalSeconds = c.actionsResetIntervalSeconds * c.databaseWipeoutIntervalCycles; c.timingEpochSec = timingEpochSec; return c; } export function getConfigFilePath() { return CONFIG_FILE_PATH; }