// ── Rust's own world events (PLAN_REDESIGNS §14.3.1, protocol 14) ────────── // // Each test is one way the feature could look right and be wrong: // // the catalogue drifts from the protocol's 24 types, and a new one is public // the four credit rules (D372) disagree with their own table — or with the // count, so the line says Alice and Rotorbane counts Bob // a name reaches a viewer below the presence audience (D376), a Steam id or a // position reaches anybody but staff (D353) // a grid is clamped onto the edge of the map for something off it // the tally's two new counts are dropped on the floor // a trigger declares a variable its emitter never fills, or names a Steam id // the call step takes a position for an event that has no route to one (D378) 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 worldEvents = require('../worldEvents') const catalogue = require('../catalogue') const { TRIGGERS } = require('../engagement/triggers') const A = '76561198000000001' const B = '76561198000000002' const C = '76561198000000003' const NOW = Date.now() /** A 6000 map of the game's own 41 cells of 146.3 m — the rigs' geometry. */ const GEOMETRY = { worldSize: 6000, gridCells: 41, gridCellSize: 146.34146341463415 } const world = (event, over = {}) => ({ kind: 'world.event', type: 'event', t: NOW, serverId: 'main', wipeId: 'w1', event, ...over }) /** Bob did 61 %, Alice the crippling hit — D372's own example. */ const downed = (over = {}) => world('heli.downed', { x: 812, z: -340.5, credit: { blow: A, last: B, most: B, damagers: [{ steamId: B, damage: 6120 }, { steamId: A, damage: 3880 }] }, names: { [A]: 'Alice', [B]: 'Bob' }, ...over, }) // ── The catalogue ────────────────────────────────────────────────────────── test('the types are exactly the 24 the protocol defines, and an unknown one is staff only', () => { // Sourced from docs/rust-link/PROTOCOL.md §20.5 (PLAN_REDESIGNS §14.3.1, D379). const PROTOCOL_14 = [ 'heli.arrived', 'heli.left', 'heli.downed', 'bradley.spawned', 'bradley.destroyed', 'cargo.arrived', 'cargo.docking', 'cargo.docked', 'cargo.undocked', 'cargo.left', 'chinook.arrived', 'chinook.dropped', 'chinook.downed', 'airdrop.called', 'airdrop.landed', 'airdrop.looted', 'crate.hack.started', 'crate.unlocked', 'crate.looted', 'vendor.arrived', 'vendor.left', 'satellite.crashed', 'deepsea.opened', 'deepsea.closed', ] assert.deepStrictEqual([...worldEvents.TYPE_IDS].sort(), [...PROTOCOL_14].sort()) assert.ok(catalogue.isPublic('world.event')) // Default deny, per type: a newer plugin's type is not served to the public. const future = world('meteor.landed', { x: 1, z: 1, steamId: A, name: 'Alice' }) assert.strictEqual(worldEvents.shapeFor(future, { presence: true }), null) assert.strictEqual(worldEvents.shapeFor(future, { staff: true }).event, 'meteor.landed') }) // ── Credit (D372, D379) ──────────────────────────────────────────────────── test('each of the four credit rules picks what its table says', () => { assert.deepStrictEqual(worldEvents.credited(downed(), 'blow'), [A]) assert.deepStrictEqual(worldEvents.credited(downed(), 'last'), [B]) assert.deepStrictEqual(worldEvents.credited(downed(), 'most'), [B]) assert.deepStrictEqual(worldEvents.credited(downed(), 'all'), [B, A]) // An unknown rule is the default, the killing blow. assert.deepStrictEqual(worldEvents.credited(downed(), 'loudest'), [A]) }) test('with no killing blow, the killing-blow rule falls back to the last hit', () => { const noBlow = downed({ credit: { last: B, most: A, damagers: [{ steamId: A, damage: 9 }, { steamId: B, damage: 1 }] } }) assert.deepStrictEqual(worldEvents.credited(noBlow, 'blow'), [B]) assert.deepStrictEqual(worldEvents.credited(noBlow, 'most'), [A]) }) test('a death nobody touched credits nobody under any rule, and counts nothing', () => { const nobody = world('bradley.destroyed', { where: 'road', x: 0, z: 0, credit: { damagers: [] } }) for (const rule of worldEvents.CREDIT_RULE_IDS) { assert.deepStrictEqual(worldEvents.credited(nobody, rule), [], rule) assert.deepStrictEqual(worldEvents.killsOf(nobody, rule), [], rule) } }) test('the count and the line credit the same players, under every rule', () => { for (const rule of worldEvents.CREDIT_RULE_IDS) { const line = worldEvents.shapeFor(downed(), { presence: true, rule }).credited.map((c) => c.name) const count = worldEvents.killsOf(downed(), rule).map((k) => k.name) assert.deepStrictEqual(line, count, rule) } // Each kill is the column the type counts toward: Rotorbane for both helicopters, Tankbreaker for Bradley. assert.strictEqual(worldEvents.killsOf(downed(), 'blow')[0].field, 'heliKills') assert.strictEqual(worldEvents.killsOf(world('chinook.downed', { credit: { blow: A } }), 'blow')[0].field, 'heliKills') assert.strictEqual(worldEvents.killsOf(world('bradley.destroyed', { where: 'launch_site', credit: { blow: A } }), 'blow')[0].field, 'apcKills') // Nothing else counts. assert.deepStrictEqual(worldEvents.killsOf(world('heli.arrived'), 'blow'), []) }) test('a junk id in a credit block is never credited', () => { const junk = downed({ credit: { blow: 'not-a-steam-id', last: '123', damagers: [{ steamId: '{}', damage: 1 }] } }) for (const rule of worldEvents.CREDIT_RULE_IDS) assert.deepStrictEqual(worldEvents.credited(junk, rule), [], rule) }) // ── Who may read what (D353, D376) ───────────────────────────────────────── test('below the presence audience a line names nobody, and carries no position', () => { const signedOut = worldEvents.shapeFor(downed(), { presence: false, rule: 'blow', geometry: GEOMETRY }) assert.strictEqual(signedOut.event, 'heli.downed') assert.strictEqual(signedOut.grid, worldEvents.gridOf(GEOMETRY, 812, -340.5)) assert.strictEqual(signedOut.creditedCount, 1) assert.strictEqual(signedOut.credited, undefined) for (const field of ['x', 'z', 'credit', 'names', 'steamId', 'name']) assert.strictEqual(signedOut[field], undefined, field) const hack = worldEvents.shapeFor(world('crate.hack.started', { where: 'oilrig_large', steamId: A, name: 'Alice', x: 1, z: 1 }), { presence: false }) assert.strictEqual(hack.name, undefined) assert.strictEqual(hack.steamId, undefined) assert.strictEqual(hack.where, 'oilrig_large') }) test('the presence audience reads names but no Steam id; staff read both, and the position', () => { const member = worldEvents.shapeFor(downed(), { presence: true, rule: 'all' }) assert.deepStrictEqual(member.credited, [{ name: 'Bob' }, { name: 'Alice' }]) assert.strictEqual(member.x, undefined) const staff = worldEvents.shapeFor(downed(), { staff: true, presence: true, rule: 'all' }) assert.deepStrictEqual(staff.credited, [{ name: 'Bob', steamId: B }, { name: 'Alice', steamId: A }]) assert.strictEqual(staff.x, 812) assert.ok(staff.credit, 'staff see the whole credit block') const loot = world('airdrop.looted', { steamId: A, name: 'Alice', x: 5, z: 5 }) assert.deepStrictEqual( [worldEvents.shapeFor(loot, { presence: true }).name, worldEvents.shapeFor(loot, { presence: true }).steamId], ['Alice', undefined], ) assert.strictEqual(worldEvents.shapeFor(loot, { staff: true, presence: true }).steamId, A) }) test('a line with nobody in it reads the same for everybody', () => { const docking = world('cargo.docking', { harbour: 'harbor_1', harbourName: 'Harbor', x: 100, z: 100 }) const a = worldEvents.shapeFor(docking, { presence: false, geometry: GEOMETRY }) const b = worldEvents.shapeFor(docking, { presence: true, geometry: GEOMETRY }) assert.deepStrictEqual(a, b) assert.strictEqual(a.harbourName, 'Harbor') }) // ── The grid ─────────────────────────────────────────────────────────────── test('the grid is the game’s, and nothing off the map gets one', () => { // The north-west corner is A0; the 41st column is AO. assert.strictEqual(worldEvents.gridOf(GEOMETRY, -2999, 2999), 'A0') assert.strictEqual(worldEvents.gridOf(GEOMETRY, 2999, -2999), 'AO40') // The Deep Sea and its ghost ships sit thousands of metres off the map. assert.strictEqual(worldEvents.gridOf(GEOMETRY, -5200, 0), null) // No geometry yet, or no position: no grid, rather than a guess. assert.strictEqual(worldEvents.gridOf(null, 0, 0), null) assert.strictEqual(worldEvents.gridOf(GEOMETRY, undefined, 0), null) }) // ── Ingest ───────────────────────────────────────────────────────────────── test('a downed helicopter counts toward Rotorbane for whom the server’s rule credits', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', { id: 1, kind: 'world.event', frame: downed() }, { id: 'main', creditRule: 'all' }) const adds = rec.touching('rust_player_wipe_stats').filter((s) => s.sql.includes('heli_kills')) assert.strictEqual(adds.length, 2) assert.deepStrictEqual(adds.map((s) => s.params[2]).sort(), [A, B].sort()) // Under the default rule, only the killing blow. const rec2 = withRecorder() await apply('main', { id: 2, kind: 'world.event', frame: downed() }, { id: 'main' }) const once = rec2.touching('rust_player_wipe_stats').filter((s) => s.sql.includes('heli_kills')) assert.strictEqual(once.length, 1) assert.strictEqual(once[0].params[2], A) }) test('a Bradley counts toward Tankbreaker, and an arrival counts nothing', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', { id: 1, kind: 'world.event', frame: world('bradley.destroyed', { where: 'launch_site', credit: { blow: C }, names: { [C]: 'Cleo' } }) }, { id: 'main' }) const apc = rec.touching('rust_player_wipe_stats').filter((s) => s.sql.includes('apc_kills')) assert.strictEqual(apc.length, 1) assert.strictEqual(apc[0].params[2], C) const rec2 = withRecorder() await apply('main', { id: 2, kind: 'world.event', frame: world('heli.arrived', { x: 1, z: 1 }) }, { id: 'main' }) assert.strictEqual(rec2.touching('rust_player_wipe_stats').length, 0) }) test('a downed one is stored with the rule that credited it; an arrival is stored as sent (W4)', async () => { const { apply } = require('../ingest') const storedRaw = async (frame) => { const rec = withRecorder() await apply('main', { id: 1, kind: 'world.event', frame }, { id: 'main', creditRule: 'all' }) const insert = rec.touching('rust_events').find((s) => /INSERT/i.test(s.sql)) return JSON.parse(insert.params.find((p) => typeof p === 'string' && p.startsWith('{'))) } assert.strictEqual((await storedRaw(downed())).creditRule, 'all') assert.strictEqual((await storedRaw(world('heli.arrived', { x: 1, z: 1 }))).creditRule, undefined) }) test('the tally’s crates hacked and drops looted are added up (D356)', 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', cratesHacked: 2, dropsLooted: 1 } }) const titleAdd = rec.touching('rust_player_wipe_stats').find((s) => s.sql.includes('crates_hacked')) assert.ok(titleAdd, 'the tally reached the title columns') assert.ok(titleAdd.sql.includes('drops_looted')) const cols = titleAdd.sql.match(/\(server_id, wipe_id, steam_id, ([^)]+)\)/)[1].split(', ') assert.strictEqual(titleAdd.params[3 + cols.indexOf('crates_hacked')], 2) assert.strictEqual(titleAdd.params[3 + cols.indexOf('drops_looted')], 1) }) // ── The feed ─────────────────────────────────────────────────────────────── test('the feed reads each world event for its viewer, with the server’s rule and grid', async () => { const stored = downed() withRecorder((sql) => { if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'world.event', t: NOW, wipeId: 'w1', steamId: null, raw: JSON.stringify(stored) }] if (sql.includes('FROM rust_servers')) return [{ id: 'main', name: 'Main', creditRule: 'most', enabled: 1 }] 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.frame.creditRule, 'most') assert.strictEqual(signedOut.frame.credited, undefined) assert.strictEqual(signedOut.frame.grid, worldEvents.gridOf(GEOMETRY, 812, -340.5)) const [member] = await events.recent({ serverId: 'main', presence: true }) assert.deepStrictEqual(member.frame.credited, [{ name: 'Bob' }]) }) test('a line keeps the rule it was stored with after the server’s rule changes (W4)', async () => { const stamped = downed({ creditRule: 'all' }) withRecorder((sql) => { if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'world.event', t: NOW, wipeId: 'w1', steamId: null, raw: JSON.stringify(stamped) }] if (sql.includes('FROM rust_servers')) return [{ id: 'main', name: 'Main', creditRule: 'most', enabled: 1 }] return [] }) const events = require('../model/events/events.model') const [member] = await events.recent({ serverId: 'main', presence: true }) assert.strictEqual(member.frame.creditRule, 'all') assert.strictEqual(member.frame.creditedCount, 2) assert.deepStrictEqual(member.frame.credited.map((c) => c.name).sort(), ['Alice', 'Bob']) }) // ── Triggers (D355) ──────────────────────────────────────────────────────── test('one public trigger per type, none of them declaring a Steam id', () => { const world = TRIGGERS.filter((t) => t.id.startsWith('rust.world.')) assert.deepStrictEqual(world.map((t) => t.id.slice('rust.world.'.length)).sort(), [...worldEvents.TYPE_IDS].sort()) for (const t of world) { assert.strictEqual(t.ceiling, 'everyone', t.id) assert.strictEqual(t.subjectKey, 'subject', t.id) assert.ok(t.variables.some((v) => v.name === 'subject'), t.id) assert.ok(!t.variables.some((v) => /steam/i.test(v.name)), `${t.id} declares a Steam id`) } }) test('a world event fires its trigger with the variables it declares, and a headline', async (t) => { const { ctx } = withRecorder((sql) => (sql.includes('FROM rust_map_images') ? [{ serverId: 'main', ...GEOMETRY }] : [])) const emit = require('../engagement/emit') emit.reset() const server = { id: 'main', name: 'Main', creditRule: 'all' } await emit.onEvent(server, { id: 1, kind: 'world.event', frame: downed() }) await emit.onEvent(server, { id: 2, kind: 'world.event', frame: world('crate.hack.started', { where: 'oilrig_small', steamId: A, name: 'Alice', x: 1, z: 1 }) }) await emit.onEvent(server, { id: 3, kind: 'world.event', frame: world('meteor.landed') }) const calls = ctx.events.emit.calls assert.strictEqual(calls.length, 2, 'an unknown type fires nothing') const [heliId, heli] = calls[0] assert.strictEqual(heliId, 'rust.world.heli.downed') assert.strictEqual(heli.data.player, 'Bob and Alice') assert.strictEqual(heli.data.credited, 2) assert.strictEqual(heli.data.subject, 'main:heli.downed') assert.ok(heli.data.grid) assert.match(heli.data.intro, /downed by Bob and Alice at /) const [crateId, crate] = calls[1] assert.strictEqual(crateId, 'rust.world.crate.hack.started') assert.strictEqual(crate.data.subject, 'main:crate.hack.started:oilrig_small') assert.match(crate.data.intro, /Alice started hacking the locked crate on Small Oil Rig on Main/) // Every variable a trigger fills is one it declares. for (const [id, envelope] of calls) { const declared = new Set(TRIGGERS.find((x) => x.id === id).variables.map((v) => v.name)) for (const name of Object.keys(envelope.data)) assert.ok(declared.has(name), `${id} fills ${name}, which it does not declare`) } }) test('an old world event tells nobody (D63)', async () => { const { ctx } = withRecorder() const emit = require('../engagement/emit') emit.reset() await emit.onEvent({ id: 'main', name: 'Main' }, { id: 1, kind: 'world.event', frame: world('heli.arrived', { t: NOW - 3 * 3600 * 1000 }) }) assert.strictEqual(ctx.events.emit.calls.length, 0) }) // ── The call step (D354, D367, D378) ─────────────────────────────────────── function stubCall(t, reply) { const client = require('../sidecarClient') const serversDb = require('../model/servers/servers.db') const saved = { worldCall: client.worldCall, getServer: serversDb.getServer } const calls = [] serversDb.getServer = async (id) => (id === 'main' ? { id: 'main', name: 'Main', enabled: 1, sidecarBaseUrl: 'http://main:1' } : null) client.worldCall = async (server, body) => { calls.push({ server: server.id, body }) return typeof reply === 'function' ? reply(body) : reply } t.after(() => { client.worldCall = saved.worldCall serversDb.getServer = saved.getServer }) return calls } const callStep = () => require('../eventWorldCall').ACTIONS[0] const ok = (data) => ({ ok: true, status: 'ok', data }) test('the call step is kept, priced per event, and mirrors the plugin’s list', () => { const step = callStep() assert.strictEqual(step.id, 'rust.world.call') assert.strictEqual(step.reversible, 'none') assert.strictEqual(step.risk, 'change') assert.strictEqual(step.revert, undefined) assert.deepStrictEqual(step.cost({ event: 'airdrop', count: 3 }), { 'rust.world.calls': 3 }) assert.deepStrictEqual(step.cost({ event: 'cargo' }), { 'rust.world.calls': 1 }) const { EVENTS } = require('../eventWorldCall') assert.deepStrictEqual(EVENTS.map((e) => e.value).sort(), ['airdrop', 'bradley_road', 'cargo', 'chinook', 'f15', 'patrol_heli', 'satellite', 'travelling_vendor']) assert.deepStrictEqual(EVENTS.filter((e) => e.placeable).map((e) => e.value).sort(), ['airdrop', 'patrol_heli']) }) test('a call is refused in the dry run for a bad event, a bad count, a position it cannot take, or no server', async (t) => { const calls = stubCall(t, ok({ kind: 'world.called', called: 1 })) const step = callStep() const run = (params) => step.perform({ runId: 7, idempotencyKey: '7:1', params, verify: true }) assert.match((await run({ server: 'main', event: 'meteor' })).error, /not one of the world events/) assert.match((await run({ server: 'main', event: 'cargo', count: 2 })).error, /call 1 to 1/) assert.match((await run({ server: 'main', event: 'airdrop', count: 6 })).error, /call 1 to 5/) // D378: the Chinook goes where the game chooses. assert.match((await run({ server: 'main', event: 'chinook', x: 10, z: 10 })).error, /only an airdrop and a patrol helicopter take a position/) assert.match((await run({ event: 'airdrop' })).error, /which server/) assert.ok((await run({ server: 'main', event: 'airdrop', x: 10, z: 10 })).ok) for (const r of [await run({ server: 'main', event: 'meteor' })]) assert.strictEqual(r.retry, false) assert.strictEqual(calls.length, 0, 'the dry run calls nothing') }) test('a call sends the key, the run and the position, and reads the plugin’s answer', async (t) => { const calls = stubCall(t, (body) => ok({ kind: 'world.called', key: body.key, event: body.event, called: body.count, repeat: false })) const step = callStep() const result = await step.perform({ runId: 7, idempotencyKey: '7:2', params: { event: 'airdrop', count: 2, monument: 'main/powerplant_1', offsetX: 10, y: 99 } }) assert.ok(result.ok) assert.deepStrictEqual(result.detail, { event: 'airdrop', called: 2 }) assert.deepStrictEqual(calls[0], { server: 'main', body: { runId: '7', key: '7:2', event: 'airdrop', count: 2, monument: 'powerplant_1', offsetX: 10, offsetZ: 0 }, }) }) test('a refusal the plugin would repeat is not retried; a game that is down is', async (t) => { stubCall(t, (body) => (body.event === 'cargo' ? ok({ kind: 'world.error', reason: 'too-many', message: 'a run calls at most 10 world events on this server' }) : { ok: false, status: 'http-503' })) const step = callStep() const refused = await step.perform({ runId: 7, idempotencyKey: '7:3', params: { server: 'main', event: 'cargo' } }) assert.strictEqual(refused.ok, false) assert.strictEqual(refused.retry, false) assert.match(refused.error, /at most 10/) const down = await step.perform({ runId: 7, idempotencyKey: '7:4', params: { server: 'main', event: 'airdrop' } }) assert.strictEqual(down.ok, false) assert.strictEqual(down.retry, undefined) assert.match(down.error, /has no game connected/) }) test('a Travelling Vendor already out is no failure: the step says so and counts nothing called (W3)', async (t) => { stubCall(t, ok({ kind: 'world.error', reason: 'already-out', message: 'a Travelling Vendor is already out' })) const result = await callStep().perform({ runId: 7, idempotencyKey: '7:6', params: { server: 'main', event: 'travelling_vendor' } }) assert.strictEqual(result.ok, true) assert.deepStrictEqual(result.detail, { event: 'travelling_vendor', called: 0, note: 'a Travelling Vendor is already out on Main, and the game runs one at a time; nothing new was called', }) }) test('a repeat of a key says so and calls nothing new', async (t) => { stubCall(t, ok({ kind: 'world.called', called: 1, repeat: true })) const result = await callStep().perform({ runId: 7, idempotencyKey: '7:5', params: { server: 'main', event: 'patrol_heli' } }) assert.ok(result.ok) assert.strictEqual(result.detail.repeat, true) })