feat(rust): the live map draws monuments by label, minor labels hidden (PLAN_REDESIGNS §4, D196, D213) #28

Merged
whitlocktech merged 3 commits from feat/map-marker-labels into edge 2026-09-30 00:17:26 +00:00
12 changed files with 795 additions and 26 deletions

View File

@@ -0,0 +1,91 @@
// ── Which monument labels a map draws (PLAN_REDESIGNS §4, D196) ───────────
//
// The admin card's half of the rule `model/map/map.model.js` applies on the
// server: a switch per monument LABEL, the minor labels off unless somebody
// turns them on, every other label on. A server's switch wins over the fleet's,
// the fleet's over the built-in list.
//
// The form holds only the EXPLICIT switches, as the server does — `{ key: bool }`
// — so a label nobody touched keeps following the default above it. Setting the
// fleet back to what the built-in list says clears the row rather than storing a
// copy of the default: the next change to the list then reaches that label.
//
// Pure, and free of React, so it is tested in Node.
/** A label as its switch's key: trimmed, single-spaced, lower case. The server's rule exactly. */
export function markerKey(label) {
return String(label == null ? '' : label).trim().replace(/\s+/g, ' ').toLowerCase()
}
/** The built-in answer for one key: hidden for the minor labels, drawn for everything else. */
export function builtIn(minorLabels, key) {
return !(minorLabels || []).some((l) => markerKey(l) === key)
}
const has = (o, k) => Boolean(o) && Object.prototype.hasOwnProperty.call(o, k)
/** Whether the fleet draws one key: its own switch, or the built-in list. */
export function fleetShows(minorLabels, fleetMarkers, key) {
return has(fleetMarkers, key) ? fleetMarkers[key] : builtIn(minorLabels, key)
}
/** Whether one server draws one key: its switch, or the fleet's answer. */
export function serverShows(minorLabels, fleetMarkers, serverMarkers, key) {
return has(serverMarkers, key) ? serverMarkers[key] : fleetShows(minorLabels, fleetMarkers, key)
}
/**
* Every label the fleet list offers, sorted by name: each server's current
* labels and the minor list, each once. A minor label no map has yet is still
* listed, so it can be decided before a wipe brings it.
*/
export function fleetLabels(minorLabels, servers) {
const byKey = new Map()
// A capitalised spelling wins over the game's lower-case one ("jungle swamp").
const add = (key, label, count) => {
const seen = byKey.get(key)
if (!seen) byKey.set(key, { key, label, count })
else {
seen.count += count
if (seen.label === seen.label.toLowerCase() && label !== label.toLowerCase()) seen.label = label
}
}
for (const s of servers || []) {
for (const l of s.markerLabels || []) add(l.key, l.label, l.count || 0)
}
for (const label of minorLabels || []) add(markerKey(label), label, 0)
return [...byKey.values()].sort((a, b) => a.label.localeCompare(b.label))
}
/** The fleet form after ticking one key: an explicit switch, or none where it matches the built-in list. */
export function setFleet(minorLabels, fleetMarkers, key, shown) {
const next = { ...(fleetMarkers || {}) }
if (shown === builtIn(minorLabels, key)) delete next[key]
else next[key] = shown
return next
}
/** A server form after choosing one key: `null` follows the fleet, otherwise an explicit switch. */
export function setServer(serverMarkers, key, shown) {
const next = { ...(serverMarkers || {}) }
if (shown === null) delete next[key]
else next[key] = shown
return next
}
/**
* What changed between two explicit maps, as the PUT takes it: `{ key: bool }`
* for a switch set or changed, `{ key: null }` for one cleared. Null when
* nothing changed.
*/
export function markerDiff(before, now) {
const out = {}
const keys = new Set([...Object.keys(before || {}), ...Object.keys(now || {})])
for (const key of keys) {
const was = has(before, key) ? before[key] : undefined
const is = has(now, key) ? now[key] : undefined
if (was === is) continue
out[key] = is === undefined ? null : is
}
return Object.keys(out).length ? out : null
}

View File

@@ -31,11 +31,18 @@
// (D118). Beside each server is what its map picture is, and the two buttons
// that act at once rather than on Save: Fetch again, and Render now — shown only
// where there is no picture, with how long it will stall that server (D109).
//
// PLAN_REDESIGNS §4 adds which monuments the map draws (D196): a switch per
// monument label, because that is what staff read and it groups the variants
// (31 substations of four prefabs are one "Substation"). The minor labels start
// hidden and every other label drawn, so a monument Facepunch adds appears. One
// fleet list, and each server's own labels with an override each.
import { useCallback, useEffect, useState } from 'react'
import { ErrorState, Loading, useAsync } from '../../core.js'
import api from '../../api.js'
import { fleetLabels, fleetShows, markerDiff, serverShows, setFleet as setFleetMarker, setServer as setServerMarker } from '../../lib/mapMarkers.js'
const INHERIT = ''
@@ -117,7 +124,7 @@ export default function Visibility() {
setNews(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, Boolean(s.on)])))
setDelivery(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, s.delivery || 'chat'])))
if (state.map) {
setMapFleet({ ...state.map.fleet })
setMapFleet({ ...state.map.fleet, markers: { ...(state.map.fleet.markers || {}) } })
setMapServers(Object.fromEntries(state.map.servers.map((s) => [s.id, mapOverridesToForm(s.overrides)])))
}
}, [])
@@ -386,6 +393,7 @@ function mapOverridesToForm(overrides) {
const form = {}
for (const l of MAP_LAYERS) form[l.id] = overrides[l.id] || INHERIT
form.mates = overrides.mates === null || overrides.mates === undefined ? INHERIT : overrides.mates ? 'on' : 'off'
form.markers = { ...(overrides.markers || {}) }
return form
}
@@ -396,6 +404,8 @@ function mapDiff(card, fleet, servers) {
for (const key of [...MAP_LAYERS.map((l) => l.id), 'mates']) {
if (key in fleet && fleet[key] !== card.fleet[key]) fleetChanges[key] = fleet[key]
}
const fleetMarkers = markerDiff(card.fleet.markers, fleet.markers)
if (fleetMarkers) fleetChanges.markers = fleetMarkers
if (Object.keys(fleetChanges).length) out.fleet = fleetChanges
const serverChanges = {}
@@ -403,8 +413,10 @@ function mapDiff(card, fleet, servers) {
const before = mapOverridesToForm(s.overrides)
const now = servers[s.id] || before
const changed = {}
const markers = markerDiff(before.markers, now.markers)
if (markers) changed.markers = markers
for (const key of Object.keys(before)) {
if (now[key] === before[key]) continue
if (key === 'markers' || now[key] === before[key]) continue
if (key === 'mates') changed.mates = now.mates === INHERIT ? null : now.mates === 'on'
else changed[key] = now[key] === INHERIT ? null : now[key]
}
@@ -477,6 +489,8 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
</FleetRow>
</div>
<FleetMarkers card={card} fleet={fleet} setFleet={setFleet} />
{card.servers.length === 0 && (
<p className="sans dim" style={{ fontSize: '0.82rem', margin: '12px 0 0' }}>No servers are configured yet.</p>
)}
@@ -517,6 +531,13 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
</select>
</label>
</div>
<ServerMarkers
server={s}
minorLabels={card.minorLabels || []}
fleetMarkers={fleet.markers}
markers={form.markers}
onChange={(next) => set('markers', next)}
/>
{capped && (
<p className="dim" style={{ fontSize: '0.76rem', margin: '6px 0 0' }}>
Players will be shown to {(LABEL[presence] || presence).toLowerCase()} on this server, because that is who may see who is online.
@@ -549,6 +570,84 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
)
}
/**
* The fleet's monument labels (D196): every label on any server's current map,
* and the minor list, each a checkbox. Ticked is drawn.
*/
function FleetMarkers({ card, fleet, setFleet }) {
const minor = card.minorLabels || []
const labels = fleetLabels(minor, card.servers)
const hidden = labels.filter((l) => !fleetShows(minor, fleet.markers, l.key)).length
return (
<details className="sans" style={{ marginTop: 12, fontSize: '0.84rem' }}>
<summary style={{ cursor: 'pointer', color: 'var(--head)' }}>
Monuments on the map <span className="dim" style={{ fontSize: '0.76rem' }}>· {labels.length} labels, {hidden} hidden by default</span>
</summary>
<p className="dim" style={{ fontSize: '0.76rem', margin: '6px 0 8px' }}>
Ticked labels are drawn on every server that has not chosen for itself. The minor ones — substations, caves, train
tunnels, wells and the like — start hidden. Any label not listed here, such as a monument a game update adds, is drawn.
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))', gap: '4px 12px' }}>
{labels.map((l) => (
<label key={l.key} style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
<input
type="checkbox"
checked={fleetShows(minor, fleet.markers, l.key)}
onChange={(e) => setFleet((f) => ({ ...f, markers: setFleetMarker(minor, f.markers, l.key, e.target.checked) }))}
aria-label={`Draw ${l.label} by default`}
/>
<span>{l.label}</span>
{l.count > 0 && <span className="dim" style={{ fontSize: '0.72rem' }}>{l.count}</span>}
</label>
))}
</div>
</details>
)
}
/**
* One server's monument labels (D196): the labels on its current map, each
* following the fleet or drawn or hidden on this server alone.
*/
function ServerMarkers({ server, minorLabels, fleetMarkers, markers, onChange }) {
const labels = server.markerLabels || []
if (!labels.length) return null
const hidden = labels.filter((l) => !serverShows(minorLabels, fleetMarkers, markers, l.key)).length
const own = Object.keys(markers || {}).length
return (
<details style={{ marginTop: 8, fontSize: '0.8rem' }}>
<summary style={{ cursor: 'pointer' }} className="dim">
Monuments: {labels.length} labels on this map, {hidden} hidden{own ? `, ${own} set for this server` : ''}
</summary>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '4px 12px', marginTop: 6 }}>
{labels.map((l) => {
const mine = markers && Object.prototype.hasOwnProperty.call(markers, l.key) ? (markers[l.key] ? 'on' : 'off') : INHERIT
const fleetOn = fleetShows(minorLabels, fleetMarkers, l.key)
return (
<label key={l.key} style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'space-between' }}>
<span>
{l.label} <span className="dim" style={{ fontSize: '0.72rem' }}>{l.count}</span>
</span>
<select
value={mine}
onChange={(e) => onChange(setServerMarker(markers, l.key, e.target.value === INHERIT ? null : e.target.value === 'on'))}
style={{ ...selectStyle, fontSize: '0.78rem', padding: '2px 6px' }}
aria-label={`${l.label} on ${server.name}`}
>
<option value={INHERIT}>{`Default (${fleetOn ? 'drawn' : 'hidden'})`}</option>
<option value="on">Drawn</option>
<option value="off">Hidden</option>
</select>
</label>
)
})}
</div>
</details>
)
}
function FleetRow({ label, hint, children }) {
return (
<>

View File

@@ -0,0 +1,70 @@
// ── Which monument labels a map draws (PLAN_REDESIGNS §4, D196) ───────────
//
// The admin card must agree with the server about what a switch means, or the
// page says a label is drawn while the map leaves it out. These are the ways it
// could disagree: case, the built-in list, the fleet under a server, and a
// switch set back to its default.
import test from 'node:test'
import assert from 'node:assert/strict'
import { builtIn, fleetLabels, fleetShows, markerDiff, markerKey, serverShows, setFleet, setServer } from '../src/lib/mapMarkers.js'
const MINOR = ['Substation', 'Jungle Swamp', 'Ranch']
test('a label becomes the same key the server stores', () => {
assert.equal(markerKey(' jungle Swamp '), 'jungle swamp')
assert.equal(markerKey(null), '')
})
test('the minor labels start hidden and every other label drawn', () => {
assert.equal(builtIn(MINOR, 'substation'), false)
assert.equal(builtIn(MINOR, 'something facepunch added'), true)
})
test('a server switch wins over the fleet, and the fleet over the built-in list', () => {
assert.equal(fleetShows(MINOR, {}, 'substation'), false)
assert.equal(fleetShows(MINOR, { substation: true }, 'substation'), true)
assert.equal(serverShows(MINOR, { substation: true }, {}, 'substation'), true)
assert.equal(serverShows(MINOR, { substation: true }, { substation: false }, 'substation'), false)
assert.equal(serverShows(MINOR, {}, { 'power plant': false }, 'power plant'), false)
})
test('the fleet list is every server’s labels and the minor list, each once', () => {
const labels = fleetLabels(MINOR, [
{ markerLabels: [{ key: 'substation', label: 'Substation', count: 31 }, { key: 'harbor', label: 'Harbor', count: 2 }] },
{ markerLabels: [{ key: 'substation', label: 'Substation', count: 12 }] },
{},
])
assert.deepEqual(
labels.map((l) => [l.key, l.count]),
[
['harbor', 2],
['jungle swamp', 0],
['ranch', 0],
['substation', 43],
],
)
})
test('the fleet list shows a capitalised spelling where the game only wrote lower case', () => {
const labels = fleetLabels(MINOR, [{ markerLabels: [{ key: 'jungle swamp', label: 'jungle swamp', count: 3 }] }])
assert.deepEqual(labels.find((l) => l.key === 'jungle swamp'), { key: 'jungle swamp', label: 'Jungle Swamp', count: 3 })
})
test('setting the fleet back to the built-in answer clears the row instead of copying it', () => {
assert.deepEqual(setFleet(MINOR, {}, 'substation', true), { substation: true })
assert.deepEqual(setFleet(MINOR, { substation: true }, 'substation', false), {})
assert.deepEqual(setFleet(MINOR, {}, 'harbor', false), { harbor: false })
})
test('a server set to follow the fleet loses its own switch', () => {
assert.deepEqual(setServer({ substation: true }, 'substation', null), {})
assert.deepEqual(setServer({}, 'substation', false), { substation: false })
})
test('the diff sends a changed switch as its value and a removed one as null', () => {
assert.equal(markerDiff({ a: true }, { a: true }), null)
assert.deepEqual(markerDiff({ a: true, b: false }, { a: false, c: true }), { a: false, b: null, c: true })
assert.equal(markerDiff(undefined, {}), null)
})

View File

@@ -53,9 +53,108 @@ const MATES_KEY = 'map.mates'
const layerKey = (layer) => `map.layer.${layer}.audience`
/** Every override setting name a server may carry. */
/** Every override setting name a server may carry, besides the marker switches below. */
const SETTINGS = Object.freeze([...LAYERS.map(layerKey), MATES_KEY])
// ── Which monuments are drawn (PLAN_REDESIGNS §4, D196) ───────────────────
//
// A switch per monument LABEL, because that is what staff read and it groups the
// variants: 31 substations of four prefabs are one "Substation". It is clutter
// control, not a security boundary — the world layer's audience still decides
// whether a viewer gets monuments at all — but a hidden label is still left out
// of the answer rather than sent and hidden, like every other map rule.
//
// The minor labels below start OFF. **Every other label is on**, so a monument
// Facepunch adds next month appears rather than disappears. One row per label in
// `rust_settings` (the fleet) and `rust_map_overrides` (a server), `on` or `off`.
// Labels match case-insensitively: the game says "jungle swamp" for one prefab.
const MARKER_PREFIX = 'map.marker.'
/** Both setting columns are VARCHAR(64); a longer label cannot carry a switch. */
const MARKER_KEY_MAX = 64 - MARKER_PREFIX.length
/**
* The labels off unless an admin turns them on, spelled as the game labels
* them: all were read from the Carbon rig's 6000 map, seed 981448696, which has
* every one (2026-09-29). The game writes "Jungle ruin" there and "Jungle Ruin"
* on a 3000 map; the key is lower case, so both match.
*/
const MINOR_LABELS = Object.freeze([
'Substation',
'Underground Cave',
'Train Tunnel',
'Water Well',
'Wild Swamp',
'Jungle Swamp',
'Ice Lake',
'Jungle Ruin',
'Fishing Village',
'Large Barn',
'Abandoned Supermarket',
"Oxum's Gas Station",
'Mining Outpost',
'Ranch',
// D213: found on the 6000 map, hidden at the org lead's word.
'Canyon B',
'Canyon C',
'Lake A',
'Oasis A',
'Oasis C',
'Mountain',
'Train Tunnel Link',
'Abandoned Cabins',
])
/** A label as its switch's key: trimmed, single-spaced, lower case. */
const markerKey = (label) => String(label == null ? '' : label).trim().replace(/\s+/g, ' ').toLowerCase()
const MINOR = new Set(MINOR_LABELS.map(markerKey))
/** Whether a label is drawn when nobody has said: off for the minor list, on for everything else. */
const markerDefault = (key) => !MINOR.has(key)
/** A stored word as a boolean, or undefined for a word that is neither — which then follows the default. */
function markerWord(value) {
if (value === 'on') return true
if (value === 'off') return false
return undefined
}
/**
* Whether one label is drawn, given the explicit switches that apply (a
* server's over the fleet's, already merged). **Pure.**
*/
function markerShown(markers, label) {
const key = markerKey(label)
return markers && Object.prototype.hasOwnProperty.call(markers, key) ? markers[key] : markerDefault(key)
}
/** The monuments a viewer is sent: those whose label is drawn on this server. **Pure.** */
function filterMonuments(monuments, markers) {
return (monuments || []).filter((m) => markerShown(markers, m.label || m.kind))
}
/**
* The labels on one stored map, each once, with how many markers carry it and
* the spelling to show (a capitalised one where the game uses both).
*/
function markerLabels(monuments) {
const byKey = new Map()
for (const m of monuments || []) {
const label = String(m.label || m.kind || '').trim().replace(/\s+/g, ' ')
const key = markerKey(label)
if (!key) continue
const seen = byKey.get(key)
if (!seen) byKey.set(key, { key, label, count: 1 })
else {
seen.count += 1
if (seen.label === seen.label.toLowerCase() && label !== label.toLowerCase()) seen.label = label
}
}
return [...byKey.values()].sort((a, b) => a.label.localeCompare(b.label))
}
/**
* `DERIVATION_VERSION` (R9): how a row's geometry is worked out from what the
* plugin said. A stored row with an older number is re-derived from a fresh
@@ -100,19 +199,35 @@ function narrower(a, b) {
/** The fleet defaults, as stored, with the built-in defaults where nothing is. */
async function fleet() {
const stored = await Promise.all([...LAYERS.map((l) => visibilityDb.getSetting(layerKey(l))), visibilityDb.getSetting(MATES_KEY)])
const stored = await Promise.all([
...LAYERS.map((l) => visibilityDb.getSetting(layerKey(l))),
visibilityDb.getSetting(MATES_KEY),
visibilityDb.listSettings(MARKER_PREFIX),
])
const out = {}
LAYERS.forEach((layer, i) => {
out[layer] = stored[i] == null ? DEFAULTS[layer] : visibility.normalise(stored[i])
})
const mates = stored[LAYERS.length]
out.mates = mates == null ? DEFAULT_MATES : matesOn(mates)
out.markers = markersFrom(stored[LAYERS.length + 1] || [])
return out
}
/** Overrides rows as `{ world: 'staff', mates: false, … }`, only for what is set. */
function overridesFrom(rows) {
/** Marker rows as `{ substation: true, … }`, only for the labels somebody switched. */
function markersFrom(rows) {
const out = {}
for (const { setting, value } of rows) {
if (!String(setting).startsWith(MARKER_PREFIX)) continue
const shown = markerWord(value)
if (shown !== undefined) out[String(setting).slice(MARKER_PREFIX.length)] = shown
}
return out
}
/** Overrides rows as `{ world: 'staff', mates: false, markers: { … }, … }`, only for what is set. */
function overridesFrom(rows) {
const out = { markers: markersFrom(rows) }
for (const { setting, value } of rows) {
if (setting === MATES_KEY) out.mates = matesOn(value)
else {
@@ -123,10 +238,15 @@ function overridesFrom(rows) {
return out
}
/** A server's overrides over the fleet, the marker switches merged label by label. */
function over(base, overrides) {
return { ...base, ...overrides, markers: { ...base.markers, ...overrides.markers } }
}
/** What applies to one server: its overrides over the fleet. */
async function forServer(serverId) {
const [base, rows] = await Promise.all([fleet(), db.getOverrides(serverId)])
return { ...base, ...overridesFrom(rows) }
return over(base, overridesFrom(rows))
}
/**
@@ -160,12 +280,12 @@ async function access(req, serverId) {
visible: settings.mates && steamIds.length > 0,
}
return { level: viewer.level, userId: viewer.userId, layers, mates, steamIds }
return { level: viewer.level, userId: viewer.userId, layers, mates, steamIds, markers: settings.markers }
} catch (err) {
log.warn('could not resolve map visibility; showing the picture only', { server: serverId, error: err.message })
const layers = {}
for (const layer of LAYERS) layers[layer] = { visible: false, audience: 'staff' }
return { level: 'public', userId: null, layers, mates: { on: false, linked: false, visible: false }, steamIds: [] }
return { level: 'public', userId: null, layers, mates: { on: false, linked: false, visible: false }, steamIds: [], markers: {} }
}
}
@@ -305,12 +425,15 @@ async function describeSwitches(servers) {
return {
layers: [...LAYERS],
defaults: { ...DEFAULTS, mates: DEFAULT_MATES },
// D196: the labels that start off. Every label not listed here starts on.
minorLabels: [...MINOR_LABELS],
fleet: base,
servers: servers.map((s) => {
const overrides = overridesFrom(byServer.get(s.id) || [])
const full = {}
for (const key of [...LAYERS, 'mates']) full[key] = key in overrides ? overrides[key] : null
return { id: s.id, overrides: full, effective: { ...base, ...overrides } }
full.markers = overrides.markers
return { id: s.id, overrides: full, effective: over(base, overrides) }
}),
}
}
@@ -329,6 +452,30 @@ async function describeSwitches(servers) {
async function update({ fleet: fleetIn, servers } = {}, actor = null, serverExists = async () => true, { dryRun = false } = {}) {
const fleetChanges = []
const serverChanges = []
const fleetMarkers = []
const serverMarkers = []
// `markers` is `{ <label key>: true | false | null }` (D196). `null` clears:
// on a server it follows the fleet again, on the fleet the built-in list.
const checkMarkers = (markers, where) => {
if (!markers || typeof markers !== 'object' || Array.isArray(markers)) {
return { problem: `The marker switches${where} must be an object of label: on or off.` }
}
const out = []
for (const [key, value] of Object.entries(markers)) {
if (!key || markerKey(key) !== key) {
return { problem: `"${key}" is not a marker label key${where}: keys are the label in lower case, single-spaced.` }
}
if (key.length > MARKER_KEY_MAX) {
return { problem: `The label "${key}"${where} is longer than ${MARKER_KEY_MAX} characters, so it cannot carry a switch.` }
}
if (value !== null && typeof value !== 'boolean') {
return { problem: `The "${key}" markers are shown or hidden${where}, not "${value}".` }
}
out.push([key, value])
}
return { out }
}
const check = (key, value, where, allowNull) => {
if (value === null && allowNull) return null
@@ -344,6 +491,12 @@ async function update({ fleet: fleetIn, servers } = {}, actor = null, serverExis
}
for (const [key, value] of Object.entries(fleetIn || {})) {
if (key === 'markers') {
const { problem, out } = checkMarkers(value, '')
if (problem) return { ok: false, status: 400, message: problem }
fleetMarkers.push(...out)
continue
}
const problem = check(key, value, '', false)
if (problem) return { ok: false, status: 400, message: problem }
fleetChanges.push([key, value])
@@ -356,6 +509,12 @@ async function update({ fleet: fleetIn, servers } = {}, actor = null, serverExis
// eslint-disable-next-line no-await-in-loop
if (!(await serverExists(id))) return { ok: false, status: 404, message: `There is no server called ${id}.` }
for (const [key, value] of Object.entries(settings)) {
if (key === 'markers') {
const { problem, out } = checkMarkers(value, ` on server ${id}`)
if (problem) return { ok: false, status: 400, message: problem }
for (const [label, shown] of out) serverMarkers.push([id, label, shown])
continue
}
const problem = check(key, value, ` on server ${id}`, true)
if (problem) return { ok: false, status: 400, message: problem }
serverChanges.push([id, key, value])
@@ -376,14 +535,32 @@ async function update({ fleet: fleetIn, servers } = {}, actor = null, serverExis
// eslint-disable-next-line no-await-in-loop
await db.setOverride(id, settingOf(key), value === null ? null : wordOf(key, value), userId)
}
const markerWordOf = (shown) => (shown ? 'on' : 'off')
for (const [key, shown] of fleetMarkers) {
// eslint-disable-next-line no-await-in-loop
if (shown === null) await visibilityDb.clearSetting(MARKER_PREFIX + key)
// eslint-disable-next-line no-await-in-loop
else await visibilityDb.setSetting(MARKER_PREFIX + key, markerWordOf(shown), userId)
}
for (const [id, key, shown] of serverMarkers) {
// eslint-disable-next-line no-await-in-loop
await db.setOverride(id, MARKER_PREFIX + key, shown === null ? null : markerWordOf(shown), userId)
}
const changed = {}
if (fleetChanges.length) changed.fleet = Object.fromEntries(fleetChanges)
if (serverChanges.length) {
if (fleetMarkers.length) {
changed.fleet = { ...(changed.fleet || {}), markers: Object.fromEntries(fleetMarkers.map(([k, v]) => [k, v === null ? 'default' : v])) }
}
if (serverChanges.length || serverMarkers.length) {
changed.servers = {}
for (const [id, key, value] of serverChanges) {
changed.servers[id] = { ...(changed.servers[id] || {}), [key]: value === null ? 'inherit' : value }
}
for (const [id, key, shown] of serverMarkers) {
const entry = changed.servers[id] || {}
changed.servers[id] = { ...entry, markers: { ...(entry.markers || {}), [key]: shown === null ? 'inherit' : shown } }
}
}
return { ok: true, changed }
}
@@ -394,11 +571,18 @@ module.exports = {
DEFAULT_MATES,
MATES_KEY,
SETTINGS,
MARKER_PREFIX,
MARKER_KEY_MAX,
MINOR_LABELS,
DERIVATION_VERSION,
POLL_MS,
LIVE_CACHE_MS,
RENDER_MEASURED,
layerKey,
markerKey,
markerShown,
filterMonuments,
markerLabels,
renderStallSeconds,
narrower,
fleet,

View File

@@ -25,6 +25,22 @@ async function setSetting(key, value, userId = null) {
)
}
/**
* Every stored setting whose key starts with `prefix`, as `{ setting, value }`
* rows: the map's per-label marker switches are one row each (D196).
*/
async function listSettings(prefix) {
return core.query(
`SELECT setting_key AS setting, value FROM ${SETTINGS} WHERE setting_key LIKE CONCAT(?, '%')`,
[prefix.replace(/[\\%_]/g, '\\$&')],
)
}
/** Forget one setting, so it follows the built-in default again. */
async function clearSetting(key) {
await core.query(`DELETE FROM ${SETTINGS} WHERE setting_key = ?`, [key])
}
/** One server's override, `null` for "inherit", or `undefined` when there is no such server. */
async function getServerPresence(serverId) {
const rows = await core.query(`SELECT presence_audience AS presence FROM ${SERVERS} WHERE id = ?`, [serverId])
@@ -69,6 +85,8 @@ async function setServerNewsDelivery(serverId, delivery) {
module.exports = {
getSetting,
setSetting,
listSettings,
clearSetting,
getServerPresence,
listServerPresence,
setServerPresence,

View File

@@ -42,6 +42,8 @@ async function describeMap() {
const row = byId.get(s.id)
return {
...sw,
// D196: the labels on this server's current map, one switch each.
markerLabels: map.markerLabels(map.monumentsOf(row)),
name: s.name,
enabled: Boolean(s.enabled),
picture: row

View File

@@ -27,7 +27,7 @@ visibilityRouter.get(
'/',
// #swagger.tags = ['Admin · Rust']
// #swagger.summary = 'Who may see who is online, and who may see a clan roster'
// #swagger.description = 'The presence fleet default and every server’s optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboard’s `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting. `clans` carries the clan roster audience (default `members`: the clan’s own linked members, and staff) and each server’s clan board — whether it is current, at the game’s 100-clan ceiling, or running the uMod Clans plugin, whose clans are not Teams. `map` carries the live map’s switches: each layer’s fleet audience (`world` and `events` public, `players` and `bases` staff by default), the own-and-mates switch (on), each server’s overrides, and what each server’s map picture is — its source, the map it is of, when it was fetched, and how long a render would stall that server.'
// #swagger.description = 'The presence fleet default and every server’s optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboard’s `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting. `clans` carries the clan roster audience (default `members`: the clan’s own linked members, and staff) and each server’s clan board — whether it is current, at the game’s 100-clan ceiling, or running the uMod Clans plugin, whose clans are not Teams. `map` carries the live map’s switches: each layer’s fleet audience (`world` and `events` public, `players` and `bases` staff by default), the own-and-mates switch (on), the monument-label switches (`minorLabels` start hidden, every other label is drawn; `markerLabels` lists each server’s current map’s labels), each server’s overrides, and what each server’s map picture is — its source, the map it is of, when it was fetched, and how long a render would stall that server.'
/* #swagger.responses[200] = { description: 'The fleet default and each server', content: { "application/json": { schema: { $ref: "#/components/schemas/RustVisibility" } } } } */
requireRole('admin'),
visibility.read,
@@ -37,7 +37,7 @@ visibilityRouter.put(
'/',
// #swagger.tags = ['Admin · Rust']
// #swagger.summary = 'Change who may see who is online, who may see a clan roster, or which servers say news in chat'
// #swagger.description = 'Sets the presence fleet default, one or more server overrides, the clan roster audience, the per-server news-in-chat switches, or any of them together. A server set to `null` follows the fleet default again. `news` maps a server id to `true` or `false`: whether a published news post is also said in the in-game chat of that server (off by default). `newsDelivery` maps a server id to `chat` or `popup`: where that post goes — a popup needs PopupNotifications on the server, and one without it refuses the post with a reason. Validated whole before anything is written: a request naming a server that does not exist changes nothing. Widening the clan roster audience also shows which members are online to that audience, because a roster row carries it. `map` is `{ fleet, servers }`: `fleet` maps a layer (`world`, `events`, `players`, `bases`) to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. The players layer never shows more than who may see who is online, whatever it is set to.'
// #swagger.description = 'Sets the presence fleet default, one or more server overrides, the clan roster audience, the per-server news-in-chat switches, or any of them together. A server set to `null` follows the fleet default again. `news` maps a server id to `true` or `false`: whether a published news post is also said in the in-game chat of that server (off by default). `newsDelivery` maps a server id to `chat` or `popup`: where that post goes — a popup needs PopupNotifications on the server, and one without it refuses the post with a reason. Validated whole before anything is written: a request naming a server that does not exist changes nothing. Widening the clan roster audience also shows which members are online to that audience, because a roster row carries it. `map` is `{ fleet, servers }`: `fleet` maps a layer (`world`, `events`, `players`, `bases`) to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. `markers`, in either, maps a monument label key (lower case) to true or false, and null clears it (D196). The players layer never shows more than who may see who is online, whatever it is set to.'
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/RustVisibilityUpdate" } } } } */
/* #swagger.responses[200] = { description: 'Saved; answers the new state', content: { "application/json": { schema: { $ref: "#/components/schemas/RustVisibility" } } } } */
/* #swagger.responses[400] = { description: 'An audience that does not exist' } */

View File

@@ -264,7 +264,8 @@ async function getMap(req, res) {
}
: null,
geometry: map.geometryOf(row),
...(acc.layers.world.visible ? { monuments: map.monumentsOf(row) } : {}),
// D196: only the labels drawn on this server; a hidden label is not sent at all.
...(acc.layers.world.visible ? { monuments: map.filterMonuments(map.monumentsOf(row), acc.markers) } : {}),
layers: acc.layers,
// `signedIn` is what lets the page offer "link your Steam account" to the
// person who can act on it, and not to a visitor who has no account at all.

View File

@@ -152,7 +152,7 @@ rustRouter.get(
'/servers/:id/map',
// #swagger.tags = ['Public · Rust']
// #swagger.summary = 'One Rust server’s map'
// #swagger.description = 'Where the picture of the current map is, the geometry to draw it with (world size, the picture’s size and ocean margin in pixels, and the game’s own grid), the monuments when the viewer may see the world layer, and which of the four layers — `world`, `events`, `players`, `bases` — this viewer gets. A hidden layer says which audience can see it and never what it holds. The players layer can never be wider than who may see who is online (`cappedByPresence`). `mates` says whether this viewer gets their own position and their online clan mates’. `picture` is null when the game has no picture of its map; `geometry` is null when the server has never described its map.'
// #swagger.description = 'Where the picture of the current map is, the geometry to draw it with (world size, the picture’s size and ocean margin in pixels, and the game’s own grid), the monuments when the viewer may see the world layer — only the labels drawn on this server, so a hidden label such as Substation is not sent at all (D196) — and which of the four layers — `world`, `events`, `players`, `bases` — this viewer gets. A hidden layer says which audience can see it and never what it holds. The players layer can never be wider than who may see who is online (`cappedByPresence`). `mates` says whether this viewer gets their own position and their online clan mates’. `picture` is null when the game has no picture of its map; `geometry` is null when the server has never described its map.'
// #swagger.parameters['id'] = { in: 'path', required: true, description: 'The server’s slug', schema: { type: 'string' } }
/* #swagger.responses[200] = { description: 'The map, as this viewer may see it', content: { "application/json": { schema: { $ref: "#/components/schemas/RustMap" } } } } */
/* #swagger.responses[404] = { description: 'No such server, or it is disabled' } */

View File

@@ -741,6 +741,12 @@ module.exports = {
description: 'The live map’s switches and each server’s picture (phase 14).',
properties: {
layers: { type: 'array', items: { type: 'string' }, example: ['world', 'events', 'players', 'bases'] },
minorLabels: {
type: 'array',
items: { type: 'string' },
description: 'D196: the monument labels hidden unless an admin turns them on. Every other label is drawn.',
example: ['Substation', 'Underground Cave', 'Train Tunnel'],
},
fleet: {
type: 'object',
properties: {
@@ -749,6 +755,7 @@ module.exports = {
players: { $ref: '#/components/schemas/RustAudience' },
bases: { $ref: '#/components/schemas/RustAudience' },
mates: { type: 'boolean', example: true },
markers: { type: 'object', description: 'D196: a monument label key (lower case) to true (drawn) or false (hidden). Only the labels somebody switched; every other label follows the default.', additionalProperties: { type: 'boolean' }, example: { substation: true } },
},
},
servers: {
@@ -758,8 +765,20 @@ module.exports = {
properties: {
id: { type: 'string', example: 'main' },
name: { type: 'string', example: 'Main · Vanilla' },
overrides: { type: 'object', description: 'Each layer and `mates`, or null to follow the fleet.' },
overrides: { type: 'object', description: 'Each layer and `mates`, or null to follow the fleet; `markers` holds only the labels this server switched itself.' },
effective: { type: 'object' },
markerLabels: {
type: 'array',
description: 'D196: the monument labels on this server’s current map, each once, with how many markers carry it.',
items: {
type: 'object',
properties: {
key: { type: 'string', example: 'substation' },
label: { type: 'string', example: 'Substation' },
count: { type: 'integer', example: 31 },
},
},
},
picture: {
type: 'object',
nullable: true,
@@ -852,7 +871,7 @@ module.exports = {
},
monuments: {
type: 'array',
description: 'Present only when the viewer may see the world layer.',
description: 'Present only when the viewer may see the world layer, and only the labels drawn on this server (D196): minor labels such as Substation are left out unless an admin turned them on.',
items: {
type: 'object',
properties: {
@@ -1120,8 +1139,8 @@ module.exports = {
},
map: {
type: 'object',
description: 'The live map’s switches. `fleet` maps a layer to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet.',
example: { fleet: { players: 'signed_in', mates: true }, servers: { pve: { players: 'public' }, pvp: { players: null } } },
description: 'The live map’s switches. `fleet` maps a layer to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. `markers` (in either) maps a monument label key — the label in lower case, single-spaced — to true (drawn) or false (hidden); null clears it, so a server follows the fleet and the fleet the built-in list (D196).',
example: { fleet: { players: 'signed_in', mates: true, markers: { substation: true } }, servers: { pve: { players: 'public', markers: { substation: false } }, pvp: { players: null } } },
},
},
},

View File

@@ -14,6 +14,8 @@
// a fetch that straddles a map change is not spliced
// one fetch per server, and a plugin that predates the map is never asked
// any number of viewers cost one ask of the game (D111)
// D196: the minor labels start off, every other label on, and a hidden label
// is not sent; a server's switch wins over the fleet's, label by label
const test = require('node:test')
const assert = require('node:assert')
@@ -39,6 +41,7 @@ const visibilityDb = require('../model/visibility/visibility.db')
function settings(t, { stored = {}, overrides = [], viewer = { level: 'public', userId: null }, presence = 'staff', links = [], mates = [] } = {}) {
const saved = {
getSetting: visibilityDb.getSetting,
listSettings: visibilityDb.listSettings,
getOverrides: mapDb.getOverrides,
viewer: visibility.viewer,
presenceFor: visibility.presenceFor,
@@ -47,6 +50,8 @@ function settings(t, { stored = {}, overrides = [], viewer = { level: 'public',
}
const asked = { clans: 0 }
visibilityDb.getSetting = async (key) => (key in stored ? stored[key] : null)
visibilityDb.listSettings = async (prefix) =>
Object.entries(stored).filter(([k]) => k.startsWith(prefix)).map(([setting, value]) => ({ setting, value }))
mapDb.getOverrides = async () => overrides
visibility.viewer = async () => viewer
visibility.presenceFor = async () => presence
@@ -57,6 +62,7 @@ function settings(t, { stored = {}, overrides = [], viewer = { level: 'public',
}
t.after(() => {
visibilityDb.getSetting = saved.getSetting
visibilityDb.listSettings = saved.listSettings
mapDb.getOverrides = saved.getOverrides
visibility.viewer = saved.viewer
visibility.presenceFor = saved.presenceFor
@@ -82,7 +88,7 @@ const LIVE = {
test('a fresh install shows the world and events to anybody, and positions to staff only', async (t) => {
settings(t)
assert.deepStrictEqual(await map.fleet(), { world: 'public', events: 'public', players: 'staff', bases: 'staff', mates: true })
assert.deepStrictEqual(await map.fleet(), { world: 'public', events: 'public', players: 'staff', bases: 'staff', mates: true, markers: {} })
const acc = await map.access({}, 'main')
assert.equal(acc.layers.world.visible, true)
@@ -218,6 +224,137 @@ test('the admin write refuses a word it does not know, and writes nothing on a d
])
})
// ── Which monuments are drawn (PLAN_REDESIGNS §4, D196) ───────────────────
const MONUMENTS = [
{ kind: 'power_sub_big_1', label: 'Substation', x: 1, z: 1 },
{ kind: 'power_sub_small_2', label: 'Substation', x: 2, z: 2 },
{ kind: 'ue_jungle_swamp_a', label: 'jungle swamp', x: 3, z: 3 },
{ kind: 'fishing_village_b', label: 'Fishing Village', x: 4, z: 4 },
{ kind: 'fishing_village_a', label: 'Large Fishing Village', x: 5, z: 5 },
{ kind: 'powerplant_1', label: 'Power Plant', x: 6, z: 6 },
{ kind: 'brand_new_thing', label: 'Something Facepunch Added', x: 7, z: 7 },
]
const labelsOf = (list) => list.map((m) => m.label)
test('D196: a fresh install hides the minor labels and shows everything else, a new label included', () => {
assert.deepStrictEqual(labelsOf(map.filterMonuments(MONUMENTS, {})), [
'Large Fishing Village',
'Power Plant',
'Something Facepunch Added',
])
assert.equal(map.markerShown({}, 'JUNGLE Swamp'), false, 'labels match case- and space-insensitively')
})
test('D196/D213: the Carbon rig’s 6000 map (seed 981448696) draws its 29 places and hides its 22 minor labels', () => {
// Every label that map carries, as the game spelled them on 2026-09-29.
const minor = [
'Abandoned Cabins', 'Abandoned Supermarket', 'Canyon B', 'Canyon C', 'Fishing Village', 'Ice Lake', 'Jungle ruin',
'jungle swamp', 'Lake A', 'Large Barn', 'Mining Outpost', 'Mountain', 'Oasis A', 'Oasis C', "Oxum's Gas Station",
'Ranch', 'Substation', 'Train Tunnel', 'Train Tunnel Link', 'Underground Cave', 'Water Well', 'Wild Swamp',
]
const places = [
'Abandoned Military Base', 'Airfield', 'Apartment Complex', 'Arctic Research Base', 'Bandit Camp', 'Ferry Terminal',
'Giant Excavator Pit', 'Harbor', 'HQM Quarry', 'Jungle Ziggurat', 'Junkyard', 'Large Fishing Village', 'Large Oil Rig',
'Launch Site', 'Lighthouse', 'Military Tunnel', 'Missile Silo', 'Oil Rig', 'Outpost', 'Power Plant', 'Radtown',
'Satellite Dish', 'Sewer Branch', 'Stone Quarry', 'Sulfur Quarry', 'The Dome', 'Train Yard', 'Underwater Lab',
'Water Treatment Plant',
]
const all = [...minor, ...places].map((label, i) => ({ kind: `k${i}`, label, x: i, z: i }))
assert.deepStrictEqual(labelsOf(map.filterMonuments(all, {})).sort(), [...places].sort())
assert.equal(map.MINOR_LABELS.length, minor.length, 'every built-in label is on that map')
})
test('D196: the fleet turns a minor label on, and a server turns it off again for itself only', async (t) => {
settings(t, {
stored: { 'map.marker.substation': 'on', 'map.marker.power plant': 'off', 'map.marker.jungle swamp': 'maybe' },
overrides: [{ setting: 'map.marker.substation', value: 'off' }],
})
const fleet = await map.fleet()
assert.deepStrictEqual(fleet.markers, { substation: true, 'power plant': false }, 'a word that is neither on nor off follows the default')
const pve = await map.forServer('pve')
assert.deepStrictEqual(pve.markers, { substation: false, 'power plant': false })
assert.deepStrictEqual(labelsOf(map.filterMonuments(MONUMENTS, pve.markers)), ['Large Fishing Village', 'Something Facepunch Added'])
assert.deepStrictEqual(labelsOf(map.filterMonuments(MONUMENTS, fleet.markers)), [
'Substation',
'Substation',
'Large Fishing Village',
'Something Facepunch Added',
])
})
test('D196: the viewer’s access carries the switches that apply to that server', async (t) => {
settings(t, { overrides: [{ setting: 'map.marker.water well', value: 'on' }] })
const acc = await map.access({}, 'main')
assert.deepStrictEqual(acc.markers, { 'water well': true })
})
test('D196: the admin sees each label once, with its count and a capitalised spelling', () => {
const labels = map.markerLabels([...MONUMENTS, { kind: 'ue_jungle_swamp_b', label: 'Jungle Swamp', x: 8, z: 8 }])
assert.deepStrictEqual(
labels.map((l) => [l.key, l.label, l.count]),
[
['fishing village', 'Fishing Village', 1],
['jungle swamp', 'Jungle Swamp', 2],
['large fishing village', 'Large Fishing Village', 1],
['power plant', 'Power Plant', 1],
['something facepunch added', 'Something Facepunch Added', 1],
['substation', 'Substation', 2],
],
)
assert.deepStrictEqual(map.markerLabels(undefined), [])
})
test('D196: the admin write stores a switch per label, clears with null, and refuses a bad key or word', async (t) => {
settings(t)
const writes = []
const saved = { setSetting: visibilityDb.setSetting, clearSetting: visibilityDb.clearSetting, setOverride: mapDb.setOverride }
visibilityDb.setSetting = async (...a) => writes.push(['fleet', ...a])
visibilityDb.clearSetting = async (...a) => writes.push(['fleet-clear', ...a])
mapDb.setOverride = async (...a) => writes.push(['server', ...a])
t.after(() => Object.assign(visibilityDb, { setSetting: saved.setSetting, clearSetting: saved.clearSetting }) && Object.assign(mapDb, { setOverride: saved.setOverride }))
assert.equal((await map.update({ fleet: { markers: { Substation: true } } })).status, 400, 'keys are lower case')
assert.equal((await map.update({ fleet: { markers: { substation: 'on' } } })).status, 400, 'values are booleans')
assert.equal((await map.update({ fleet: { markers: ['substation'] } })).status, 400)
assert.equal((await map.update({ fleet: { markers: { ['x'.repeat(map.MARKER_KEY_MAX + 1)]: true } } })).status, 400)
assert.equal((await map.update({ servers: { pve: { markers: { substation: 'off' } } } })).status, 400)
assert.equal(writes.length, 0)
const done = await map.update(
{ fleet: { world: 'staff', markers: { substation: true, 'water well': null } }, servers: { pve: { markers: { substation: false, 'power plant': null } } } },
{ id: 3 },
)
assert.equal(done.ok, true)
assert.deepStrictEqual(writes, [
['fleet', 'map.layer.world.audience', 'staff', 3],
['fleet', 'map.marker.substation', 'on', 3],
['fleet-clear', 'map.marker.water well'],
['server', 'pve', 'map.marker.substation', 'off', 3],
['server', 'pve', 'map.marker.power plant', null, 3],
])
assert.deepStrictEqual(done.changed, {
fleet: { world: 'staff', markers: { substation: true, 'water well': 'default' } },
servers: { pve: { markers: { substation: false, 'power plant': 'inherit' } } },
})
})
test('D196: the admin card lists the minor labels, and each server’s marker overrides', async (t) => {
settings(t, { stored: { 'map.marker.ranch': 'on' } })
const saved = { listOverrides: mapDb.listOverrides }
mapDb.listOverrides = async () => [{ serverId: 'pve', setting: 'map.marker.substation', value: 'on' }]
t.after(() => Object.assign(mapDb, saved))
const card = await map.describeSwitches([{ id: 'pve' }, { id: 'pvp' }])
assert.ok(card.minorLabels.includes('Substation'))
assert.deepStrictEqual(card.fleet.markers, { ranch: true })
assert.deepStrictEqual(card.servers[0].overrides.markers, { substation: true })
assert.deepStrictEqual(card.servers[0].effective.markers, { ranch: true, substation: true })
assert.deepStrictEqual(card.servers[1].overrides.markers, {})
})
test('derive: a map with no picture is sized like the Rust+ cache would be', () => {
const row = map.derive('main', { mapKey: '4500.7.1', source: 'none', worldSize: 4500, oceanMargin: 500, gridCells: 30, gridCellSize: 150, monuments: [] })
assert.equal(row.width, 3250)

View File

@@ -1430,7 +1430,7 @@
"Admin · Rust"
],
"summary": "Who may see who is online, and who may see a clan roster",
"description": "The presence fleet default and every server’s optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboard’s `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting. `clans` carries the clan roster audience (default `members`: the clan’s own linked members, and staff) and each server’s clan board — whether it is current, at the game’s 100-clan ceiling, or running the uMod Clans plugin, whose clans are not Teams. `map` carries the live map’s switches: each layer’s fleet audience (`world` and `events` public, `players` and `bases` staff by default), the own-and-mates switch (on), each server’s overrides, and what each server’s map picture is — its source, the map it is of, when it was fetched, and how long a render would stall that server.",
"description": "The presence fleet default and every server’s optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboard’s `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting. `clans` carries the clan roster audience (default `members`: the clan’s own linked members, and staff) and each server’s clan board — whether it is current, at the game’s 100-clan ceiling, or running the uMod Clans plugin, whose clans are not Teams. `map` carries the live map’s switches: each layer’s fleet audience (`world` and `events` public, `players` and `bases` staff by default), the own-and-mates switch (on), the monument-label switches (`minorLabels` start hidden, every other label is drawn; `markerLabels` lists each server’s current map’s labels), each server’s overrides, and what each server’s map picture is — its source, the map it is of, when it was fetched, and how long a render would stall that server.",
"responses": {
"200": {
"description": "The fleet default and each server",
@@ -1452,7 +1452,7 @@
"Admin · Rust"
],
"summary": "Change who may see who is online, who may see a clan roster, or which servers say news in chat",
"description": "Sets the presence fleet default, one or more server overrides, the clan roster audience, the per-server news-in-chat switches, or any of them together. A server set to `null` follows the fleet default again. `news` maps a server id to `true` or `false`: whether a published news post is also said in the in-game chat of that server (off by default). `newsDelivery` maps a server id to `chat` or `popup`: where that post goes — a popup needs PopupNotifications on the server, and one without it refuses the post with a reason. Validated whole before anything is written: a request naming a server that does not exist changes nothing. Widening the clan roster audience also shows which members are online to that audience, because a roster row carries it. `map` is `{ fleet, servers }`: `fleet` maps a layer (`world`, `events`, `players`, `bases`) to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. The players layer never shows more than who may see who is online, whatever it is set to.",
"description": "Sets the presence fleet default, one or more server overrides, the clan roster audience, the per-server news-in-chat switches, or any of them together. A server set to `null` follows the fleet default again. `news` maps a server id to `true` or `false`: whether a published news post is also said in the in-game chat of that server (off by default). `newsDelivery` maps a server id to `chat` or `popup`: where that post goes — a popup needs PopupNotifications on the server, and one without it refuses the post with a reason. Validated whole before anything is written: a request naming a server that does not exist changes nothing. Widening the clan roster audience also shows which members are online to that audience, because a roster row carries it. `map` is `{ fleet, servers }`: `fleet` maps a layer (`world`, `events`, `players`, `bases`) to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. `markers`, in either, maps a monument label key (lower case) to true or false, and null clears it (D196). The players layer never shows more than who may see who is online, whatever it is set to.",
"responses": {
"200": {
"description": "Saved; answers the new state",
@@ -2432,7 +2432,7 @@
"Public · Rust"
],
"summary": "One Rust server’s map",
"description": "Where the picture of the current map is, the geometry to draw it with (world size, the picture’s size and ocean margin in pixels, and the game’s own grid), the monuments when the viewer may see the world layer, and which of the four layers — `world`, `events`, `players`, `bases` — this viewer gets. A hidden layer says which audience can see it and never what it holds. The players layer can never be wider than who may see who is online (`cappedByPresence`). `mates` says whether this viewer gets their own position and their online clan mates’. `picture` is null when the game has no picture of its map; `geometry` is null when the server has never described its map.",
"description": "Where the picture of the current map is, the geometry to draw it with (world size, the picture’s size and ocean margin in pixels, and the game’s own grid), the monuments when the viewer may see the world layer — only the labels drawn on this server, so a hidden label such as Substation is not sent at all (D196) — and which of the four layers — `world`, `events`, `players`, `bases` — this viewer gets. A hidden layer says which audience can see it and never what it holds. The players layer can never be wider than who may see who is online (`cappedByPresence`). `mates` says whether this viewer gets their own position and their online clan mates’. `picture` is null when the game has no picture of its map; `geometry` is null when the server has never described its map.",
"parameters": [
{
"name": "id",
@@ -7393,6 +7393,39 @@
}
}
},
"minorLabels": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
}
}
},
"description": {
"type": "string",
"example": "D196: the monument labels hidden unless an admin turns them on. Every other label is drawn."
},
"example": {
"type": "array",
"example": [
"Substation",
"Underground Cave",
"Train Tunnel"
],
"items": {
"type": "string"
}
}
}
},
"fleet": {
"type": "object",
"properties": {
@@ -7427,6 +7460,37 @@
"example": true
}
}
},
"markers": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "D196: a monument label key (lower case) to true (drawn) or false (hidden). Only the labels somebody switched; every other label follows the default."
},
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
}
}
},
"example": {
"type": "object",
"properties": {
"substation": {
"type": "boolean",
"example": true
}
}
}
}
}
}
}
@@ -7484,7 +7548,7 @@
},
"description": {
"type": "string",
"example": "Each layer and `mates`, or null to follow the fleet."
"example": "Each layer and `mates`, or null to follow the fleet; `markers` holds only the labels this server switched itself."
}
}
},
@@ -7497,6 +7561,72 @@
}
}
},
"markerLabels": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"description": {
"type": "string",
"example": "D196: the monument labels on this server’s current map, each once, with how many markers carry it."
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"key": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "substation"
}
}
},
"label": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Substation"
}
}
},
"count": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 31
}
}
}
}
}
}
}
}
},
"picture": {
"type": "object",
"properties": {
@@ -8082,7 +8212,7 @@
},
"description": {
"type": "string",
"example": "Present only when the viewer may see the world layer."
"example": "Present only when the viewer may see the world layer, and only the labels drawn on this server (D196): minor labels such as Substation are left out unless an admin turned them on."
},
"items": {
"type": "object",
@@ -10160,7 +10290,7 @@
},
"description": {
"type": "string",
"example": "The live map’s switches. `fleet` maps a layer to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet."
"example": "The live map’s switches. `fleet` maps a layer to an audience and `mates` to true or false; `servers` maps a server id to the same shape, where null follows the fleet. `markers` (in either) maps a monument label key — the label in lower case, single-spaced — to true (drawn) or false (hidden); null clears it, so a server follows the fleet and the fleet the built-in list (D196)."
},
"example": {
"type": "object",
@@ -10175,6 +10305,15 @@
"mates": {
"type": "boolean",
"example": true
},
"markers": {
"type": "object",
"properties": {
"substation": {
"type": "boolean",
"example": true
}
}
}
}
},
@@ -10187,6 +10326,15 @@
"players": {
"type": "string",
"example": "public"
},
"markers": {
"type": "object",
"properties": {
"substation": {
"type": "boolean",
"example": false
}
}
}
}
},