// ── RaidableBases (PLAN_REDESIGNS §11.4, protocol 14) ─────────────────────── // // Each test is one way the feature could look right and be wrong: // // a raider's or a buyer's name reaches a viewer below the presence audience // (D376, D390), or a Steam id, a position or a run's key reaches anybody but staff // a difficulty, profile or base name is anything but the plugin's own (D386): // every name below is invented, so a list written into the code cannot pass // the step asks for a spot, a profile, a base or settings on a server without a // patched raids helper, or refuses a plain spawn a server can still make (D348) // a base the helper made is not the run's resource, or one it did not make is // the dry run's spot check fails the dry run instead of advising (D391) // the grid's wait is not a ten-minute retry, or a refusal the plugin would // repeat is retried // the tally's raids completed is dropped, or Warlord's column and sort are missing // the map's raids layer is missing, not public by default, or carries the key const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') /** Installs a core whose `db.query` records every statement and answers `rows`. */ function withRecorder(rows = []) { const statements = [] const ctx = fakeCtx({ db: { query: (sql, params = []) => { statements.push({ sql, params }) return Promise.resolve(typeof rows === 'function' ? rows(sql, params) : rows) }, pool: {}, }, }) require('../core')._reset() require('../core').init(ctx) return { ctx, statements, touching: (table) => statements.filter((s) => s.sql.includes(table)) } } withRecorder() const rb = require('../raidableBases') const catalogue = require('../catalogue') const { gridOf } = require('../worldEvents') const A = '76561198000000001' const B = '76561198000000002' const NOW = Date.now() const GEOMETRY = { worldSize: 6000, gridCells: 41, gridCellSize: 146.34146341463415 } const frame = (kind, over = {}) => ({ kind, type: 'event', t: NOW, serverId: 'main', wipeId: 'w1', x: 650, z: 2037, level: 7, difficulty: 'Wobbly', pvp: true, base: 'wobble3', ...over }) const raided = (over = {}) => frame('raid.completed', { raiders: [{ steamId: A, name: 'Alice' }, { steamId: B, name: 'Bob' }], raiderCount: 2, ownerId: A, ownerName: 'Alice', id: 'raid-1-1', runId: '7', key: '7:1', ...over, }) // Invented, every one (D386): no edition ships these. const PROFILE = { name: 'Wobbly Keeps', difficulty: 'Wobbly', level: 7, enabled: true, pvp: false, bases: ['wobble1', 'wobble3'], seabed: false, float: false } const REPORT = { loaded: true, version: '3.2.2', premium: true, spawnCall: true, copyPaste: { loaded: true, version: '4.3.0' }, difficulties: [{ name: 'Gentle', level: 0 }, { name: 'Wobbly', level: 7 }], helper: { state: 'patched', version: '0.1.0' }, gridReady: true, profiles: [PROFILE, { name: 'Gentle Huts', difficulty: 'Gentle', level: 0, enabled: true, pvp: true, bases: ['hut9'], seabed: false, float: false }], } // ── Who may read what (D353, D376, D390) ─────────────────────────────────── test('the kinds are classified: four public, the despawn staff’s', () => { for (const kind of ['raid.started', 'raid.completed', 'raid.purchased', 'raid.locked']) assert.ok(catalogue.PUBLIC_KINDS.includes(kind), kind) assert.ok(catalogue.STAFF_KINDS.includes('raid.ended')) assert.ok(!catalogue.PUBLIC_KINDS.includes('raid.ended')) }) test('the public reads a base’s difficulty, base, PvP and grid as the plugin named them, never its position or a run’s key', () => { const out = rb.shapeFor(frame('raid.started', { despawnAt: NOW + 60000, id: 'raid-1-1', runId: '7', key: '7:1', radius: 50 }), { geometry: GEOMETRY }) assert.strictEqual(out.grid, gridOf(GEOMETRY, 650, 2037)) assert.strictEqual(out.difficulty, 'Wobbly') assert.strictEqual(out.level, 7) assert.strictEqual(out.base, 'wobble3') assert.strictEqual(out.pvp, true) assert.strictEqual(out.despawnAt, NOW + 60000) for (const k of ['x', 'z', 'id', 'runId', 'key', 'radius']) assert.strictEqual(out[k], undefined, k) }) test('a completion names its raiders only to the presence audience, and their Steam ids only to staff', () => { const signedOut = rb.shapeFor(raided(), { geometry: GEOMETRY }) assert.strictEqual(signedOut.raiderCount, 2, 'how many is a count, and names nobody') assert.strictEqual(signedOut.names, undefined) assert.strictEqual(signedOut.ownerName, undefined) assert.strictEqual(signedOut.raiders, undefined) assert.strictEqual(signedOut.ownerId, undefined) const member = rb.shapeFor(raided(), { presence: true, geometry: GEOMETRY }) assert.deepStrictEqual(member.names, ['Alice', 'Bob']) assert.strictEqual(member.ownerName, 'Alice') assert.strictEqual(member.raiders, undefined) const staff = rb.shapeFor(raided(), { staff: true, geometry: GEOMETRY }) assert.deepStrictEqual(staff.raiders, [{ steamId: A, name: 'Alice' }, { steamId: B, name: 'Bob' }]) assert.strictEqual(staff.ownerId, A) assert.strictEqual(staff.key, '7:1') assert.strictEqual(staff.x, 650) }) test('a purchase or an admin lock names the player only to the presence audience (D390)', () => { for (const kind of ['raid.purchased', 'raid.locked']) { const f = frame(kind, { steamId: A, name: 'Alice' }) assert.strictEqual(rb.shapeFor(f, {}).name, undefined, kind) assert.strictEqual(rb.shapeFor(f, { presence: true }).name, 'Alice', kind) assert.strictEqual(rb.shapeFor(f, { presence: true }).steamId, undefined, kind) assert.strictEqual(rb.shapeFor(f, { staff: true }).steamId, A, kind) } }) test('the feed reads a stored completion for its viewer, the row’s own Steam id staff’s', async () => { withRecorder((sql) => { if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'raid.completed', t: NOW, wipeId: 'w1', steamId: A, raw: JSON.stringify(raided()) }] if (sql.includes('FROM rust_map_images')) return [{ serverId: 'main', ...GEOMETRY }] return [] }) const events = require('../model/events/events.model') const [signedOut] = await events.recent({ serverId: 'main', presence: false }) assert.strictEqual(signedOut.steamId, null) assert.strictEqual(signedOut.frame.names, undefined) assert.strictEqual(signedOut.frame.raiderCount, 2) assert.strictEqual(signedOut.frame.difficulty, 'Wobbly') assert.ok(signedOut.frame.grid) const [staff] = await events.recent({ serverId: 'main', admin: true }) assert.strictEqual(staff.steamId, A) assert.deepStrictEqual(staff.frame.names, ['Alice', 'Bob']) }) test('a server’s report is read leniently, and the helper’s profiles only as it sent them', () => { assert.deepStrictEqual(rb.reportOf(null), { raidableBases: null, consolePlugins: null }) const read = rb.reportOf({ raidableBases: JSON.stringify(REPORT), console: '{"plugins":false}' }) assert.strictEqual(read.consolePlugins, false) assert.deepStrictEqual(read.raidableBases.difficulties, REPORT.difficulties) assert.deepStrictEqual(read.raidableBases.profiles[0], PROFILE) assert.strictEqual(rb.helperPatched(read), true) const bare = rb.reportOf({ raidableBases: '{"loaded":true,"version":"3.2.2","helper":{"state":"missing"}}', console: null }) assert.strictEqual(bare.raidableBases.profiles, null, 'no helper, no profile list') assert.strictEqual(bare.raidableBases.spawnCall, false) assert.strictEqual(rb.helperPatched(bare), false) assert.deepStrictEqual(rb.reportOf({ raidableBases: 'not json' }).raidableBases, null) }) // ── The step ─────────────────────────────────────────────────────────────── const SERVERS = { main: { id: 'main', name: 'Main', enabled: 1, sidecarBaseUrl: 'http://main:1' }, pve: { id: 'pve', name: 'PvE', enabled: 1, sidecarBaseUrl: 'http://pve:1' }, } /** The step against `main` and `pve`, whose last reports are `reports`, and a sidecar answering `reply` and `check`. */ function stubStep(t, { reports = { main: { raidableBases: REPORT, console: { plugins: true } } }, reply, check } = {}) { const row = (id) => { const r = reports[id] return r ? { serverId: id, name: SERVERS[id].name, raidableBases: JSON.stringify(r.raidableBases ?? null), console: JSON.stringify(r.console ?? null) } : null } withRecorder((sql, params) => { if (sql.includes('JOIN rust_server_state')) return Object.keys(reports).map(row) if (sql.includes('FROM rust_server_state')) return row(params[0]) ? [row(params[0])] : [] if (sql.includes('FROM rust_map_images')) return [{ serverId: 'main', ...GEOMETRY }] return [] }) const client = require('../sidecarClient') const serversDb = require('../model/servers/servers.db') const saved = { raidSpawn: client.raidSpawn, raidCheck: client.raidCheck, getServer: serversDb.getServer } const calls = [] serversDb.getServer = async (id) => SERVERS[id] || null client.raidSpawn = async (server, body) => { calls.push({ what: 'spawn', server: server.id, body }) return typeof reply === 'function' ? reply(body) : reply } client.raidCheck = async (server, body) => { calls.push({ what: 'check', server: server.id, body }) return typeof check === 'function' ? check(body) : check } t.after(() => { client.raidSpawn = saved.raidSpawn client.raidCheck = saved.raidCheck serversDb.getServer = saved.getServer }) return calls } const module_ = () => require('../eventRaidableBases') const spawnStep = () => module_().ACTIONS[0] const ok = (data) => ({ ok: true, status: 'ok', data }) const queued = (over = {}) => ok({ kind: 'raid.queued', key: '7:1', id: 'raid-1-1', route: 'pick', despawns: 'teardown', difficulty: 'Wobbly', level: 7, warnings: [], repeat: false, ...over }) test('the step is ledgered like a zone, priced one base each, and every param has an example', () => { const step = spawnStep() assert.strictEqual(step.id, 'rust.raidbase.spawn') assert.strictEqual(step.reversible, 'ledger') assert.strictEqual(step.risk, 'change') assert.strictEqual(typeof step.revert, 'function') assert.strictEqual(typeof step.reconcile, 'function') assert.deepStrictEqual(step.cost({}), { 'rust.raids': 1 }) for (const p of step.params) assert.ok(p.example !== undefined, p.name) }) test('the pickers list each server’s own names, once each, and nothing for a server that has not said (D386)', async (t) => { const [difficulties, profiles, bases, switches] = module_().OPTION_SOURCES stubStep(t, { reports: { main: { raidableBases: REPORT }, pve: { raidableBases: { ...REPORT, difficulties: [{ name: 'wobbly', level: 7 }, { name: 'Brutal', level: 9 }], profiles: [] } }, }, }) assert.deepStrictEqual(await difficulties.resolve(), [ { value: 'Gentle', label: 'Gentle (level 0) — Main' }, { value: 'Wobbly', label: 'Wobbly (level 7) — Main, PvE' }, { value: 'Brutal', label: 'Brutal (level 9) — PvE' }, ]) assert.deepStrictEqual((await profiles.resolve()).map((p) => p.value), ['Wobbly Keeps', 'Gentle Huts']) assert.deepStrictEqual((await bases.resolve({ q: 'wobble' })).map((b) => b.value), ['wobble1', 'wobble3']) assert.strictEqual(bases.searchable, true) assert.deepStrictEqual((await switches.resolve()).map((s) => s.value), ['on', 'off']) stubStep(t, { reports: { main: { raidableBases: { loaded: false } } } }) assert.deepStrictEqual(await difficulties.resolve(), [], 'nothing is made up for a server without the plugin') }) test('without a patched helper, a spot, a profile, a base or settings are refused; a plain spawn is not (D348, D387)', async (t) => { const run = (params) => spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', ...params }, verify: true }) stubStep(t, { reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, profiles: null }, console: { plugins: true } } } }) for (const params of [{ x: 650, z: 2037 }, { profile: 'Wobbly Keeps' }, { base: 'wobble1' }, { pvp: 'off' }, { npcCount: 3 }, { lock: 'on' }, { despawnMinutes: 60 }]) { const no = await run(params) assert.strictEqual(no.retry, false, JSON.stringify(params)) assert.match(no.error, /RunicGatewayRaids\.cs is not installed on Main/) assert.match(no.error, /despawns the base on its own timer/) } assert.deepStrictEqual(await run({ difficulty: 'Wobbly' }), { ok: true }) assert.deepStrictEqual(await run({}), { ok: true }) stubStep(t, { reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, spawnCall: false, profiles: null }, console: { plugins: false } } } }) const carbon = await run({}) assert.match(carbon.error, /cannot run RaidableBases' console command/) }) test('the dry run refuses names the server does not have, and names that do not fit together', async (t) => { const run = (params) => spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', ...params }, verify: true }) stubStep(t) assert.match((await run({ difficulty: 'Easy' })).error, /"Easy" is not one of RaidableBases' difficulties on Main \(Gentle, Wobbly\)/) assert.match((await run({ profile: 'Nope' })).error, /no profile "Nope"/) assert.match((await run({ base: 'nope1' })).error, /no base "nope1"/) assert.match((await run({ profile: 'Gentle Huts', base: 'wobble1' })).error, /belongs to profile "Wobbly Keeps"/) assert.match((await run({ profile: 'Wobbly Keeps', difficulty: 'Gentle' })).error, /is Wobbly, not Gentle/) assert.match((await run({ heightAdjust: 5 })).error, /a height needs a spot/) assert.match((await run({ x: 650, z: 2037, heightAdjust: 5, height: 30 })).error, /not both/) assert.match((await run({ pvp: 'maybe' })).error, /pvp is on, off or blank/) assert.match((await run({ npcCount: 21 })).error, /0 to 20/) assert.deepStrictEqual(await run({ base: 'WOBBLE3' }), { ok: true }, 'names match as the plugin matches them, ignoring case') stubStep(t, { reports: { main: { raidableBases: { loaded: false } } } }) assert.match((await run({})).error, /RaidableBases is not installed on Main/) stubStep(t, { reports: { main: { raidableBases: { ...REPORT, copyPaste: { loaded: false } } } } }) assert.match((await run({})).error, /CopyPaste is missing/) stubStep(t, { reports: {} }) assert.deepStrictEqual(await run({ x: 650, z: 2037 }), { ok: true }, 'a server that has not said is the plugin’s to answer') }) test('a spot’s check in the dry run is advice: warnings, never a refusal (D208, D388, D391)', async (t) => { const calls = stubStep(t, { check: ok({ kind: 'raid.checked', ok: false, reason: 'spot-blocked', message: 'a player is within 50 m (Alice)', warnings: ['Rock is too large: 1 2 3'] }), }) const advised = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037, base: 'wobble3', pvp: 'off' }, verify: true }) assert.strictEqual(advised.ok, true) assert.deepStrictEqual(advised.detail.warnings, [ 'the spot is refused now (spot-blocked): a player is within 50 m (Alice). It is checked again when the event runs', 'at the spot now: Rock is too large: 1 2 3', ]) assert.deepStrictEqual(calls[0].body, { base: 'wobble3', x: 650, z: 2037 }, 'the check asks the spot and the base, not the settings') assert.ok(!calls.some((c) => c.what === 'spawn'), 'a dry run spawns nothing') stubStep(t, { check: ok({ kind: 'raid.checked', ok: true, warnings: [] }) }) assert.deepStrictEqual(await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037 }, verify: true }), { ok: true }) stubStep(t, { check: { ok: false, status: 'http-503' } }) const down = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037 }, verify: true }) assert.strictEqual(down.ok, true) assert.match(down.detail.warnings[0], /could not be checked now/) }) test('a spawn sends the plugin’s names, the spot and the settings, and a base the helper made is the run’s resource', async (t) => { const calls = stubStep(t, { reply: queued({ route: 'spot', x: 650, z: 2037, base: 'wobble3', profile: 'Wobbly Keeps', warnings: ['Rock is too large: 1 2 3'] }) }) const result = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { monument: 'main/powerplant_1', offsetX: 10, offsetZ: -5, y: 99, heightAdjust: 5, base: 'wobble3', pvp: 'off', npcs: 'on', npcCount: 4, despawnMinutes: 90, lock: 'on' }, }) assert.ok(result.ok) assert.deepStrictEqual(calls[0].body, { key: '7:1', runId: '7', base: 'wobble3', monument: 'powerplant_1', offsetX: 10, offsetZ: -5, heightAdjust: 5, pvp: false, npcs: true, npcCount: 4, despawnMinutes: 90, lock: true, }) assert.deepStrictEqual(result.resources, [ { kind: 'world', ref: 'main:raid-1-1', payload: { serverId: 'main', what: 'raidbase', name: 'wobble3', difficulty: 'Wobbly' } }, ]) assert.strictEqual(result.detail.grid, gridOf(GEOMETRY, 650, 2037)) assert.deepStrictEqual(result.detail.warnings, ['Rock is too large: 1 2 3'], 'a rock is carried into the run log (D388)') assert.strictEqual(result.detail.note, undefined) }) test('a base the helper did not make is not the run’s: no resource, and the run log says it despawns on its own', async (t) => { stubStep(t, { reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, profiles: null }, console: { plugins: true } } }, reply: queued({ id: undefined, route: 'plugin', despawns: 'own-timer' }), }) const result = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', difficulty: 'Wobbly' } }) assert.ok(result.ok) assert.strictEqual(result.resources, undefined) assert.match(result.detail.note, /RaidableBases picks the spot/) assert.match(result.detail.note, new RegExp(module_().OWN_TIMER)) }) test('the grid’s wait is a ten-minute retry; a refusal the plugin would repeat is not retried', async (t) => { stubStep(t, { reply: (body) => ok(body.difficulty === 'Gentle' ? { kind: 'raid.error', reason: 'rb-not-ready', message: 'still building its spawn grid' } : body.difficulty === 'Wobbly' ? { kind: 'raid.error', reason: 'rb-busy', message: 'pasting another base' } : { kind: 'raid.error', reason: 'spot-blocked', message: 'a player is within 50 m', warnings: ['Rock is too large'] }), }) const step = spawnStep() const wait = await step.perform({ runId: 7, idempotencyKey: '7:2', params: { server: 'main', difficulty: 'Gentle' } }) assert.strictEqual(wait.retry, true) assert.strictEqual(wait.retryAfterMs, 10 * 60 * 1000) assert.match(wait.error, /not ready yet/) const busy = await step.perform({ runId: 7, idempotencyKey: '7:3', params: { server: 'main', difficulty: 'Wobbly' } }) assert.strictEqual(busy.retry, undefined, 'a busy spawner retries on core’s ordinary delay') const blocked = await step.perform({ runId: 7, idempotencyKey: '7:4', params: { server: 'main', x: 650, z: 2037 } }) assert.strictEqual(blocked.retry, false) assert.match(blocked.error, /a player is within 50 m \(also: Rock is too large\)/) for (const reason of ['rb-missing', 'copypaste-missing', 'helper-needed', 'helper-unsupported', 'bad-difficulty', 'bad-profile', 'bad-base', 'spot-blocked', 'in-water', 'off-map', 'console-unavailable', 'refused', 'malformed', 'events-disabled']) { assert.ok(module_().PERMANENT.has(reason), reason) } for (const reason of ['rb-not-ready', 'rb-busy']) assert.ok(!module_().PERMANENT.has(reason), reason) }) test('a repeat of a key spawns nothing new, and says so', async (t) => { stubStep(t, { reply: queued({ repeat: true }) }) const again = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main' } }) assert.ok(again.ok) assert.strictEqual(again.detail.repeat, true) assert.match(again.detail.note, /^answered from the first attempt; nothing new was spawned/) assert.strictEqual(again.resources[0].ref, 'main:raid-1-1', 'the repeat still names the run’s base') }) // ── Warlord (D202) ───────────────────────────────────────────────────────── test('the tally’s raids completed is added up', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', { id: 1, kind: 'player.tally', frame: { kind: 'player.tally', type: 'event', t: NOW, serverId: 'main', wipeId: 'w1', steamId: A, name: 'Alice', raidsCompleted: 2 } }) const titleAdd = rec.touching('rust_player_wipe_stats').find((s) => s.sql.includes('raids_completed')) assert.ok(titleAdd, 'the tally reached the title columns') const cols = titleAdd.sql.match(/\(server_id, wipe_id, steam_id, ([^)]+)\)/)[1].split(', ') assert.strictEqual(titleAdd.params[3 + cols.indexOf('raids_completed')], 2) }) test('the leaderboard sorts by raids completed and carries the count on every row', async () => { const rec = withRecorder((sql) => (sql.includes('FROM rust_player_wipe_stats') ? [{ steamId: A, name: 'Alice', kills: 1, raidsCompleted: '3' }] : [])) const events = require('../model/events/events.model') const [row] = await events.leaderboard({ serverId: 'main', wipeId: 'w1', sort: 'raidsCompleted' }) assert.strictEqual(row.raidsCompleted, 3) const board = rec.touching('rust_player_wipe_stats').at(-1) assert.match(board.sql, /SUM\(s\.raids_completed\) AS raidsCompleted/) assert.match(board.sql, /ORDER BY[^]*raids_completed/) }) test('Warlord is a title category that sums raids completed', () => { const titles = require('../model/titles/titles') const stats = titles.STATS || titles._STATS || null if (!stats) return assert.deepStrictEqual(stats.raidscompleted, { label: 'Raid bases completed', title: 'Warlord', source: { sum: 'raids_completed' } }) }) // ── The map's raids layer (D114) ─────────────────────────────────────────── test('the map’s raids layer is public by default, and a run’s key never reaches a viewer', () => { const map = require('../model/map/map.model') assert.ok(map.LAYERS.includes('raids')) assert.strictEqual(map.DEFAULTS.raids, 'public') const layers = {} for (const l of map.LAYERS) layers[l] = { visible: l === 'raids', audience: 'public' } const out = map.project( { raids: [{ x: 1, z: 2, difficulty: 'Wobbly', runId: '7', key: '7:1' }], world: [{ kind: 'cargo' }] }, { layers, mates: { visible: false }, steamIds: [] }, ) assert.deepStrictEqual(out.raids, [{ x: 1, z: 2, difficulty: 'Wobbly', runId: '7' }]) assert.strictEqual(out.world, undefined, 'a hidden layer is absent, not empty') layers.raids.visible = false assert.strictEqual(map.project({ raids: [{ x: 1, z: 2 }] }, { layers, mates: { visible: false }, steamIds: [] }).raids, undefined) })