From fa696e9c84a145acc0300971dbfc2bc06a505c51 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 15 Oct 2026 06:45:33 -0500 Subject: [PATCH] =?UTF-8?q?docs(rust):=20stage=205=20built=20=E2=80=94=20D?= =?UTF-8?q?angerous=20Treasures=20(PROTOCOL=20=C2=A720.8,=20PLAN=5FREDESIG?= =?UTF-8?q?NS=20=C2=A714.3.18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 68 +++++++++++++++++++- rust-link/PROTOCOL.md | 110 ++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 2a09dff..15b06c0 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -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 diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index a222c6c..20a763c 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -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 ` 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. -- 2.49.1