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) {
|
||||
|
||||
+24
-18
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user