// ── SQL for the map ─────────────────────────────────────────────────────── // // Three things: the picture each server's map is drawn on (`rust_map_images`), // the per-server overrides of the map's switches (`rust_map_overrides`), and the // two reads the own-and-mates view needs — which Steam accounts a website user // holds, and who shares a clan with them on one server. // // Nothing here stores a POSITION. Where people are is asked for while somebody // is looking and kept in memory for seconds (D111, `mapLive.js`). const core = require('../../core') const IMAGES = 'rust_map_images' const OVERRIDES = 'rust_map_overrides' const LINKS = 'rust_account_links' const CLANS = 'rust_clans' const MEMBERS = 'rust_clan_members' /** The columns every read but the picture's own wants — everything except the bytes. */ const META = `server_id AS serverId, map_key AS mapKey, sha256, source, width, height, ocean_margin AS oceanMargin, world_size AS worldSize, grid_cells AS gridCells, grid_cell_size AS gridCellSize, background, derivation, monuments, OCTET_LENGTH(bytes) AS byteCount, fetched_at AS fetchedAt` /** One server's picture row without the bytes, or undefined. */ async function getMeta(serverId) { const rows = await core.query(`SELECT ${META} FROM ${IMAGES} WHERE server_id = ?`, [serverId]) return rows[0] } /** Every server's picture row without the bytes, for the admin card. */ async function listMeta() { return core.query(`SELECT ${META} FROM ${IMAGES}`) } /** * The picture itself, but only if it is still the one named. A URL carries the * hash it was minted for, and a picture replaced since must not be served under * it: the response is cached as immutable. */ async function getBytes(serverId, sha256) { const rows = await core.query( `SELECT bytes FROM ${IMAGES} WHERE server_id = ? AND sha256 = ? AND bytes IS NOT NULL`, [serverId, sha256], ) return rows[0] ? rows[0].bytes : null } /** * Replace one server's row whole, in ONE statement. A new map's picture and its * geometry arrive together or not at all; a reader never sees the new bytes * under the old monuments. */ async function putImage(row) { await core.query( `REPLACE INTO ${IMAGES} (server_id, map_key, sha256, source, width, height, ocean_margin, world_size, grid_cells, grid_cell_size, background, derivation, monuments, bytes, fetched_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, [ row.serverId, row.mapKey, row.sha256 || null, row.source, row.width, row.height, row.oceanMargin, row.worldSize, row.gridCells, row.gridCellSize, row.background || null, row.derivation, JSON.stringify(row.monuments || []), row.bytes || null, ], ) } /** * Re-derive a row's geometry and monuments without touching its picture — the * `DERIVATION_VERSION` path, and a map whose picture is unchanged but whose * source moved (a render replaced by the Rust+ cache of the same bytes). */ async function putGeometry(row) { await core.query( `UPDATE ${IMAGES} SET source = ?, width = ?, height = ?, ocean_margin = ?, world_size = ?, grid_cells = ?, grid_cell_size = ?, background = ?, derivation = ?, monuments = ? WHERE server_id = ? AND map_key = ?`, [ row.source, row.width, row.height, row.oceanMargin, row.worldSize, row.gridCells, row.gridCellSize, row.background || null, row.derivation, JSON.stringify(row.monuments || []), row.serverId, row.mapKey, ], ) } /** Every override for one server, as `{ setting: value }` rows. */ async function getOverrides(serverId) { return core.query(`SELECT setting, value FROM ${OVERRIDES} WHERE server_id = ?`, [serverId]) } /** Every override in the fleet, for the admin card. */ async function listOverrides() { return core.query(`SELECT server_id AS serverId, setting, value FROM ${OVERRIDES}`) } /** Set (a word) or clear (`null`) one server's override of one switch. */ async function setOverride(serverId, setting, value, userId = null) { if (value === null) { await core.query(`DELETE FROM ${OVERRIDES} WHERE server_id = ? AND setting = ?`, [serverId, setting]) return } await core.query( `INSERT INTO ${OVERRIDES} (server_id, setting, value, updated_by, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_by = VALUES(updated_by), updated_at = CURRENT_TIMESTAMP`, [serverId, setting, value, userId], ) } /** Every Steam id one website user has linked. A link reaches every server (D28). */ async function steamIdsForUser(userId) { const rows = await core.query(`SELECT steam_id AS steamId FROM ${LINKS} WHERE user_id = ?`, [userId]) return rows.map((r) => String(r.steamId)) } /** * Every Steam id sharing a first-party clan with any of `steamIds` on ONE * server (D115, D117) — the viewer's own accounts included, since they are * members too. A clan the board no longer carries (`gone_at`) is nobody's clan. */ async function clanMatesOn(serverId, steamIds) { if (!steamIds.length) return [] const rows = await core.query( `SELECT DISTINCT m2.steam_id AS steamId FROM ${MEMBERS} m1 JOIN ${CLANS} c ON c.external_id = m1.external_id JOIN ${MEMBERS} m2 ON m2.external_id = m1.external_id WHERE c.server_id = ? AND c.gone_at IS NULL AND m1.steam_id IN (${steamIds.map(() => '?').join(', ')})`, [serverId, ...steamIds], ) return rows.map((r) => String(r.steamId)) } module.exports = { getMeta, listMeta, getBytes, putImage, putGeometry, getOverrides, listOverrides, setOverride, steamIdsForUser, clanMatesOn, }