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

Merged
whitlocktech merged 1 commits from docs/dangerous-treasures into main 2026-10-15 12:04:18 +00:00
2 changed files with 176 additions and 2 deletions

View File

@@ -930,7 +930,7 @@ pulls) is research, not a walk, so it still comes first:
answered before code: **written on 2026-10-14 as §14.3**, from D361–D369, and answered as D370–D376. Then the
build, in §14.3.11's nine stages (two of them core), §13 first. **Stage 1, §13, was built on 2026-10-15
(§14.3.14)**, with D378 and D379. **Stage 2, core's `retryAfterMs` (MODULE_API 1.15.0), was built on
2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16)**, and **stage 4, Dynamic PVP, the same day (§14.3.17).**
2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16)**, and **stage 4, Dynamic PVP, the same day (§14.3.17)**, and **stage 5, Dangerous Treasures, the same day (§14.3.18).**
**Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and
§14's rows; the in-game rows
@@ -2588,6 +2588,9 @@ the column and category.
see *"not ready yet"*, the automatic retry and the manual retry; win one and see the line, the column and the title.
On Carbon: the step refused with Rust-Plugins#34 (until D363 clears).
> **Built on 2026-10-15** (§14.3.18), specified in PROTOCOL.md §20.8. The key's join, how a start that opened
> nothing is recognised, and the Carbon row differ from this text; §14.3.18 says how.
#### 14.3.5 Guarded Crate, with NpcSpawn (D361, D364)
**Hello.** `integrations.guardedCrate`: `{ loaded, version, npcSpawn: { loaded, version }, events: […] }`.
@@ -3032,6 +3035,69 @@ are exercised by §14.3.3's walk rows.
**Walk rows** are §14.3.3's, at the end walk, with one more: hotload the bridge while a zone stands and see
the ring still drawn (the seed).
#### 14.3.18 Stage 5, built: Dangerous Treasures (2026-10-15)
Built in Rust-Plugins, Rust-Link and Module-Rust, specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.8
and §17.3. `PROTOCOL_VERSION` stays 14. Builds: Rust 25902425, Oxide 2.0.7830, Carbon 2.0.262 (the latest of
each that day); the code read is Dangerous Treasures 3.0.0, the version the §14.2 test ran and still the newest
on uMod. **Module-Rust moves to MODULE_API 1.15.0** (`coreApi ^1.15.0`, `ci/core-ref.json` pinned to website
`main` at #215's merge), because the step answers DT's warm-up with `retryAfterMs` (§14.3.15).
**Three things in §14.3.4 were wrong or unsaid, found in DT's source:**
1. **The key's join is exact, not "the first start within ten seconds".** DT raises `OnDangerousEventStarted`
inside the call that opens the chest (the chest's radius setter runs its `Awaken`), so a start our `dtevent`
caused is heard before `ConsoleSystem.Run` returns. The bridge holds the key for the length of that call and
no longer; a natural start ten seconds later can never take it.
2. **"Started" is the hook, never DT's text.** §14.3.4 told the outcomes apart by the reply *"Unable to obtain a
valid position"*, but DT words every reply from its lang file, which an operator may translate. The bridge
reads DT's own *Manual Event Failed* message through the lang API and compares the reply with that; any
other reply with nothing opened is **`refused`**, a fourth outcome the plan did not have (a difficulty DT's
config no longer has, or its console command renamed), which is not retried.
3. **The Carbon walk row is out of date.** §14.3.4 expected the step refused on Carbon (Rust-Plugins#34). Stage 1
found the console probe true on Carbon from the bridge's own path (§14.3.14 item 7), and `rg.world` read
`consolePlugins=True` on the Carbon rig again today. So on Carbon the step is expected to **work**, and the end
walk checks that instead; it is still refused wherever the probe reads false.
**Settled in the code, and overrulable:**
1. **DT's warm-up window starts when the bridge first saw DT loaded**: DT's own load, or the bridge's when DT was
already there. It can only start late, so a bridge hotloaded beside a long-running DT reads a no-spot as
*not ready* for up to fifteen minutes (a ten-minute retry instead of a one-minute one), never the other way.
2. **A win is placed at the running chest nearest the winner** (within 100 m), since the hook does not say which
chest; else at the winner. `invaders` names at most 32 and `invaderCount` counts them all.
3. **Running chests are found at load by their component.** DT has no API that lists them, so the seed looks for
large wooden boxes carrying DT's `TreasureChest` component, at the world's ready and at DT's load.
4. **The winner's name is the presence audience's, the invaders and every Steam id staff's.** How many fought is
a count and is public. A start's `key` is staff's.
5. **A level is 0, 1 or 2**, as DT ships them; blank is Easy. An operator's own extra levels are not offered.
6. **The Treasures column shows where the server reports DT, or the page's rows count a treasure**, so a wipe
won before DT was removed still shows what was won.
7. **`refused`, `dt-missing`, `console-unavailable`, `bad-level` and `malformed` are permanent failures**; only
`dt-no-spot` and a game that is down retry on core's ordinary delay, and `dt-not-ready` in ten minutes.
8. **A server that has not said whether it has DT is left to the plugin**, as a zone's TruePVE is (§14.3.16).
9. **Triggers' cooldown subject is the server**, so a busy hour of treasures is one notice per cooldown; a start
carries `byEvent` when one of the site's steps made it.
10. **The admin integrations read gains `console`**, so the server settings' plugin line can say DT is loaded but
cannot be started there.
**Tests.** Bridge: `checkPlugin.js` (81 hooks, every one void and listed), 18 tests. Compiled and loaded on both
rigs; `rg.world` read `dangerousTreasures=False treasures=0` on each, and both were put back on the protocol-13
bridge. DT itself is not on the rigs; its hooks and the start are exercised by §14.3.4's walk rows. Sidecar: 72
tests (one new, the stamp), `fmt` and `clippy` clean. Module-Rust server: `test/dangerousTreasures.test.js`, 16
new (the public read the grid, never the position or the key; the winner's name for the presence audience, the
Steam ids and invaders for staff; the feed reads a stored win per viewer; the report read leniently; the step's
price, levels, dry-run refusals for DT absent and the console unavailable, and a server that has not said left
to the plugin; the key and level on the wire; not ready with the ten-minute retry; no spot, a refusal and a
game down; a repeat; the tally's count, the column's sort; the triggers and their variables; an old treasure
tells nobody), and the catalogue, title and registration tests extended: 627/627 and every check. Client: three
new in `feed.test.js`, 92/92, the build and externals. Booted against the newly pinned core: 45 triggers, 13
actions, 11 budgets, 21 option sources; the 89 routes unchanged.
**Walk rows** are §14.3.4's, at the end walk, with the Carbon row now expecting the step to work, and two more:
hotload the bridge while a chest stands and see it still on the map (the seed); and start one from a run and
read its `dt.started` row's `key` from the staff events API (the web client has no staff feed view).
### 14.4 Questions for the org lead
**Answered on 2026-10-14:** Q17 as proposed, out (D358). **Q18 against the proposal: the end walk waits for

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.