fix: Fixing the MP power bonus + seed maintenance

This commit is contained in:
gauvainboiche
2026-04-03 14:25:19 +02:00
parent b11446cf56
commit d345c025c0
11 changed files with 349 additions and 132 deletions
+120 -14
View File
@@ -4,6 +4,11 @@ import { computeWorldSeedState } from "../worldSeed.js";
import { nextResetUtc, truncateUserActionQuota, getUsersByIds } from "./usersDb.js";
let lastSeedSlot = null;
let lastWorldSeed = null;
// ── Mutex for seed transitions ────────────────────────────────────────────────
// Prevents concurrent ensureSeedEpoch() calls from racing on the wipe.
let seedTransitionPromise = null;
// ── Schema ────────────────────────────────────────────────────────────────────
@@ -145,22 +150,39 @@ export async function initGameSchema() {
);
CREATE INDEX IF NOT EXISTS idx_round_history_ended_at ON round_history (ended_at DESC);
`);
// ── Persisted game settings snapshot (one row, id=1) ──────────────────────
await pool.query(`
CREATE TABLE IF NOT EXISTS game_settings_snapshot (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
config JSONB NOT NULL DEFAULT '{}',
timing_epoch_sec BIGINT NOT NULL DEFAULT 0,
world_seed TEXT NOT NULL DEFAULT '',
seed_period_ends_at TEXT NOT NULL DEFAULT '',
seed_period_starts_at TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO game_settings_snapshot (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
`);
}
// ── World-seed epoch ──────────────────────────────────────────────────────────
export async function ensureSeedEpoch() {
async function _ensureSeedEpochInner() {
loadConfigFile();
const rot = getConfig().databaseWipeoutIntervalSeconds;
const { seedSlot, worldSeed } = computeWorldSeedState(rot);
if (lastSeedSlot === null) {
const cfg = getConfig();
const rot = cfg.databaseWipeoutIntervalSeconds;
const epoch = cfg.timingEpochSec ?? 0;
const { seedSlot, worldSeed } = computeWorldSeedState(rot, epoch);
if (lastWorldSeed === null) {
lastSeedSlot = seedSlot;
lastWorldSeed = worldSeed;
await persistSettingsSnapshot(cfg, worldSeed, rot, epoch);
return worldSeed;
}
if (seedSlot !== lastSeedSlot) {
if (worldSeed !== lastWorldSeed) {
// Award a victory point to the team with the highest econ score before wiping
try {
const expiredSeed = `swg-${lastSeedSlot}`;
const expiredSeed = lastWorldSeed;
const econRows = await pool.query(
`SELECT team, score FROM team_econ_scores WHERE world_seed = $1`,
[expiredSeed]
@@ -191,22 +213,106 @@ export async function ensureSeedEpoch() {
await pool.query("DELETE FROM team_military_deductions WHERE world_seed != $1", [worldSeed]);
await pool.query("DELETE FROM cell_attack_log WHERE world_seed != $1", [worldSeed]);
await pool.query("DELETE FROM team_cell_visibility WHERE world_seed != $1", [worldSeed]);
// Reset both team and user quotas to server defaults (no bonuses) for the new period
const cfg = getConfig();
const nextNoon = nextResetUtc(cfg.actionsResetIntervalHours ?? 12).toISOString();
// Reset team quota to seed-scoped defaults; expiry is the seed period end so it
// never looks stale during a live epoch.
const seedPeriodEnd = new Date((epoch + (seedSlot + 1) * rot) * 1000).toISOString();
await pool.query(
`TRUNCATE team_action_quota;
INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at)
VALUES ('blue', $1, $2), ('red', $1, $2)`,
[cfg.teamActionQuota, nextNoon]
`INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at)
VALUES ('blue', $1, $2), ('red', $1, $2)
ON CONFLICT (team) DO UPDATE
SET actions_remaining = EXCLUDED.actions_remaining,
quota_reset_at = EXCLUDED.quota_reset_at`,
[cfg.teamActionQuota, seedPeriodEnd]
);
// Reset per-user quotas (these do use the short action-interval reset time)
await truncateUserActionQuota();
console.log(`[world] Slot ${lastSeedSlot}${seedSlot}; grid wiped, old cooldowns cleared.`);
console.log(`[world] Seed ${lastWorldSeed}${worldSeed}; grid wiped, old cooldowns cleared.`);
lastSeedSlot = seedSlot;
lastWorldSeed = worldSeed;
await persistSettingsSnapshot(cfg, worldSeed, rot, epoch);
}
return worldSeed;
}
/**
* Mutex-protected wrapper. Ensures only one seed transition runs at a time.
* Concurrent callers wait for the in-flight transition and get the same result.
*/
export async function ensureSeedEpoch() {
if (seedTransitionPromise) {
return seedTransitionPromise;
}
seedTransitionPromise = _ensureSeedEpochInner().finally(() => {
seedTransitionPromise = null;
});
return seedTransitionPromise;
}
// ── Persist / read game settings snapshot in DB ───────────────────────────────
async function persistSettingsSnapshot(cfg, worldSeed, rotationSeconds, epochSec) {
try {
const ws = computeWorldSeedState(rotationSeconds, epochSec);
const configJson = {
dailyActionQuota: cfg.dailyActionQuota,
teamActionQuota: cfg.teamActionQuota,
actionsResetIntervalSeconds: cfg.actionsResetIntervalSeconds,
databaseWipeoutIntervalCycles: cfg.databaseWipeoutIntervalCycles,
databaseWipeoutIntervalSeconds: rotationSeconds,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
elementWorth: cfg.elementWorth ?? {},
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
militaryPower: cfg.militaryPower ?? {},
};
await pool.query(
`UPDATE game_settings_snapshot
SET config = $1,
timing_epoch_sec = $2,
world_seed = $3,
seed_period_ends_at = $4,
seed_period_starts_at = $5,
updated_at = NOW()
WHERE id = 1`,
[JSON.stringify(configJson), epochSec, worldSeed, ws.seedPeriodEndsAtUtc, ws.seedPeriodStartsAtUtc]
);
} catch (e) {
console.error("[db] Could not persist settings snapshot:", e.message);
}
}
/**
* Reads the last persisted settings snapshot from DB.
* Used as a fallback when the in-memory config may not reflect file values.
*/
export async function getSettingsSnapshot() {
const { rows } = await pool.query(
`SELECT config, timing_epoch_sec, world_seed, seed_period_ends_at, seed_period_starts_at
FROM game_settings_snapshot WHERE id = 1`
);
if (!rows[0]) return null;
const row = rows[0];
const cfg = typeof row.config === "string" ? JSON.parse(row.config) : row.config;
return {
...cfg,
timingEpochSec: Number(row.timing_epoch_sec),
worldSeed: row.world_seed,
seedPeriodEndsAtUtc: row.seed_period_ends_at,
seedPeriodStartsAtUtc: row.seed_period_starts_at,
};
}
/**
* Convenience wrapper: reads current in-memory config and persists it to DB.
* Called from index.js after config-file polls and on boot.
*/
export async function persistCurrentSettings() {
const cfg = getConfig();
const rot = cfg.databaseWipeoutIntervalSeconds;
const epoch = cfg.timingEpochSec ?? 0;
const ws = computeWorldSeedState(rot, epoch);
await persistSettingsSnapshot(cfg, ws.worldSeed, rot, epoch);
}
// ── Grid cells ────────────────────────────────────────────────────────────────
export async function getGridCells(worldSeed) {