const { query } = require('../../utils/db') // ── Online players ───────────────────────────────────────────────────────── const ONLINE_COLS = 'serial, name, acct, web_id, map, x, y, z, hits, hits_max, mana, mana_max, stam, stam_max, str, dex, `int`, updated_at' // Upsert one online player. `fields` already prepared by the model (only the // columns it wants to write); serial is required and is the primary key. async function upsertOnline(serial, fields) { const cols = Object.keys(fields) const allCols = ['serial', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') // Never overwrite an existing column with NULL on refresh (a char.vitals frame // that omits acct/name shouldn't blank what mob.login set) — COALESCE keeps the // prior value when the incoming one is NULL. const updates = cols.map((c) => `\`${c}\` = COALESCE(VALUES(\`${c}\`), \`${c}\`)`).join(', ') await query( `INSERT INTO shard_online (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [serial, ...cols.map((c) => fields[c])], ) } const removeOnline = (serial) => query('DELETE FROM shard_online WHERE serial = ?', [serial]) const clearOnline = () => query('DELETE FROM shard_online') async function countOnline() { const rows = await query('SELECT COUNT(*) AS n FROM shard_online') return rows[0] ? Number(rows[0].n) : 0 } const listOnline = () => query(`SELECT ${ONLINE_COLS} FROM shard_online ORDER BY name ASC`) // Online players on any of the given game accounts (admin: a user's linked // accounts). Empty list short-circuits so we never emit `IN ()`. const listOnlineByAccounts = (accounts) => accounts.length === 0 ? Promise.resolve([]) : query( `SELECT ${ONLINE_COLS} FROM shard_online WHERE acct IN (${accounts.map(() => '?').join(', ')}) ORDER BY name ASC`, accounts, ) // Staff roles whose online presence is shown on the public Shard page. Players // who link an account are NOT surfaced publicly — only staff opt into visibility // by virtue of being staff. const PUBLIC_ONLINE_ROLES = ['admin', 'editor', 'moderator'] // Online players whose game account is linked to a STAFF website user. Joined // against shard_account_links (not the sidecar-supplied web_id) so a link takes // effect immediately, regardless of whether the player has re-logged since // linking, then through to users so only staff roles are surfaced publicly. const listOnlineLinked = () => query( `SELECT ${ONLINE_COLS.split(', ').map((c) => `o.${c}`).join(', ')} FROM shard_online o JOIN shard_account_links l ON l.account = o.acct JOIN users u ON u.id = l.user_id WHERE u.role IN (${PUBLIC_ONLINE_ROLES.map(() => '?').join(', ')}) ORDER BY o.name ASC`, PUBLIC_ONLINE_ROLES, ) // ── Economy supply series ──────────────────────────────────────────────── const insertEconomy = ({ accounts, gold, t }) => query('INSERT INTO shard_economy (accounts, gold, t) VALUES (?, ?, ?)', [ accounts ?? null, gold ?? null, t, ]) const listEconomy = (limit) => query('SELECT accounts, gold, t FROM shard_economy ORDER BY t DESC LIMIT ?', [limit]) async function latestEconomy() { const rows = await query('SELECT accounts, gold, t FROM shard_economy ORDER BY t DESC LIMIT 1') return rows[0] || null } // ── Houses / IDOC ──────────────────────────────────────────────────────── const HOUSE_COLS = 'serial, stage, map, x, y, z, region, name, owner_serial, owner_acct, built_on, last_refreshed, is_idoc, updated_at' async function upsertHouse(serial, fields) { const cols = Object.keys(fields) const allCols = ['serial', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') await query( `INSERT INTO shard_houses (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [serial, ...cols.map((c) => fields[c])], ) } const listIdocHouses = () => query(`SELECT ${HOUSE_COLS} FROM shard_houses WHERE is_idoc = 1 ORDER BY updated_at DESC`) // Houses owned by any of the given game accounts (admin: a user's linked // accounts). IDOC houses first, then newest-refreshed. Empty list short-circuits. const listHousesByAccounts = (accounts) => accounts.length === 0 ? Promise.resolve([]) : query( `SELECT ${HOUSE_REG_COLS} FROM shard_houses WHERE owner_acct IN (${accounts.map(() => '?').join(', ')}) ORDER BY is_idoc DESC, updated_at DESC`, accounts, ) // ── House registry (Protocol 2.0 house.update / house.remove) ────────────── // The registry columns extend HOUSE_COLS; a registry row is one we've seen via // house.update (in_registry = 1), as opposed to a decay-only transition row. const HOUSE_REG_COLS = `${HOUSE_COLS}, owner_name, co_owners, friends, price, decay, in_registry` const removeHouse = (serial) => query('DELETE FROM shard_houses WHERE serial = ?', [serial]) // The full registered-house browser: every row we've seen via house.update. const listRegistryHouses = () => query(`SELECT ${HOUSE_REG_COLS} FROM shard_houses WHERE in_registry = 1 ORDER BY name ASC`) // ── Champion spawns ──────────────────────────────────────────────────────── const CHAMP_COLS = 'serial, category, type, name, status, active, map, x, y, z, boss_up, payload, t, updated_at' async function upsertChamp(serial, fields) { const cols = Object.keys(fields) const allCols = ['serial', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') await query( `INSERT INTO shard_champs (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [serial, ...cols.map((c) => fields[c])], ) } const removeChamp = (serial) => query('DELETE FROM shard_champs WHERE serial = ?', [serial]) const clearChamps = () => query('DELETE FROM shard_champs') // Ordered by name (matches the sidecar's /champs ordering). const listChamps = () => query(`SELECT ${CHAMP_COLS} FROM shard_champs ORDER BY name ASC`) // ── Help-page (support) queue ────────────────────────────────────────────── const PAGE_COLS = 'page_id, type, sender_name, sender_acct, web_id, message, map, x, y, z, sent_ms, handled, handler, payload, updated_at' async function upsertPage(pageId, fields) { const cols = Object.keys(fields) const allCols = ['page_id', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') await query( `INSERT INTO shard_pages (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [pageId, ...cols.map((c) => fields[c])], ) } const removePage = (pageId) => query('DELETE FROM shard_pages WHERE page_id = ?', [pageId]) const clearPages = () => query('DELETE FROM shard_pages') // Oldest-open first so the queue reads like a work list. const listPages = () => query(`SELECT ${PAGE_COLS} FROM shard_pages ORDER BY sent_ms ASC`) // ── Guild board (Protocol 2.0) ───────────────────────────────────────────── const GUILD_COLS = 'id, name, abbr, members, online, alliance, leader_serial, leader_name, leader_acct, leader_web_id, payload, t, updated_at' async function upsertGuild(id, fields) { const cols = Object.keys(fields) const allCols = ['id', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') await query( `INSERT INTO shard_guilds (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [id, ...cols.map((c) => fields[c])], ) } const removeGuild = (id) => query('DELETE FROM shard_guilds WHERE id = ?', [id]) const clearGuilds = () => query('DELETE FROM shard_guilds') const listGuilds = () => query(`SELECT ${GUILD_COLS} FROM shard_guilds ORDER BY name ASC`) // The guild an actor LEADS — matched on the current board (leader_serial or the // linked leader_acct), so it reflects live state. Guild MEMBERSHIP for non-leaders // is not modelled (the board carries only counts + leader), so we don't guess it. const findGuildLedByActor = (serial, acct) => query( `SELECT id, name, abbr, alliance, leader_name FROM shard_guilds WHERE leader_serial = ? OR (leader_acct IS NOT NULL AND leader_acct = ?) LIMIT 1`, [serial ?? null, acct ?? null], ) // Guilds led by any of the given game accounts (admin: a user's linked accounts). const listGuildsLedByAccounts = (accounts) => accounts.length === 0 ? Promise.resolve([]) : query( `SELECT id, name, abbr, alliance, leader_name FROM shard_guilds WHERE leader_acct IN (${accounts.map(() => '?').join(', ')}) ORDER BY name ASC`, accounts, ) // ── Governor board + term history (Protocol 2.0) ─────────────────────────── const GOV_COLS = 'city, governor_serial, governor_name, governor_acct, governor_web_id, elect_serial, elect_name, elect_acct, election_phase, candidates, auto_pick_at, payload, t, updated_at' async function upsertGovernor(city, fields) { const cols = Object.keys(fields) const allCols = ['city', ...cols] const insertCols = allCols.map((c) => `\`${c}\``).join(', ') const placeholders = allCols.map(() => '?').join(', ') const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') await query( `INSERT INTO shard_governors (${insertCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`, [city, ...cols.map((c) => fields[c])], ) } const listGovernors = () => query(`SELECT ${GOV_COLS} FROM shard_governors ORDER BY city ASC`) // Cities whose current governor is one of the given game accounts (cross-link: // does this user hold a governorship?). Empty list short-circuits. const listGovernorshipsByAccounts = (accounts) => accounts.length === 0 ? Promise.resolve([]) : query( `SELECT ${GOV_COLS} FROM shard_governors WHERE governor_acct IN (${accounts.map(() => '?').join(', ')}) ORDER BY city ASC`, accounts, ) // The single open term (ended_at IS NULL) for a city, if any. async function currentGovernorTerm(city) { const rows = await query( 'SELECT id, city, governor_serial, governor_name, governor_acct, governor_web_id, started_at, ended_at, votes FROM shard_governor_terms WHERE city = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1', [city], ) return rows[0] || null } const closeGovernorTerm = (id, endedAt) => query('UPDATE shard_governor_terms SET ended_at = ? WHERE id = ?', [endedAt, id]) const openGovernorTerm = ({ city, serial, name, acct, webId, startedAt }) => query( `INSERT INTO shard_governor_terms (city, governor_serial, governor_name, governor_acct, governor_web_id, started_at) VALUES (?, ?, ?, ?, ?, ?)`, [city, serial ?? null, name ?? null, acct ?? null, webId ?? null, startedAt], ) const listGovernorTerms = (city, limit) => query( 'SELECT id, city, governor_serial, governor_name, governor_acct, governor_web_id, started_at, ended_at, votes FROM shard_governor_terms WHERE city = ? ORDER BY started_at DESC LIMIT ?', [city, limit], ) // ── Online-population snapshot (Protocol 2.0 presence.online) ─────────────── async function setPresence({ count, byFacet, byRegion, t }) { await query( `INSERT INTO shard_presence (id, count, by_facet, by_region, t) VALUES (1, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE count = VALUES(count), by_facet = VALUES(by_facet), by_region = VALUES(by_region), t = VALUES(t)`, [ Number.isFinite(count) ? count : 0, byFacet ? JSON.stringify(byFacet) : null, byRegion ? JSON.stringify(byRegion) : null, Number.isFinite(t) ? t : null, ], ) } async function latestPresence() { const rows = await query('SELECT count, by_facet, by_region, t FROM shard_presence WHERE id = 1') return rows[0] || null } module.exports = { upsertOnline, removeOnline, clearOnline, countOnline, listOnline, listOnlineLinked, listOnlineByAccounts, insertEconomy, listEconomy, latestEconomy, upsertHouse, listIdocHouses, listHousesByAccounts, removeHouse, listRegistryHouses, upsertGuild, removeGuild, clearGuilds, listGuilds, findGuildLedByActor, listGuildsLedByAccounts, upsertGovernor, listGovernors, listGovernorshipsByAccounts, currentGovernorTerm, closeGovernorTerm, openGovernorTerm, listGovernorTerms, setPresence, latestPresence, upsertChamp, removeChamp, clearChamps, listChamps, upsertPage, removePage, clearPages, listPages, }