docs(rust): stage 6 built — Guarded Crate (PROTOCOL §20.9, PLAN_REDESIGNS §14.3.19, D380)

- rust-link/PROTOCOL.md §20.9: hello's guardedCrate with npcSpawn and events,
  gc.started/ended/failed, gc.place and gc.error's reasons, guardedWon, map.live's
  guarded rows and how they are kept, POST /gc/place, and the website side.
  §17.3's world row gains guarded.
- modules/rust/PLAN_REDESIGNS.md: D380 (the kept crate's run-log note at
  placement), §14.3.19 the stage record, pointers from §9 and §14.3.5.
- modules/rust/OPERATING.md: Runic Gateway's own section on Guarded Crate and
  NpcSpawn (install, images, what it contacts at load), after the uMod mirror.

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 12:26:53 -05:00
parent 966ba41deb
commit a305e98e05
3 changed files with 248 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`, `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 |
| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`, `treasure`, `guarded`), `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; a `guarded` (protocol 14, §20.9) is a running Guarded Crate event |
| `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 |
@@ -3249,3 +3249,128 @@ events like any other.
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.
### 20.9 Guarded Crate — `gc.started`, `gc.ended`, `gc.failed`, `gc.place` (§14.3.5, D361, D363, D364, D373, D375, D380)
Bazz3l's **Guarded Crate** (2.0.17, uMod), optional like Economics (D199) and not in `overlay.toml`, with its
dependency **NpcSpawn** (3.4.8, Codefling; D361), which spawns its guards. A cargo plane drops a locked crate at
the event's spot, ringed by guards (8 to 20 by event); **whoever kills the last guard has cleared it**, and the
crate unlocks for them. An event nobody clears ends after its time (1,800 s as it ships, counted from the last
guard killed). The bridge reports its events, and places one for a run's step through Guarded Crate's own
console command. The design is [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §14.3.5; what the
§14.2 test found is §14.2.1 there, and what the build found in its source is §14.3.19.
**Hello and status.** `integrations` gains `guardedCrate`, with NpcSpawn beside it (D364):
```json
"guardedCrate": { "loaded": true, "version": "2.0.17",
"npcSpawn": { "loaded": true, "version": "3.4.8" },
"events": ["Easy", "Medium", "Hard", "Elite"] }
```
`npcSpawn` is always sent, `{ "loaded": false }` when it is absent. **`events` are the event names in Guarded
Crate's DATA file** (`data/GuardedCrate.json`, each `CrateEventEntries` row's `event display name)`, the key's
stray bracket and all), not its config; it is absent while Guarded Crate is not loaded or when the file cannot
be read. Hello is sent again when Guarded Crate or NpcSpawn loads or unloads. Placing one also needs hello's
`console.plugins` (§20.5, D363). `rg.world` prints `guardedCrate=`, `npcSpawn=`, `guarded=` (the running
events the bridge knows), `gcPlaces=` (keys remembered), `gcEvents=`, `gcCommand=` and `gcListReadable=`.
**Three events**, from Guarded Crate's own hooks:
```json
{"kind":"gc.started","type":"event","x":-604.0,"z":-342.0,"key":"12:4"}
{"kind":"gc.ended","type":"event","steamId":"76561198000000001","name":"Alice","x":-604.0,"z":-342.0}
{"kind":"gc.failed","type":"event","x":-604.0,"z":-342.0}
```
- **`gc.started`** is `OnGuardedCrateEventStart(position)`. **`key` is set when the start answers one of our
`gc.place` commands**, and the join is exact: Guarded Crate raises the hook inside the call that creates the
event, as Dangerous Treasures does (§20.8).
- **`gc.ended`** is `OnGuardedCrateEventEnded(player, crate)`, raised when the last guard dies. **`steamId` and
`name` are the player who killed it, and are absent when nobody did**: the guards went another way (NpcSpawn
reloaded under them, or they died of falls or drowning), or another NPC killed the last one. The hook does not
say which event, so the bridge finds it in Guarded Crate's own list (the one marked complete), else at the
crate's position or the player's, within 100 m, else the only one running; `x`, `z` are absent if none can be
told.
- **`gc.failed`** is `OnGuardedCrateEventFailed(position)`: its time ran out with guards standing.
- **Guarded Crate's unload and `gcrate stop all` end every event and raise no hook.** Nothing is sent for them;
the map's set is corrected instead (below).
- **None is sent while the world is not live** (§20.5's rule). A clearing is still counted.
- **Guarded Crate's cargo plane is not an airdrop.** The plane that delivers its crate raises no
`airdrop.called` (§20.5): the bridge asks Guarded Crate's own `API_IsGuardedCrateCargoPlane`. Its crate is
never Rust's own either, by §20.5's rule for crates.
**The tally** gains **`guardedWon`**, the events a player cleared (*Warden*, D375).
**`map.live`'s `world`** (§17.3) gains `kind: "guarded"` for each running event, `x`, `z`. The bridge keeps the
set by the hooks, and **checks it against Guarded Crate's own list** (its private `EventManager.Instances`, read
by reflection, since it has no API that lists them) at its load, before every `map.live` and on `rg.world`: a
hotload of the bridge hears no starts, and `stop all` ends events with no hook. Where the list cannot be read
(said once in the log), the hooks alone keep the set. It is cleared when Guarded Crate unloads.
**`gc.place` — place one guarded crate for a run (D364, D373).**
```json
{"cmd":"gc.place","reqId":"r-62","key":"12:4","event":"Elite","monument":"powerplant_1","offsetX":10,"offsetZ":-5}
```
`event` is one of `events`, matched ignoring case as Guarded Crate matches it. **The location is a world
call's** (§20.5): a `monument` with `offsetX`/`offsetZ`, or `x` and `z` (and `y`, else the ground). The bridge
runs `<command> position <event> x,y,z` through `ConsoleSystem.Run`, where `<command>` is Guarded Crate's
console command from its config (`gcrate` unless the operator renamed it). The answer is:
```json
{"kind":"gc.placed","type":"reply","reqId":"r-62","key":"12:4","event":"Elite","repeat":false,"x":-604.0,"z":-342.0}
```
**A key already answered is answered again with its first placement, `repeat: true`, and places nothing.** Keys
are kept seven days in `data/RunicGateway/gc-places.json`. **Nothing removes a placed crate** (D373): Guarded
Crate cannot end one event alone. Refusals are `{"kind":"gc.error","reason":…,"message":…}`:
| `reason` | When |
|---|---|
| `malformed` | No key, or one over 128 characters; or a location that is both or neither (§20.5) |
| `bad-event` | No event named, a name not in `events`, or Guarded Crate's own "event not found" |
| `gc-missing` | Guarded Crate is not loaded |
| `npcspawn-missing` | NpcSpawn is not loaded. **Guarded Crate's `position` does not check this itself** (only its `start` does), so without the bridge's refusal the crate would land unguarded and free to hack (D364) |
| `console-unavailable` | Hello's `console.plugins` is false (D363, Rust-Plugins#34) |
| `no-monument`, `out-of-range`, `off-map` | The location, as §20.5 |
| `gc-too-close` | Another guarded crate is running within 80 m (Guarded Crate's own rule) |
| `bad-position` | Guarded Crate's own "position invalid" |
| `refused` | Nothing was placed and Guarded Crate answered something else, in its own words; or its console command is switched off in its config |
`events-disabled` and the world's `not-ready` come first, as `world.error`, as for every world verb (§15).
"Placed" is the start hook firing; Guarded Crate's replies are recognised by reading its own messages from its
lang file, as §20.8 does for DT.
**Hooks.** `OnGuardedCrateEventStart`, `OnGuardedCrateEventEnded` and `OnGuardedCrateEventFailed` join
`rg.hooks`' list.
**The sidecar** adds `POST /gc/place`, a thin forward that keeps nothing, and stores and forwards the three
events like any other.
**The website (Module-Rust).**
- **`gc.started` and `gc.ended` are public kinds, `gc.failed` a staff kind.** Each row is read per viewer by
`guardedCrate.shapeFor`, as §20.8's are: everyone gets `grid`, and for an ending `won` (whether a player
cleared it); **the clearer's `name` only for the presence audience**; **`x`, `z`, `key` and the clearer's
`steamId` only for staff**. The lines read *"A guarded crate landed · M4"*, *"Alice cleared the guarded crate ·
M4"* (*"The guarded crate was cleared"* below the presence audience), *"A guarded crate ended with nobody
clearing it"*, and for staff *"A guarded crate timed out with its guards standing"*. The first two are in the
feed's *World events* filter.
- **Triggers `rust.gc.started` and `rust.gc.ended`** (§14.3.9): public, `everyone` the ceiling, the cooldown
subject the server; `byEvent` on a placement one of the site's steps made; **`rust.gc.ended` fires only for a
crate a player cleared**, with the clearer by in-game name only. A timeout raises nothing.
- **The step `rust.gc.place`**, *Place a guarded crate* (`risk: 'change'`, `reversible: 'none'`, budget
`rust.gc.crates`, one per step): the event (option source `rust.options.gc_events`, every server's own names,
or Guarded Crate's four defaults where none has said) and a world call's location. It is refused at verify
where the server said Guarded Crate is not loaded, that NpcSpawn is missing (*"Guarded Crate is loaded on Main
but NpcSpawn is missing: its events cannot start"*), that `console.plugins` is false, or that it has no event
of that name; a server that has not said is left to the plugin. **Its success line in the run log says the
crate is kept when the run ends** and how it ends (D373, D380). `gc-too-close` and a game that is down retry on
core's ordinary delay; every other refusal is permanent.
- **Warden (D375):** `guardedWon` → `rust_player_wipe_stats.guarded_won`, the title category `guardedwon`
(default *Warden*), and the leaderboard's **Crates cleared** column (`sort=guardedWon`), shown where the
server reports Guarded Crate or a row counts one (`guardedCrate: true` on the answer).
- The map draws a running event on the world layer. Server settings' plugin line names Guarded Crate and
NpcSpawn, the events, and whether a step can place one.