fix: Fixing the MP power bonus + seed maintenance
This commit is contained in:
+120
-14
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user