docs(rust): stage 5 built — Dangerous Treasures (PROTOCOL §20.8, PLAN_REDESIGNS §14.3.18)

- rust-link/PROTOCOL.md §20.8: hello's integrations.dangerousTreasures; the
  dt.started / dt.won / dt.ended events and their fields; dt.start, dt.opened
  and the dt.error reasons (dt-not-ready, dt-no-spot, refused among them); the
  tally's treasuresWon; map.live's treasure rows and their seed; the sidecar's
  POST /dt/start; and the website side (classes, per-viewer reading, triggers,
  the step, Treasure Hunter). §17.3's world row gains `treasure`.
- modules/rust/PLAN_REDESIGNS.md §14.3.18: the stage-5 record. Three things
  §14.3.4 had wrong or unsaid (the key's join is exact; "started" is the hook,
  never DT's text, with a fourth outcome `refused`; the Carbon walk row now
  expects the step to work), ten points settled in code, the tests, the rig
  compile and the walk rows. Pointers from §9's summary and §14.3.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-15 06:45:33 -05:00
parent 8829222f18
commit fa696e9c84
2 changed files with 176 additions and 2 deletions

View File

@@ -1805,7 +1805,7 @@ drawing a second time. A new map key makes the file stale, and it is ignored.
| Field | Rows |
|---|---|
| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true`; a `boss` (protocol 13, RunicNPC D302) is one of RunicNPC's bosses placed outside any event, with its `name` |
| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`, `treasure`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true`; a `boss` (protocol 13, RunicNPC D302) is one of RunicNPC's bosses placed outside any event, with its `name`; a `treasure` (protocol 14, §20.8) is a running Dangerous Treasures chest |
| `events` | what this site's events placed (§15.2): `kind` (`zone`, `crate`, `npc`), `runId`, `x`, `z`, and a zone's `radius` and `name` or a thing's `prefab` and `key` (the step that placed it). Protocol 13 adds, for one of RunicNPC's NPCs, its `name`, and for a boss `boss: true` and `health` (0–1); and a boss's **adds**, which no step placed, under their boss's run with `add: true` |
| `players` | `steamId`, `name`, `x`, `z`, `sleeping`, `online`. Connected players, then offline sleepers |
| `bases` | `kind` (`tc`, `vending`), `x`, `z`. **Positions only**: no owner, no authorised list, no shop name |
@@ -3141,3 +3141,111 @@ it changed.
(`map.layer.pvp.audience`), **public by default**: the game shows the zones to everybody near one, and they
name nobody. It is drawn as a red ring with its event's name.
- Server settings' plugin line says whether Dynamic PVP is loaded.
### 20.8 Dangerous Treasures — `dt.started`, `dt.won`, `dt.ended`, `dt.start` (§14.3.4, D363, D365, D371, D375)
nivex's **Dangerous Treasures** (3.0.0, uMod), optional like Economics (D199) and not in `overlay.toml`. It
opens a guarded, burning treasure chest somewhere on the map, with its own NPCs, fire and marker, unlocks it
after 300–480 s and despawns it 900 s after that if nobody empties it. The bridge reports its events, and opens
one for a run's step through DT's own console command. The design is
[`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §14.3.4; what the §14.2 test found is §14.2.1 there,
and what the build found in DT's own source is §14.3.18.
**Hello and status.** `integrations` gains `dangerousTreasures`:
```json
"dangerousTreasures": { "loaded": true, "version": "3.0.0" }
```
Hello is sent again when DT loads or unloads. Opening one also needs hello's `console.plugins` (§20.5, D363).
`rg.world` prints `dangerousTreasures=`, `treasures=` (the running chests the bridge knows), `dtStarts=` (keys
remembered) and `dtWarmingUp=`.
**Three events**, from DT's own hooks:
```json
{"kind":"dt.started","type":"event","x":812.4,"z":-340.5,"key":"12:3"}
{"kind":"dt.won","type":"event","steamId":"76561198000000001","name":"Alice",
"invaders":["76561198000000001","76561198000000002"],"invaderCount":2,"x":812.4,"z":-340.5}
{"kind":"dt.ended","type":"event","x":812.4,"z":-340.5}
```
- **`dt.started`** is `OnDangerousEventStarted(position)`, the chest's position. **`key` is set when the start
answers one of our `dt.start` commands.** DT raises the hook *inside* the call that opens the chest, so a
start our `dtevent` caused is heard before the console call returns: the join is exact, not the plan's "first
start within ten seconds".
- **`dt.won`** is `OnDangerousEventWon(looter, invaders)`, raised when the chest is emptied, for the player who
emptied it. `invaders` are the players who entered the event, at most 32 by Steam id; `invaderCount` counts
them all. The hook does not say which chest, so `x`, `z` are those of the running chest nearest the winner
(within 100 m), else the winner's own.
- **`dt.ended`** is `OnDangerousEventEnded(position)`: won, despawned, or DT unloading.
- **None is sent while the world is not live** (§20.5's rule). A win is still counted.
**The tally** gains **`treasuresWon`**, the chests a player emptied (*Treasure Hunter*, D375).
**`map.live`'s `world`** (§17.3) gains `kind: "treasure"` for each running chest, `x`, `z`. The bridge keeps
the set by the start and end hooks, and **seeds it at the world's ready and at DT's load** by finding DT's
chests in the world (a large wooden box carrying DT's `TreasureChest` component), since DT has no API that
lists them and a hotload of the bridge hears no starts.
**`dt.start` — open one Dangerous Treasures event for a run (D365).**
```json
{"cmd":"dt.start","reqId":"r-60","key":"12:3","level":1}
```
`level` is 0 (Easy), 1 (Medium) or 2 (Hard). The bridge runs `dtevent <level>` through
`ConsoleSystem.Run`; DT picks the spot. The answer is:
```json
{"kind":"dt.opened","type":"reply","reqId":"r-60","key":"12:3","level":1,"repeat":false,"x":812.4,"z":-340.5}
```
**A key already answered is answered again with its first start, `repeat: true`, and opens nothing.** Keys are
kept seven days in `data/RunicGateway/dt-starts.json`, as a world call's are. Refusals are
`{"kind":"dt.error","reason":…,"message":…}`:
| `reason` | When |
|---|---|
| `malformed` | No key, or one over 128 characters |
| `bad-level` | `level` is not 0, 1 or 2 |
| `dt-missing` | DT is not loaded |
| `console-unavailable` | Hello's `console.plugins` is false (D363, Rust-Plugins#34) |
| `dt-not-ready` | Nothing opened, DT's answer was its own "Manual Event Failed" message (or empty), and DT loaded under fifteen minutes ago: it is still building its spawn grid (§14.2.1) |
| `dt-no-spot` | The same, fifteen minutes or more after DT loaded |
| `refused` | Nothing opened and DT answered something else, in its own words: a level its config no longer has, or its console command renamed |
`events-disabled` and the world's `not-ready` come first, as `world.error`, as for every world verb (§15).
"Opened" is the start hook firing, never DT's text: DT words its replies from its lang file, so the bridge
reads DT's own "Manual Event Failed" message from DT's lang to recognise a start that found no spot. **DT's
load is when the bridge first saw DT loaded**: DT's own load, or the bridge's when DT was already there, which
can only make the window start late. DT's console command is a config value; the bridge runs its default,
`dtevent`.
**Hooks.** `OnDangerousEventStarted`, `OnDangerousEventWon` and `OnDangerousEventEnded` join `rg.hooks`' list.
**The sidecar** adds `POST /dt/start`, a thin forward that keeps nothing, and stores and forwards the three
events like any other.
**The website (Module-Rust).** Module-Rust needs MODULE_API 1.15.0 for this (`coreApi ^1.15.0`).
- **`dt.started` and `dt.won` are public kinds, `dt.ended` a staff kind.** Each row is read per viewer by
`dangerousTreasures.shapeFor`, as a world event is (§20.5, D376): everyone gets `grid`, and for a win
`invaderCount`; **the winner's `name` only for the presence audience**; and **`x`, `z`, `key`, the winner's
`steamId` and the `invaders` only for staff**. The public lines read *"A Dangerous Treasures event has started ·
K12"* and *"Alice won the treasure · K12 · 3 players fought for it"* (*"The treasure was won"* below the
presence audience). Both are in the feed's *World events* filter.
- **Triggers `rust.dt.started` and `rust.dt.won`** (§14.3.9): public, `everyone` the ceiling, the cooldown
subject the server; `byEvent` on a start one of the site's steps made; the winner by in-game name only.
- **The step `rust.dt.start`**, *Start a Dangerous Treasures event* (`risk: 'change'`, `reversible: 'none'`,
budget `rust.dt.events`, one per step): server and difficulty. It is refused at verify where the server said
DT is not loaded, or that `console.plugins` is false (naming Rust-Plugins#34); a server that has not said is
left to the plugin. **`dt-not-ready` answers *"Dangerous Treasures is not ready yet"* with `retryAfterMs:
600000`** (MODULE_API 1.15.0, D371), so core tries once more ten minutes later; `dt-no-spot` retries on core's
ordinary delay; `malformed`, `bad-level`, `dt-missing`, `console-unavailable`, `events-disabled` and `refused`
are not retried.
- **Treasure Hunter (D375):** `treasuresWon` → `rust_player_wipe_stats.treasures_won`, the title category
`treasureswon` (default *Treasure Hunter*), and the leaderboard's **Treasures** column (`sort=treasuresWon`),
shown where the server reports DT or a row counts one (`dangerousTreasures: true` on the answer).
- The map draws a running chest on the world layer. Server settings' plugin line says whether DT is loaded, and
whether a step can start one; `GET /admin/rust/servers/{id}/integrations` gains `console` for it.