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

A switch per monument label, a fleet default and a per-server override, on
Admin -> Rust visibility's live map card. Substations, caves, train tunnels,
wells and the other minor labels start hidden; every other label is drawn,
so a monument a game update adds appears. GET /map leaves hidden labels out
of the answer, so the website and the app both lose them.

No schema change: rows are map.marker.<label key> in rust_settings and
rust_map_overrides. No wire change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-29 12:10:47 -05:00
parent eb184aaac8
commit 0da8f44b55
12 changed files with 759 additions and 26 deletions

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,118 @@ 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: 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)