// ── The live map (PLAN.md §30, protocol 11) ─────────────────────────────── // // The map is a security boundary before it is a picture: player positions // locate people, and base positions are where they sleep. Every test here is one // of the ways that boundary could look right and be wrong: // // a fresh install shows the world and events, and nobody's position // a word nobody recognises narrows — a layer to staff, the mates switch to off // the players layer can never be wider than presence (D113) // a hidden layer is ABSENT from the answer, not an empty array // own dot and mates: own accounts (asleep too), ONLINE clan mates, nobody else // the roster's audience has nothing to do with who sees positions (D117) // a picture is stored only when it matches its own hash // 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) const test = require('node:test') const assert = require('node:assert') const crypto = require('crypto') const { fakeCtx } = require('./_fakes') require('../core')._reset() require('../core').init(fakeCtx()) const client = require('../sidecarClient') const mapDb = require('../model/map/map.db') const map = require('../model/map/map.model') const mapImages = require('../mapImages') const mapLive = require('../mapLive') const visibility = require('../model/visibility/visibility.model') const visibilityDb = require('../model/visibility/visibility.db') /** * Stub every collaborator the model reads: the stored settings, the overrides, * the viewer, the presence audience, the viewer's links and the clan query. */ function settings(t, { stored = {}, overrides = [], viewer = { level: 'public', userId: null }, presence = 'staff', links = [], mates = [] } = {}) { const saved = { getSetting: visibilityDb.getSetting, getOverrides: mapDb.getOverrides, viewer: visibility.viewer, presenceFor: visibility.presenceFor, steamIdsForUser: mapDb.steamIdsForUser, clanMatesOn: mapDb.clanMatesOn, } const asked = { clans: 0 } visibilityDb.getSetting = async (key) => (key in stored ? stored[key] : null) mapDb.getOverrides = async () => overrides visibility.viewer = async () => viewer visibility.presenceFor = async () => presence mapDb.steamIdsForUser = async () => links mapDb.clanMatesOn = async () => { asked.clans += 1 return mates } t.after(() => { visibilityDb.getSetting = saved.getSetting mapDb.getOverrides = saved.getOverrides visibility.viewer = saved.viewer visibility.presenceFor = saved.presenceFor mapDb.steamIdsForUser = saved.steamIdsForUser mapDb.clanMatesOn = saved.clanMatesOn }) return asked } const LIVE = { mapKey: '3000.1234.1', world: [{ kind: 'cargo', x: 10, z: 20 }], events: [{ kind: 'zone', runId: '4', x: 0, z: 0, radius: 50 }], players: [ { steamId: '7001', name: 'Me', x: 1, z: 1, sleeping: false, online: true }, { steamId: '7002', name: 'Mate', x: 2, z: 2, sleeping: false, online: true }, { steamId: '7003', name: 'Asleep mate', x: 3, z: 3, sleeping: true, online: false }, { steamId: '7004', name: 'Stranger', x: 4, z: 4, sleeping: false, online: true }, { steamId: '7005', name: 'My alt, asleep', x: 5, z: 5, sleeping: true, online: false }, ], bases: [{ kind: 'tc', x: 9, z: 9 }], } 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 }) const acc = await map.access({}, 'main') assert.equal(acc.layers.world.visible, true) assert.equal(acc.layers.events.visible, true) assert.equal(acc.layers.players.visible, false) assert.equal(acc.layers.bases.visible, false) assert.equal(acc.mates.visible, false, 'an anonymous viewer has no accounts to be shown') }) test('a stored word nobody recognises narrows: a layer to staff, the mates switch to off', async (t) => { settings(t, { stored: { 'map.layer.world.audience': 'everyone', 'map.mates': 'yes' } }) const fleet = await map.fleet() assert.equal(fleet.world, 'staff') assert.equal(fleet.mates, false) }) test('an override wins over the fleet for its server only', async (t) => { settings(t, { overrides: [{ setting: 'map.layer.bases.audience', value: 'public' }, { setting: 'map.mates', value: 'off' }] }) const eff = await map.forServer('pve') assert.equal(eff.bases, 'public') assert.equal(eff.mates, false) assert.equal(eff.players, 'staff', 'untouched layers inherit') }) test('D113: widening the players layer alone shows an anonymous viewer nothing more', async (t) => { settings(t, { stored: { 'map.layer.players.audience': 'public' }, presence: 'staff' }) const acc = await map.access({}, 'main') assert.equal(acc.layers.players.visible, false) assert.equal(acc.layers.players.audience, 'staff') assert.equal(acc.layers.players.cappedByPresence, true) assert.equal(map.project(LIVE, acc).players, undefined) }) test('D113: widening both the layer and presence does show them', async (t) => { settings(t, { stored: { 'map.layer.players.audience': 'public' }, presence: 'public' }) const acc = await map.access({}, 'main') assert.equal(acc.layers.players.visible, true) assert.equal(acc.layers.players.cappedByPresence, undefined) assert.equal(map.project(LIVE, acc).players.length, LIVE.players.length) }) test('a hidden layer is absent from the answer, not an empty array', async (t) => { settings(t) const out = map.project(LIVE, await map.access({}, 'main')) assert.ok(Array.isArray(out.world)) assert.ok(Array.isArray(out.events)) for (const hidden of ['players', 'bases', 'mates', 'playersTruncated', 'basesTruncated']) { assert.equal(Object.prototype.hasOwnProperty.call(out, hidden), false, `${hidden} must not be on the wire`) } }) test('staff get every layer', async (t) => { settings(t, { viewer: { level: 'staff', userId: 1 } }) const out = map.project({ ...LIVE, basesTruncated: true }, await map.access({}, 'main')) assert.equal(out.players.length, 5) assert.equal(out.bases.length, 1) assert.equal(out.basesTruncated, true) }) test('own dot and mates: own accounts asleep or awake, ONLINE clan mates, and nobody else', async (t) => { settings(t, { viewer: { level: 'signed_in', userId: 9 }, links: ['7001', '7005'], mates: ['7001', '7002', '7003', '7005'], }) const acc = await map.access({}, 'main') assert.equal(acc.layers.players.visible, false, 'a player is below the players layer') assert.equal(acc.mates.visible, true) const out = map.project(LIVE, acc, await map.mateIdsFor('main', acc)) const ids = out.mates.map((m) => m.steamId).sort() assert.deepStrictEqual(ids, ['7001', '7002', '7005']) assert.equal(out.mates.find((m) => m.steamId === '7005').self, true, 'the viewer’s own sleeper is theirs') assert.equal(out.mates.find((m) => m.steamId === '7002').self, false) assert.equal(out.players, undefined, 'the players layer itself stays absent') }) test('the mates switch off removes own dot and mates alike', async (t) => { const asked = settings(t, { stored: { 'map.mates': 'off' }, viewer: { level: 'signed_in', userId: 9 }, links: ['7001'], mates: ['7001', '7002'], }) const acc = await map.access({}, 'main') assert.equal(acc.mates.visible, false) const out = map.project(LIVE, acc, await map.mateIdsFor('main', acc)) assert.equal(out.mates, undefined) assert.equal(asked.clans, 0, 'nobody’s clan is even looked up') }) test('D117: a viewer with nothing linked sees no positions, whatever the roster audience is', async (t) => { settings(t, { stored: { 'clans.roster.audience': 'public' }, viewer: { level: 'signed_in', userId: 9 }, links: [], }) const acc = await map.access({}, 'main') assert.equal(acc.mates.visible, false) assert.equal(acc.mates.linked, false) }) test('a setting that cannot be read hides every layer', async (t) => { settings(t) visibility.viewer = async () => { throw new Error('pool exhausted') } const acc = await map.access({}, 'main') for (const layer of map.LAYERS) assert.equal(acc.layers[layer].visible, false) }) test('the admin write refuses a word it does not know, and writes nothing on a dry run', async (t) => { settings(t) const writes = [] const saved = { setSetting: visibilityDb.setSetting, setOverride: mapDb.setOverride } visibilityDb.setSetting = async (...a) => writes.push(['fleet', ...a]) mapDb.setOverride = async (...a) => writes.push(['server', ...a]) t.after(() => Object.assign(visibilityDb, { setSetting: saved.setSetting }) && Object.assign(mapDb, { setOverride: saved.setOverride })) assert.equal((await map.update({ fleet: { players: 'everyone' } })).status, 400) assert.equal((await map.update({ fleet: { mates: 'on' } })).status, 400, 'mates is a boolean') assert.equal((await map.update({ servers: { nope: { world: 'staff' } } }, null, async () => false)).status, 404) assert.deepStrictEqual(await map.update({ fleet: { world: 'staff' } }, null, undefined, { dryRun: true }), { ok: true, changed: {} }) assert.equal(writes.length, 0) const done = await map.update({ fleet: { players: 'signed_in', mates: false }, servers: { pve: { players: 'public', mates: null } } }, { id: 3 }) assert.equal(done.ok, true) assert.deepStrictEqual(writes, [ ['fleet', 'map.layer.players.audience', 'signed_in', 3], ['fleet', 'map.mates', 'off', 3], ['server', 'pve', 'map.layer.players.audience', 'public', 3], ['server', 'pve', 'map.mates', null, 3], ]) }) 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) assert.equal(row.height, 3250) assert.equal(row.sha256, null) assert.equal(row.source, 'none') assert.equal(row.derivation, map.DERIVATION_VERSION) }) test('derive: the picture’s own size wins, and nothing unsafe reaches a style', () => { const row = map.derive('main', { mapKey: '3000.1234.1', source: 'companion', sha256: 'AB'.repeat(32), width: 2500, height: 2500, worldSize: 3000, oceanMargin: 500, gridCells: 20, gridCellSize: 150, background: 'red;x:1', monuments: [{ value: 'harbor_1#2', kind: 'harbor_1', label: 'Harbor', x: 1, z: 2 }, { kind: 'broken', x: 'nope', z: 1 }], }) assert.equal(row.sha256, 'ab'.repeat(32), 'hashes are compared lower-case') assert.equal(row.background, null) assert.equal(row.monuments.length, 1) }) test('a render’s stall is estimated from the measured one, scaled by area', () => { assert.equal(map.renderStallSeconds(3000), 9) assert.equal(map.renderStallSeconds(4500), 14) assert.equal(map.renderStallSeconds(null), 9, 'unknown size estimates the measured map') }) // ── The picture ─────────────────────────────────────────────────────────── const SERVER = { id: 'main', baseUrl: 'http://main:1', token: 't' } const HELLO = { bootId: 'boot-1', wipeId: 'w-1', seed: 1234, worldSize: 3000, worldReady: true, protocol: 11 } function picture(bytes, { source = 'companion', mapKey = '3000.1234.1', chunkBytes = 4 } = {}) { const sha = crypto.createHash('sha256').update(bytes).digest('hex') const info = { kind: 'map.info', mapKey, source, sha256: sha, bytes: bytes.length, width: 2500, height: 2500, worldSize: 3000, oceanMargin: 500, gridCells: 20, gridCellSize: 150, background: '#0B3B4A', chunks: Math.ceil(bytes.length / chunkBytes), monuments: [], } const chunk = (n) => ({ kind: 'map.chunk', chunk: n, data: bytes.subarray(n * chunkBytes, (n + 1) * chunkBytes).toString('base64') }) return { info, chunk, sha } } function bridge(t, { info, chunk, stored = null } = {}) { mapImages._reset() const calls = { info: 0, chunks: [], put: [], geometry: [] } const saved = { mapInfo: client.mapInfo, mapChunk: client.mapChunk, getMeta: mapDb.getMeta, putImage: mapDb.putImage, putGeometry: mapDb.putGeometry } client.mapInfo = async () => { calls.info += 1 return typeof info === 'function' ? info(calls.info) : { ok: true, status: 'ok', data: info } } client.mapChunk = async (server, q) => { calls.chunks.push(q) return { ok: true, status: 'ok', data: chunk(q.n, q) } } mapDb.getMeta = async () => stored mapDb.putImage = async (row) => calls.put.push(row) mapDb.putGeometry = async (row) => calls.geometry.push(row) t.after(() => { Object.assign(client, { mapInfo: saved.mapInfo, mapChunk: saved.mapChunk }) Object.assign(mapDb, { getMeta: saved.getMeta, putImage: saved.putImage, putGeometry: saved.putGeometry }) mapImages._reset() }) return calls } test('a new map is fetched in slices, checked against its hash, and stored whole', async (t) => { const p = picture(Buffer.from('a jpeg, honestly')) const calls = bridge(t, { info: p.info, chunk: p.chunk }) const result = await mapImages.run(SERVER) assert.equal(result.outcome, 'fetched') assert.equal(calls.chunks.length, p.info.chunks) assert.equal(calls.put.length, 1) assert.equal(calls.put[0].bytes.toString(), 'a jpeg, honestly') assert.equal(calls.put[0].sha256, p.sha) }) test('bytes that do not match their hash are never stored', async (t) => { const p = picture(Buffer.from('the real picture')) const calls = bridge(t, { info: p.info, chunk: (n) => ({ ...p.chunk(n), data: Buffer.from('xxxx').toString('base64') }) }) const result = await mapImages.run(SERVER) assert.equal(result.ok, false) assert.equal(calls.put.length, 0) }) test('a fetch that straddles a map change is abandoned whole and retried soon, not spliced', async (t) => { const p = picture(Buffer.from('first map picture')) const calls = bridge(t, { info: p.info, chunk: (n) => (n === 0 ? p.chunk(0) : { kind: 'map.error', reason: 'stale', message: 'moved' }), }) const result = await mapImages.run(SERVER) assert.equal(result.ok, false) assert.equal(result.retrySoon, true) assert.equal(calls.put.length, 0) }) test('the stored picture of this map is not fetched again', async (t) => { const p = picture(Buffer.from('same')) const calls = bridge(t, { info: p.info, chunk: p.chunk, stored: { mapKey: p.info.mapKey, sha256: p.sha, source: 'companion', derivation: map.DERIVATION_VERSION }, }) assert.equal((await mapImages.run(SERVER)).outcome, 'current') assert.equal(calls.chunks.length, 0) assert.equal(calls.geometry.length, 0) }) test('a game with no picture keeps the one already stored for the same map', async (t) => { const calls = bridge(t, { info: { kind: 'map.info', mapKey: '3000.1234.1', source: 'none', worldSize: 3000, oceanMargin: 500, gridCells: 20, gridCellSize: 150, monuments: [] }, stored: { mapKey: '3000.1234.1', sha256: 'f'.repeat(64), source: 'rendered', width: 2500, height: 2500 }, }) assert.equal((await mapImages.run(SERVER)).outcome, 'current') assert.equal(calls.put.length, 0, 'the picture row is not replaced') assert.equal(calls.geometry[0].source, 'rendered') }) test('a different map with no picture stores geometry so the layers can be drawn', async (t) => { const calls = bridge(t, { info: { kind: 'map.info', mapKey: '3500.9.1', source: 'none', worldSize: 3500, oceanMargin: 500, gridCells: 23, gridCellSize: 152.174, monuments: [] }, stored: { mapKey: '3000.1234.1', sha256: 'f'.repeat(64), source: 'companion' }, }) assert.equal((await mapImages.run(SERVER)).outcome, 'none') assert.equal(calls.put.length, 1) assert.equal(calls.put[0].bytes, undefined) }) test('one fetch per server at a time', async (t) => { const p = picture(Buffer.from('slow')) let release const gate = new Promise((resolve) => { release = resolve }) bridge(t, { info: async () => { await gate return { ok: true, status: 'ok', data: p.info } }, chunk: p.chunk, }) const first = mapImages.run(SERVER) assert.equal((await mapImages.run(SERVER)).outcome, 'busy') release() assert.equal((await first).outcome, 'fetched') }) test('observe asks only a connected, ready plugin that knows the map verbs, and only when something moved', async (t) => { const p = picture(Buffer.from('pic')) const calls = bridge(t, { info: p.info, chunk: p.chunk }) mapImages.observe(SERVER, { ...HELLO, protocol: 10 }) mapImages.observe(SERVER, { ...HELLO, worldReady: false }) await new Promise((r) => setImmediate(r)) assert.equal(calls.info, 0) mapImages.observe(SERVER, HELLO) await new Promise((r) => setTimeout(r, 10)) assert.equal(calls.info, 1) mapImages.observe(SERVER, HELLO) await new Promise((r) => setTimeout(r, 10)) assert.equal(calls.info, 1, 'the same boot, wipe and map is not asked about twice') mapImages.observe(SERVER, { ...HELLO, wipeId: 'w-2' }) await new Promise((r) => setTimeout(r, 10)) assert.equal(calls.info, 2, 'a wipe is a reason to ask') }) // ── What moves ──────────────────────────────────────────────────────────── test('D111: any number of viewers inside the window cost one ask of the game', async (t) => { mapLive._reset() let asks = 0 const saved = client.mapLive client.mapLive = async () => { asks += 1 await new Promise((r) => setTimeout(r, 5)) return { ok: true, status: 'ok', data: { kind: 'map.live', ...LIVE } } } t.after(() => { client.mapLive = saved mapLive._reset() }) let now = 1000 const clock = () => now const answers = await Promise.all(Array.from({ length: 12 }, () => mapLive.live(SERVER, clock))) assert.equal(asks, 1, 'concurrent viewers share the ask in flight') assert.ok(answers.every((a) => a.ok)) now += map.LIVE_CACHE_MS - 1 await mapLive.live(SERVER, clock) assert.equal(asks, 1, 'inside the window') now += 2 await mapLive.live(SERVER, clock) assert.equal(asks, 2, 'after it') }) test('a game that does not answer is asked once per window too', async (t) => { mapLive._reset() let asks = 0 const saved = client.mapLive client.mapLive = async () => { asks += 1 return { ok: false, status: 'http-503' } } t.after(() => { client.mapLive = saved mapLive._reset() }) const clock = () => 5000 assert.equal((await mapLive.live(SERVER, clock)).ok, false) assert.equal((await mapLive.live(SERVER, clock)).ok, false) assert.equal(asks, 1) }) // ── The picture's route ─────────────────────────────────────────────────── function fakeRes() { const res = { headers: {}, statusCode: 200, body: null, vary: () => res } res.set = (k, v) => { res.headers[k] = v return res } res.status = (c) => { res.statusCode = c return res } res.json = (b) => { res.body = b return res } res.send = (b) => { res.body = b return res } return res } test('the picture is immutable under its hash, and a stale hash is a 404 that nothing caches', async (t) => { const controller = require('../router/public/rust.controller') const servers = require('../model/servers/servers.model') const sha = 'a'.repeat(64) const saved = { getPublic: servers.getPublic, getBytes: mapDb.getBytes } servers.getPublic = async (id) => (id === 'main' ? { id } : null) mapDb.getBytes = async (id, v) => (v === sha ? Buffer.from([0xff, 0xd8, 0xff]) : null) t.after(() => { servers.getPublic = saved.getPublic mapDb.getBytes = saved.getBytes }) const hit = fakeRes() await controller.getMapImage({ params: { id: 'main' }, query: { v: sha } }, hit) assert.equal(hit.statusCode, 200) assert.equal(hit.headers['Cache-Control'], 'public, max-age=31536000, immutable') assert.equal(hit.headers['Content-Type'], 'image/jpeg') const stale = fakeRes() await controller.getMapImage({ params: { id: 'main' }, query: { v: 'b'.repeat(64) } }, stale) assert.equal(stale.statusCode, 404) assert.equal(stale.headers['Cache-Control'], 'no-store') const junk = fakeRes() await controller.getMapImage({ params: { id: 'main' }, query: { v: '../../etc' } }, junk) assert.equal(junk.statusCode, 404) })