feat(rust): the live map (phase 14, protocol 11)
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
This commit is contained in:
147
server/model/map/map.db.js
Normal file
147
server/model/map/map.db.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// ── 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,
|
||||
}
|
||||
Reference in New Issue
Block a user