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.