// ── Starting a Dangerous Treasures event (protocol 14, PLAN_REDESIGNS §14.3.4) ── // // `rust.dt.start`, "Start a Dangerous Treasures event": one of nivex's treasure // events, at the difficulty the step picks, where DT picks. The plugin runs DT's // own console command, `dtevent `, and joins the start DT raises to the // step's key, so a retry is answered with the first start and never opens a // second chest. // // **Kept (`reversible: 'none'`).** DT guards, unlocks and despawns its own chest // (900 s after it unlocks, if nobody empties it); a run ending takes nothing back. // `risk: 'change'`, so an operator switches it on deliberately. // // **Refused at verify** where the server said DT is not loaded, or that its // framework cannot run another plugin's console command (hello's // `console.plugins`, D363, Rust-Plugins#34). A server that has not said (a plugin // before protocol 14, or never reached) is left to the plugin, as a zone's // TruePVE is (§14.3.16). // // ── D365: DT's warm-up ───────────────────────────────────────────────────── // // For about ten minutes after it loads DT is still finding where events may go, // and every start fails. The plugin calls that `dt-not-ready`; the step reports // "Dangerous Treasures is not ready yet" and asks core to try once more in ten // minutes (`retryAfterMs`, MODULE_API 1.15.0, D371). `dt-no-spot` retries on // core's ordinary delay. Past core's attempts the run pauses on its own // `on_failure`, and staff press Retry on the paused step (EVENTS.md §I), which // sends the same key. const client = require('./sidecarClient') const { serverFor, transportError, pluginError } = require('./eventLeases') const { BUDGET_MS } = require('./eventWorld') const { gridOf } = require('./worldEvents') const { LEVELS, reportOf } = require('./dangerousTreasures') const live = require('./liveStatus') const serversDb = require('./model/servers/servers.db') const mapDb = require('./model/map/map.db') /** How long core waits before trying a start DT was not ready for (D365). */ const NOT_READY_RETRY_MS = 10 * 60 * 1000 /** What the step says DT is not ready, in the plan's words (D365). */ const NOT_READY = 'Dangerous Treasures is not ready yet' /** The plugin's refusals a second attempt would repeat. */ const PERMANENT = new Set([ 'events-disabled', 'malformed', 'bad-level', 'dt-missing', 'console-unavailable', // DT's own refusal in its own words: a difficulty its config no longer has, or // its console command renamed. Neither is fixed by waiting. 'refused', ]) const BUDGETS = [ { id: 'rust.dt.events', label: 'Dangerous Treasures events', unit: 'events', description: 'Dangerous Treasures events an event starts, each one counted. Dangerous Treasures despawns its own chest; nothing is taken back when the run ends.', }, ] const OPTION_SOURCES = [ { id: 'rust.options.dt_levels', label: 'Dangerous Treasures difficulty', description: "Dangerous Treasures' three difficulties, as it ships them.", async resolve() { return LEVELS.map((l) => ({ value: l.value, label: l.label })) }, }, ] /** A level param: 0, 1 or 2, or null for anything else. Blank is Easy. */ function levelOf(raw) { if (raw === undefined || raw === null || raw === '') return 0 const n = Number(raw) return LEVELS.some((l) => l.value === n) ? n : null } /** * Why a server cannot take the step, from what it last said, or null. Only a * report that SAYS no refuses; one that said nothing is the plugin's to answer. */ function missingOn(report, name) { if (report.dangerousTreasures && !report.dangerousTreasures.loaded) { return `Dangerous Treasures is not installed on ${name}` } if (report.consolePlugins === false) { return `${name}'s modding framework cannot run Dangerous Treasures' console command from a plugin, so a treasure event cannot be started there yet (Rust-Plugins#34)` } return null } /** The grid an opened chest is in, or null without the map's geometry. */ async function gridAt(serverId, x, z) { const meta = await mapDb.getMeta(serverId).catch(() => null) if (!meta) return null return gridOf({ worldSize: Number(meta.worldSize), gridCells: Number(meta.gridCells), gridCellSize: Number(meta.gridCellSize) }, x, z) } const dtStart = { id: 'rust.dt.start', label: 'Start a Dangerous Treasures event', description: 'Starts one Dangerous Treasures event: a guarded, burning treasure chest that unlocks after a few minutes. Dangerous Treasures picks the spot, and despawns the chest itself if nobody empties it. Needs Dangerous Treasures on the server. In the first ten minutes after Dangerous Treasures loads it is not ready, and the step tries again ten minutes later.', risk: 'change', reversible: 'none', version: 1, budgetMs: BUDGET_MS, params: [ { name: 'server', type: 'string', required: true, example: 'main', source: 'rust.options.servers', description: 'Which server. It needs Dangerous Treasures installed.' }, { name: 'level', type: 'int', required: false, example: 1, source: 'rust.options.dt_levels', description: 'The difficulty: 0 Easy, 1 Medium, 2 Hard. Left blank, Easy.' }, ], cost: () => ({ 'rust.dt.events': 1 }), async perform({ idempotencyKey, params, verify }) { const level = levelOf(params.level) if (level === null) { return { ok: false, retry: false, error: `a Dangerous Treasures event is 0 (Easy), 1 (Medium) or 2 (Hard), not "${params.level}"` } } const serverId = String(params.server || '').trim() 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 const name = found.server.name || found.server.id // Confirmed live before it refuses: the stored status lags a restart (W7). const missing = await live.confirmed(found.server, async () => missingOn(reportOf(await serversDb.dtReportFor(found.server.id)), name), Boolean) if (missing) return { ok: false, retry: false, error: missing } if (verify) return { ok: true } const result = await client.dtStart(found.server, { key: idempotencyKey, level }) if (!result.ok) { const permanent = result.status === 'not-configured' || result.status === 'no-token' return { ok: false, ...(permanent ? { retry: false } : {}), error: transportError(found.server, result, 'Dangerous Treasures start') } } const data = result.data || {} if (data.kind !== 'dt.opened') { if (data.reason === 'dt-not-ready') { return { ok: false, retry: true, retryAfterMs: NOT_READY_RETRY_MS, error: `${NOT_READY} on ${name}` } } return { ok: false, ...(PERMANENT.has(data.reason) ? { retry: false } : {}), error: pluginError(data, `${name} did not start the Dangerous Treasures event`), } } const grid = await gridAt(found.server.id, data.x, data.z) return { ok: true, detail: { level: LEVELS.find((l) => l.value === Number(data.level))?.label || String(data.level), ...(grid ? { grid } : {}), ...(data.repeat ? { repeat: true, note: 'answered from the first attempt; nothing new was started' } : {}), }, } }, } const ACTIONS = [dtStart] module.exports = { BUDGETS, ACTIONS, OPTION_SOURCES, PERMANENT, NOT_READY, NOT_READY_RETRY_MS, levelOf, missingOn }