docs(rust): stage 1 built — world events on protocol 14 (PROTOCOL §20.5, D378, D379)

- rust-link/PROTOCOL.md §20.5: `world.event` and its 24 types (the type in
  `event`, not the envelope's `type`), the credit block, the tally's two
  counts, `world.call`, hello's `console`, the sidecar forward and the site.
- modules/rust/PLAN_REDESIGNS.md: D378 (the Chinook is called with no
  position) and D379 (`chinook.downed` counts toward Rotorbane), both the org
  lead's on 2026-10-15; §14.3.14, the stage-1 record: the placement spike, the
  seven changes the build found (among them a correction to §14.2.1: Carbon
  refuses plugin console commands only inside a console command, so the
  bridge's own path works), what was settled in the code, the tests and the
  walk rows it adds; pointers from §14.3.1 and §14.3.8.

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 00:39:01 -05:00
parent 0ada1e4f26
commit adf197e5f8
2 changed files with 229 additions and 2 deletions

View File

@@ -2873,3 +2873,149 @@ through the `rust.backpack.size` step (*Bigger backpack*, `risk: 'change'`, `rev
`rust.backpacks`): one `rust_perm_run_grants` row per recipient with `backpacks.size.N` as its permission and no
kit, pushed by the next `perm.sync` and revoked when the run's revert deletes the rows. Its message, a §20.2
notice, defaults to a warning about the take-back.
### 20.5 Rust's own world events — `world.event`, `world.call`, the console probe (§14.3.1, D349, D353–D356, D363, D366, D367, D372, D376, D378, D379)
The patrol helicopter, Bradley, the cargo ship, the Chinook, airdrops, locked crates, the Travelling Vendor,
the satellite and the Deep Sea, **with no plugin** (D349). The design is
[`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §14.3.1; what the test phase and stage 1's spike found
is §14.2.1 and §14.3.14 there.
**`world.event` — one event kind, told apart by `event`.** It is rare (a few an hour), so it is one frame per
world event. **The type is in `event`, never in `type`:** `type` is the envelope's (§8.1), and the sidecar files
a frame on it. The plan's first example put the world event's type there, and the sidecar dropped every one as
untyped on the rig — protocol 13's `world.expired` lesson again (§19).
```json
{"kind":"world.event","type":"event","t":1792041350288,"serverId":"main","wipeId":"w-20261005T165054Z",
"event":"heli.arrived","x":3900.0,"z":-1714.5}
```
Positions are `x` and `z`, rounded to 0.1 m. **The site works out the grid** from them and the map's own
geometry (§17), so the grid has one source. A player is `steamId` and `name`, only where the table says.
**Nothing is said until the world is loaded, nor while the server is quitting**: a booting server spawns the
cargo ship and the vendor it saved, and that is not either of them arriving.
| `event` | Raised from | Carries |
|---|---|---|
| `heli.arrived` | `OnEntitySpawned(PatrolHelicopter)` | `x`, `z` |
| `heli.left` | `OnHelicopterRetire` | `x`, `z` (last seen) |
| `heli.downed` | `OnEntityDeath(PatrolHelicopter)` at the crash | `credit`, `names`, `x`, `z` |
| `bradley.spawned` | `OnBradleyApcInitialize`, next tick. Road Bradleys spawned within a second are one frame | `where`: `launch_site` \| `road`; `count` |
| `bradley.destroyed` | `OnEntityDeath(BradleyAPC)` (D366) | `where`: `launch_site` \| `road` \| `other`; `credit`, `names`, `x`, `z` |
| `cargo.arrived` | `OnEntitySpawned(CargoShip)` | `x`, `z` |
| `cargo.docking` / `cargo.docked` / `cargo.undocked` | `OnCargoShipHarborApproach` / `…Arrived` / `…Leave` | `x`, `z`, `harbour`, `harbourName` |
| `cargo.left` | `OnCargoShipEgress`: it turns for the edge with its radiation on. The kill follows when it is gone | `x`, `z` |
| `chinook.arrived` | `OnEntitySpawned(CH47HelicopterAIController)`, a second later; **not** an oil rig's reinforcement (it has a landing target, or comes within 10 s of a rig's hack) | `x`, `z` |
| `chinook.dropped` | `OnHelicopterDropCrate`, joined to the `OnCrateLanded` of the crate under it (within 40 m and 120 s) | `monument`, `monumentName`, `x`, `z` |
| `chinook.downed` (D379) | `OnEntityDeath(CH47Helicopter)` | `credit`, `names`, `x`, `z` |
| `airdrop.called` | **a cargo plane's spawn**, next tick, once per plane | `steamId`/`name` of a supply signal's thrower; `x`, `z` (the drop point) |
| `airdrop.landed` | `OnSupplyDropLanded`, **once per drop** | `steamId`/`name` of the caller, `x`, `z` |
| `airdrop.looted` | the **first** `OnLootEntity` on a drop | `steamId`/`name`, `x`, `z` |
| `crate.hack.started` | `CanHackCrate` (who), joined to the `OnCrateHack` that follows within 2 s | `where`, `steamId`/`name`, `x`, `z` |
| `crate.unlocked` | `OnCrateHackEnd` | `where`, `steamId`/`name` (the hacker), `x`, `z` |
| `crate.looted` | the first `OnLootEntity` on an unlocked crate | `where`, `steamId`/`name`, `x`, `z` |
| `vendor.arrived` / `vendor.left` | `OnEntitySpawned` / `OnEntityKill(TravellingVendor)` | `x`, `z` |
| `satellite.crashed` | the `SatelliteCrashRemains` spawn at the impact (no hook of its own) | `x`, `z` |
| `deepsea.opened` / `deepsea.closed` | `OnDeepSeaOpened` / `OnDeepSeaClosed` | nothing: the Deep Sea is off the map |
- **`airdrop.called` comes from the plane's spawn**, because the console's `supply.call` raised neither
`OnAirdrop` nor `OnCargoPlaneSignaled` (stage 1). Those two only say who and where: a signal's thrower and its
position, the schedule's drop point. Otherwise the drop point is the plane's own, read from it.
- **`where` for a locked crate** is `oilrig_large`, `oilrig_small` (the `_oilrig` prefab and the nearest
`oilrig_1` / `oilrig_2`), `cargo` (a `CargoShip` parent), `ghostship` (the `_ghostship` prefab) or `chinook`
(a crate seen landing under a Chinook's drop). **Anything else is `other` and says nothing** — Guarded Crate's
crate is an ordinary locked crate — and a crate one of our own steps placed (§15's ledger) is skipped.
- **`where` for a Bradley** is `launch_site` when `BradleySpawner.singleton.spawned` is it, `road` when the road
event's own list holds it, and `other` otherwise. `RoadSpawned` cannot say: it defaults to true, so a plugin's
Bradley would read as a road one. An `other` Bradley's spawn is not news, but its death is sent: it counted
toward Tankbreaker under D174 and still does.
- **`harbour` and `monument`** are the nearest monument's kind (the world verbs' vocabulary, §15) and
`harbourName` / `monumentName` the game's English name for it; absent where the map has none.
- **"Oil rig scientists called" is not a type**: starting a hack on a rig calls them at once, so
`crate.hack.started` with `where: oilrig_*` says it. **Not subscribed:** `OnHelicopterOutOfCrates` (every
tick), `OnPatrolHelicopterTakeDamage` (every hit), `OnDeepSeaTeleport`, the F-15 (D367).
- **The joins are memory only** — plane to thrower, drop to caller, crate to hacker, looted once, a Chinook's
drop — keyed by the entity, dropped when it goes and swept every ten minutes. Nothing is written to disk, so a
reload forgets a crate's hacker and that line goes without a name.
**The credit block (D372, D379).** For a patrol helicopter, a Chinook and a Bradley the plugin keeps a damage
table from `OnEntityTakeDamage` (the hook D174's last-hitter already used, after the same type test, for those
classes only) and sends every candidate. **It credits nobody itself.**
```json
{"kind":"world.event","type":"event","event":"heli.downed","x":812.0,"z":-340.5,
"credit":{"blow":"76561190000000001","last":"76561190000000002","most":"76561190000000002",
"damagers":[{"steamId":"76561190000000002","damage":6120.0},{"steamId":"76561190000000001","damage":3880.0}]},
"names":{"76561190000000001":"Alice","76561190000000002":"Bob"}}
```
- `blow` is the killing blow: for the patrol helicopter, the player the first `OnPatrolHelicopterKill` names (the
crippling hit, seconds before a crash that names nobody); for a Bradley or a Chinook, its death's
`InitiatorPlayer`. `last` is the last player to hurt it, with no window. `most` is the most damage. Each is
absent when nobody qualifies, and a death nobody touched carries `"damagers": []`.
- `damage` is the damage as each hit reported it, summed; at most 32 `damagers`, by damage.
- **The site applies the server's rule** (`creditRule`, below) to the feed line and to the count alike.
**The tally.** `player.tally` gains **`cratesHacked`** (a hack counts for its starter at `crate.hack.started`,
once per crate, from any location but `other`) and **`dropsLooted`** (a drop counts for its first looter),
both sums (D356). **`apcKills` and `heliKills` are no longer sent**: the site counts them from `world.event`'s
credit block. A protocol-13 plugin still sends them, and the site still adds them.
**`world.call` — run one of the game's own world events now, for a run (D354, D367).**
```json
{"cmd":"world.call","reqId":"r-40","runId":"9","key":"9:2","event":"airdrop","count":3,"x":-412.0,"z":1180.5}
{"kind":"world.called","type":"reply","reqId":"r-40","key":"9:2","event":"airdrop","repeat":false,"called":3}
```
- `event` is a schedule's key — `patrol_heli`, `cargo`, `chinook`, `airdrop`, `travelling_vendor`,
`bradley_road`, `f15` — or `satellite`. The plugin runs **one of that schedule's `TriggeredEvent`s at random,
as `EventSchedule.Trigger` does, but not `Trigger` itself**, which would also reset the schedule's own clock.
The schedule is found whether or not it is enabled. The satellite is the game's own `satellite.random`. **No
plugin console command is used**, so it works the same on Carbon.
- **A position**, as `x`/`z` or `monument`/`offsetX`/`offsetZ` (§15's location), **only for `airdrop` and
`patrol_heli`**, which stage 1's spike placed: the airdrop by `InitDropPosition` before it spawns (it releases
0–1 m above the point and lands there), the patrol helicopter by `SetInitialDestination` before it spawns (it
reaches the point within about 50 m, then patrols as usual). **The Chinook takes none (D378)**: it drops only
at the map's drop zones, and the one route to a chosen zone borrows the Airfield call terminal's shared state.
The rest go where the game puts them. A position for any of them is `not-placeable`.
- **Kept.** Nothing a call starts is removed at teardown. The plugin remembers the call only — its key, its run,
what it called — in `data/RunicGateway/world-calls.json` for seven days, so a retried key answers
`repeat: true` with the first answer and calls nothing.
- **Bounds:** `count` 1–5, and 1 for `cargo`, `travelling_vendor` and `bradley_road`; at most
**`EventsMaxWorldCalls`** (config, default and ceiling 10) world events per run, counted over every call the
run made; behind `EventsEnabled`. A count the game partly ran answers `called` below `count`.
- Refusals are `world.error`: `not-ready`, `events-disabled`, `malformed` (no key or run, or a key over 128
characters), `bad-event`, `bad-count`, `not-placeable`, `too-many`, §15's location refusals, and `refused`
(the game had nothing to run).
- `world.monuments` gains `maxWorldCalls`.
**Hello and status gain `console`: `{ "plugins": true | false }` (D363).** Whether this framework runs a plugin's
console command from code, found at every load by running the bridge's own `rg.probe` through
`ConsoleSystem.Run`. The steps that drive another plugin through its console command refuse while it is false.
**The probe runs from a timer**, because stage 1 found Carbon 2.0.262 refuses ("not found") every plugin command
run from *inside* a console command, and from a `NextTick` that a console command queued — which is how §14.2's
spike ran every test — while it finds them from a timer and from the `NextTick` the link's reader queues, which
is every website command's path. On both rigs it reads `true`. Hello is sent again when the answer changes.
**The sidecar** adds `POST /world/call`, a thin forward that keeps nothing.
**The website (Module-Rust).**
- **The feed** serves `world.event` (a public kind), read per viewer by `worldEvents.shapeFor`: an `event` this
build does not know is staff's alone; the event and its **`grid`** are public; a player's **`name`** (and the
names in `credited`) only for the operator's presence audience (D376); a **Steam id and a position only for
staff** (D353). A downed one carries `credited` (`[{ name, steamId? }]`), `creditedCount` and `creditRule`.
- **`rust_servers.credit_rule`**: `blow` (the default: the killing blow, else the last hit), `last`, `most` or
`all` (everyone who hurt it, one each), set on the server's settings. The same rule writes the line and adds
`heli_kills` (patrol helicopters and Chinooks, *Rotorbane*) and `apc_kills` (*Tankbreaker*). A change applies
from then on; counts already written are not recounted.
- **Triggers** `rust.world.<event>`, one per type (D355), ceiling `everyone`, cooldown subject the server and
the event (the crate types add `where`), with `grid`, `player`, `where`, `harbour`, `monument`, `count` and,
for a downed one, `credited`. No Steam id.
- **The step `rust.world.call`**, *Call a world event* (`risk: 'change'`, `reversible: 'none'`, budget
`rust.world.calls`).
- **The tally's two counts** in `rust_player_wipe_stats.crates_hacked` and `.drops_looted`; title categories
`crateshacked` (*Safecracker*) and `dropslooted` (*Scavenger*); leaderboard columns *Crates hacked* and *Drops
looted*, sortable (`sort=cratesHacked`, `sort=dropsLooted`).