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) {
+24 -18
View File
@@ -29,28 +29,19 @@ export async function initUserActionQuotaSchema() {
/** Returns the next noon (12:00:00) UTC after the current moment. */
export function nextNoonUtc() {
return nextResetUtc(12);
return nextResetUtc(43200); // 12 hours = 43200 seconds
}
/**
* Returns the next reset timestamp UTC based on a repeating interval.
* @param {number} intervalHours - must be a divisor of 24 (1,2,3,4,6,8,12,24)
* Returns the next reset timestamp UTC based on a repeating interval relative to an epoch.
* @param {number} intervalSeconds - the interval length in seconds (>= 1)
* @param {number} epochSec - Unix seconds origin for slot calculation
*/
export function nextResetUtc(intervalHours) {
const now = new Date();
const utcHours = now.getUTCHours();
const slotsPassed = Math.floor(utcHours / intervalHours);
const nextSlotHour = (slotsPassed + 1) * intervalHours;
if (nextSlotHour < 24) {
return new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),
nextSlotHour, 0, 0, 0
));
}
return new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1,
0, 0, 0, 0
));
export function nextResetUtc(intervalSeconds, epochSec = 0) {
const nowSec = Math.floor(Date.now() / 1000);
const elapsed = Math.max(0, nowSec - epochSec);
const slot = Math.floor(elapsed / intervalSeconds);
return new Date((epochSec + (slot + 1) * intervalSeconds) * 1000);
}
// ── Queries ───────────────────────────────────────────────────────────────────
@@ -153,6 +144,21 @@ export async function resetAllUserActions(actionsRemaining, quotaResetAt) {
);
}
/**
* Resets the quota for all users belonging to a specific team.
* Users who have no row yet get one inserted.
*/
export async function resetUserActionsByTeam(team, actionsRemaining, quotaResetAt) {
await usersPool.query(
`INSERT INTO user_action_quota (user_id, actions_remaining, quota_reset_at)
SELECT id, $1, $2 FROM users WHERE team = $3
ON CONFLICT (user_id) DO UPDATE
SET actions_remaining = $1,
quota_reset_at = $2`,
[actionsRemaining, quotaResetAt, team]
);
}
/** Removes all rows from user_action_quota (used on world-seed wipeout). */
export async function truncateUserActionQuota() {
await usersPool.query(`TRUNCATE user_action_quota`);