feat(rust): Dangerous Treasures — feed, map, Treasure Hunter, Start a Dangerous Treasures event (PLAN_REDESIGNS §14.3.4)
Stage 5 of the §14.3 integration plan, on protocol 14 (docs rust-link/PROTOCOL.md §20.8). - dt.started and dt.won are public kinds, dt.ended a staff kind. The new dangerousTreasures.js reads each row per viewer: grid and invaderCount for everyone, the winner's name for the presence audience (D376), the position, the step's key, the winner's Steam id and the invaders for staff. - Feed lines in the World events filter: "A Dangerous Treasures event has started · K12", "Alice won the treasure · K12 · 3 players fought for it". - Triggers rust.dt.started (with byEvent) and rust.dt.won, public, cooldown subject the server, names only. - eventDangerousTreasures.js: rust.dt.start, "Start a Dangerous Treasures event" (risk change, reversible none, budget rust.dt.events), server and difficulty. Refused at verify where the server said DT is not loaded or console.plugins is false (Rust-Plugins#34). dt-not-ready answers "Dangerous Treasures is not ready yet" with retryAfterMs 600000 (D365, D371); dt-no-spot retries on core's delay; DT's own refusals are permanent. - Treasure Hunter (D375): treasuresWon -> treasures_won, the title category treasureswon, and a sortable Treasures column shown where the server reports DT or a row counts one. - The map draws a running chest on the world layer; the server settings' plugin line names DT and whether a step can start one, from the integrations read's new `console`. - coreApi ^1.15.0, ci/core-ref.json pinned to website main at #215's merge. ci/bundle.json ships the two new files; swagger fragment and engagement manifest regenerated; README lists DT. Server 627/627 (16 new in test/dangerousTreasures.test.js) and every check; client 92/92, build and externals. Booted against the pinned core: 45 triggers, 13 actions, 11 budgets, 21 option sources; 89 routes unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
172
server/eventDangerousTreasures.js
Normal file
172
server/eventDangerousTreasures.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// ── 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 <level>`, 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 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
|
||||
|
||||
const missing = missingOn(reportOf(await serversDb.dtReportFor(found.server.id)), name)
|
||||
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 }
|
||||
Reference in New Issue
Block a user