// ── Calling one of Rust's own world events (protocol 14, PLAN_REDESIGNS §14.3.1) ── // // `rust.world.call`, "Call a world event": a patrol helicopter, a cargo ship, a // Chinook, an airdrop, the Travelling Vendor, road Bradleys, the F-15 or a // satellite crash, now, the way the game's own schedule runs them (D354, D367). // The plugin runs the schedule's `TriggeredEvent` — no console command — so it // works the same on Carbon. // // **Kept (`reversible: 'none'`).** Nothing a call starts is removed when the run // ends: a cargo ship or a drop may already hold players or their loot (D354). // `risk: 'change'` all the same, so an operator switches it on deliberately. // // **A position only where the game has a route to one** (stage 1's spike): the // airdrop's drop point and the patrol helicopter's first destination. The // Chinook drops only at the map's drop zones, and the one route to a chosen // zone borrows the Airfield terminal's shared state, so it takes no position // (D378); the rest go where the game puts them. const client = require('./sidecarClient') const { serverFor, transportError, pluginError } = require('./eventLeases') const { BUDGET_MS, location } = require('./eventWorld') /** The plugin's ceiling on calls per run (`WorldCallsHardMax`); an operator may set it lower. */ const MAX_CALLS_PER_RUN = 10 /** * The events a call may run, mirroring the plugin's list. `max` is how many one * step may call at once: the game runs the cargo ship, the vendor and road * Bradleys (twelve at a time) one at a time. */ const EVENTS = Object.freeze([ { value: 'patrol_heli', label: 'Patrol helicopter', max: 5, placeable: true }, { value: 'airdrop', label: 'Airdrop', max: 5, placeable: true }, { value: 'chinook', label: 'Chinook (drops a locked crate where the game chooses)', max: 5 }, { value: 'cargo', label: 'Cargo ship', max: 1 }, { value: 'bradley_road', label: 'Road Bradleys (several at once)', max: 1 }, { value: 'travelling_vendor', label: 'Travelling Vendor', max: 1 }, { value: 'satellite', label: 'Satellite crash', max: 5 }, { value: 'f15', label: 'F-15 flyover (decoration only)', max: 5 }, ]) /** The plugin's refusals a second attempt would repeat. */ const PERMANENT = new Set([ 'events-disabled', 'malformed', 'bad-event', 'bad-count', 'not-placeable', 'too-many', 'out-of-range', 'no-monument', 'off-map', ]) const BUDGETS = [ { id: 'rust.world.calls', label: 'World events called', unit: 'events', description: "Rust's own world events an event calls — patrol helicopters, cargo ships, airdrops and the rest — each one counted. What a call starts is kept when the run ends.", }, ] const OPTION_SOURCES = [ { // The plugin's own list, mirrored, so the form fills with every server off. id: 'rust.options.world_events', label: 'World events', description: "Rust's own world events a step can call. Only the airdrop and the patrol helicopter take a position; the rest go where the game puts them.", async resolve() { return EVENTS.map((e) => ({ value: e.value, label: e.label })) }, }, ] /** Whether a step gives a position at all: a monument, or x or z. */ function hasPosition(params) { const blank = (v) => v === undefined || v === null || v === '' return !blank(params.monument) || !blank(params.x) || !blank(params.z) } const worldCall = { id: 'rust.world.call', label: 'Call a world event', description: "Calls one of Rust's own world events now — a patrol helicopter, an airdrop, a Chinook, the cargo ship, road Bradleys, the Travelling Vendor, a satellite crash or an F-15 flyover — as the game's own schedule would. An airdrop or a patrol helicopter can be sent to a monument or a point; the rest go where the game puts them. What it starts is kept when the run ends.", risk: 'change', reversible: 'none', version: 1, budgetMs: BUDGET_MS, params: [ { name: 'event', type: 'string', required: true, example: 'airdrop', source: 'rust.options.world_events', description: 'Which world event to call.' }, { name: 'count', type: 'int', required: false, example: 1, description: 'How many at once: 1 to 5, and 1 for the cargo ship, road Bradleys and the vendor. Left blank, 1.' }, { name: 'server', type: 'string', required: false, example: 'main', source: 'rust.options.servers', description: 'Which server. A monument already says; otherwise required.' }, { name: 'monument', type: 'string', required: false, example: 'main/powerplant_1', source: 'rust.options.monuments', description: 'Airdrop and patrol helicopter only: where to send it, by monument. Give this OR x and z, or neither for where the game chooses.' }, { name: 'offsetX', type: 'float', required: false, example: 0, description: "Metres east of the monument's centre (negative is west)." }, { name: 'offsetZ', type: 'float', required: false, example: 0, description: "Metres north of the monument's centre (negative is south)." }, { name: 'x', type: 'float', required: false, example: -604, description: 'Airdrop and patrol helicopter only: world x, instead of a monument.' }, { name: 'z', type: 'float', required: false, example: -342, description: 'Airdrop and patrol helicopter only: world z, instead of a monument.' }, ], cost: (p) => ({ 'rust.world.calls': Math.max(1, Math.round(Number(p.count) || 1)) }), async perform({ runId, idempotencyKey, params, verify }) { const event = EVENTS.find((e) => e.value === String(params.event || '').trim()) if (!event) { return { ok: false, retry: false, error: `"${params.event}" is not one of the world events a Rust server calls` } } const blank = params.count === undefined || params.count === null || params.count === '' const count = blank ? 1 : Number(params.count) if (!Number.isInteger(count) || count < 1 || count > event.max) { return { ok: false, retry: false, error: `call 1 to ${event.max} of the ${event.label.toLowerCase()} at a time, and "${params.count}" is not that` } } let serverId = String(params.server || '').trim() let wire = {} if (hasPosition(params)) { if (!event.placeable) { return { ok: false, retry: false, error: `the ${event.label.toLowerCase()} goes where the game puts it; only an airdrop and a patrol helicopter take a position` } } const where = location(params) if (!where.ok) return { ok: false, retry: false, error: where.error } serverId = where.serverId // A height means nothing to a call: the drop falls to the ground and the // helicopter flies at its own. const { y, ...flat } = where.wire wire = flat } else if (!serverId) { return { ok: false, retry: false, error: 'the step does not say which server — pick one' } } const found = await serverFor(serverId) if (!found.ok) return found if (verify) return { ok: true } const result = await client.worldCall(found.server, { runId: String(runId), key: idempotencyKey, event: event.value, count, ...wire, }) if (!result.ok) { const permanent = result.status === 'not-configured' || result.status === 'no-token' return { ok: false, ...(permanent ? { retry: false } : {}), error: transportError(found.server, result, 'world event call') } } const data = result.data || {} if (data.kind !== 'world.called') { return { ok: false, ...(PERMANENT.has(data.reason) ? { retry: false } : {}), error: pluginError(data, `${found.server.name || found.server.id} refused the ${event.label.toLowerCase()}`), } } const called = Number(data.called) || 0 return { ok: true, detail: { event: event.value, called, ...(called < count ? { note: `the game started ${called} of the ${count} asked for` } : {}), ...(data.repeat ? { repeat: true, note: 'answered from the first attempt; nothing new was called' } : {}), }, } }, } const ACTIONS = [worldCall] module.exports = { EVENTS, MAX_CALLS_PER_RUN, PERMANENT, BUDGETS, ACTIONS, OPTION_SOURCES, hasPosition }