// ── Chat titles, read against the live standings (phase 17) ──────────────── // // `titles.js` decides; this file fetches what it decides from. One answer per // server serves three readers — the push to the game, the web leaderboard and // the app's — so it is remembered briefly: the leaderboard is polled by every // open page, and each rule is a leaderboard query of its own. const serversDb = require('../servers/servers.db') const db = require('./titles.db') const lists = require('./lists') const titles = require('./titles') /** How long one server's answer is reused. The same as the push loop's tick. */ const MEMO_MS = 30 * 1000 const memo = new Map() /** A server's settings as the admin form reads them. */ async function settingsFor(serverId) { const [mode, rules] = await Promise.all([db.getMode(serverId), db.listRules(serverId)]) return shapeSettings(mode, rules) } function shapeSettings(mode, rules) { return { mode: titles.normaliseMode(mode && mode.mode), max: mode && Number(mode.max) ? Number(mode.max) : 2, rules: rules.map((r) => ({ stat: r.stat, topN: Number(r.topN), text: r.text, color: r.color })), } } /** Every server's settings, keyed by id, for the admin server list. */ async function settingsByServer() { const [modes, rules] = await Promise.all([db.listModes(), db.listAllRules()]) const out = new Map(modes.map((m) => [m.serverId, shapeSettings(m, [])])) for (const r of rules) { const s = out.get(r.serverId) if (s) s.rules.push({ stat: r.stat, topN: Number(r.topN), text: r.text, color: r.color }) } return out } async function saveSettings(serverId, value) { await db.saveSettings(serverId, value) forget(serverId) } /** Forgets one server's answer, or every server's when a category was renamed. */ function forget(serverId = null) { for (const key of memo.keys()) if (serverId === null || key.startsWith(`${serverId} `)) memo.delete(key) } /** * The categories as the admin screen shows them (D175): each one's label, the * shipped default, the admin's own title if any, and what rules show now. */ async function categories() { const own = new Map((await db.listCategories()).map((r) => [r.stat, r])) return titles.CATEGORY_IDS.map((stat) => { const row = own.get(stat) const def = titles.STATS[stat] return { stat, label: def.label, default: def.title, text: row ? row.text : null, current: row ? row.text : def.title, updatedAt: row ? row.updatedAt : null, } }) } /** * Sets or resets one category's title. An empty text is the Reset. Every * server's answer is forgotten, because every server's rules may use it, and * the next push carries the new name (§4.6: "from the next push"). */ async function setCategory(stat, text, userId = null) { if (text) await db.setCategory(stat, text, userId) else await db.clearCategory(stat) forget() } /** * Who holds which title on one server right now: a Map of Steam id to * `[{ text, color }]`, after the server's mode. Empty with no rules, and empty * with no current wipe — a title ranks the current wipe (D135), and a server that * has never said which wipe it is on has none to rank. */ async function heldFor(serverId, { wipeId, now = Date.now() } = {}) { if (!wipeId) return new Map() const key = `${serverId} ${wipeId}` const hit = memo.get(key) if (hit && now - hit.at < MEMO_MS) return hit.held const [settings, overrides] = await Promise.all([settingsFor(serverId), db.categoryTitles()]) const rules = titles.resolveRules(settings.rules, overrides) const standings = {} // One query per STAT, at the deepest top N any rule on it asks for, rather // than one per rule: two rules on kills read the same rows. const depth = new Map() for (const r of rules) if (titles.STATS[r.stat]) depth.set(r.stat, Math.max(depth.get(r.stat) || 0, r.topN)) await Promise.all( [...depth.entries()].map(async ([stat, limit]) => { const rows = await db.standings({ serverId, wipeId, source: titles.STATS[stat].source, lists, limit }) // SUM() and MAX() come back as strings; the rules compare numbers. standings[stat] = rows.map((r) => ({ steamId: r.steamId, value: Number(r.value) || 0 })) }), ) const held = titles.evaluate(rules, standings, settings) memo.set(key, { at: now, held }) return held } /** The same, for a server by id, reading its current wipe from its last report. */ async function currentFor(serverId) { const state = await serversDb.getState(serverId) return heldFor(serverId, { wipeId: state && state.wipeId ? state.wipeId : null }) } module.exports = { MEMO_MS, settingsFor, settingsByServer, saveSettings, heldFor, currentFor, forget, categories, setCategory, }