PLAN.md §30 as approved, plus D119/D120 from the build. Server: - rust_map_images (one row per server: picture as MEDIUMBLOB, geometry, monuments, DERIVATION_VERSION) and rust_map_overrides; purge.sql pair. - mapImages.js: D110. The board poll notices a new boot/wipe/seed/size and asks map.info; a new key or hash from the free Rust+ cache (or a render kept on disk) is fetched in slices, checked against its SHA-256 and stored in one statement. One fetch per server, a backoff on failure, `stale` abandons a fetch that straddles a map change. Render now (D109) is admin-only and watched to completion. - mapLive.js: D111. One map.live per server per 5 s whoever asks; positions are held in memory only. - model/map: four layers (world, events public; players, bases staff), a fleet default plus per-server override (D114), the players layer capped by presence (D113), own dot and online first-party clan mates for a linked viewer (D115, D117, D118). A layer the viewer may not see is absent from the answer, never sent and hidden. - Routes: public /servers/:id/map, /map/image (immutable under its hash), /map/live; admin /servers/:id/map/fetch and /render; the Map card on the visibility PUT. Swagger fragment and frozen manifest regenerated. Client: - A Map tab: Leaflet over the picture in CRS.Simple, the game's own grid (labels only when a cell is wide enough to hold one), a legend that lists hidden layers with who can see them, polled every 10 s while visible. - D120: Leaflet is a lazy split chunk beside entry.js, not in it. release.yml copies every dist/*.js; checkExternals and build.test.js hold both ends. - The Map card on Admin -> Rust visibility, with Fetch again and Render now. Capability `map` declared for the Android app (phase 15). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
148 lines
5.8 KiB
JavaScript
148 lines
5.8 KiB
JavaScript
// ── 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,
|
|
}
|