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
613 lines
26 KiB
JavaScript
613 lines
26 KiB
JavaScript
// ── 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)
|
||
// 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')
|
||
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,
|
||
listSettings: visibilityDb.listSettings,
|
||
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)
|
||
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
|
||
mapDb.steamIdsForUser = async () => links
|
||
mapDb.clanMatesOn = async () => {
|
||
asked.clans += 1
|
||
return mates
|
||
}
|
||
t.after(() => {
|
||
visibilityDb.getSetting = saved.getSetting
|
||
visibilityDb.listSettings = saved.listSettings
|
||
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, markers: {} })
|
||
|
||
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],
|
||
])
|
||
})
|
||
|
||
// ── 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)
|
||
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)
|
||
})
|