docs(rust): the §14.3 integration plan for world events and six plugins (D370–D377) #347

Merged
whitlocktech merged 3 commits from docs/rust-integration-plan-14-3 into main 2026-10-15 02:30:17 +00:00
Showing only changes of commit d99c8e15b5 - Show all commits

View File

@@ -927,7 +927,8 @@ pulls) is research, not a walk, so it still comes first:
8. **Permissions as event rewards** (§12), built on 2026-10-12.
9. **Rust's own world events and six more optional plugins** (§13, §14). First comes one test phase covering
all of them on both rigs (§14.2, D352); **it ran on 2026-10-14 (§14.2.1)**. Then one integration plan,
answered before code (§14.3), which starts from D361–D369. Then the build, §13 first.
answered before code: **written on 2026-10-14 as §14.3**, from D361–D369, with Q26–Q32 open. Then the
build, in §14.3.11's seven stages, §13 first.
**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
@@ -2334,17 +2335,373 @@ not "this wipe".
### 14.3 The integration plan
Written after §14.2, in one docs PR, and answered by the org lead before any code. For §13 and for each of the
six it says:
- what hello reports (`integrations` gains each plugin's `{ loaded, version }`);
- the wire additions, in protocol 14 unless Q16 says otherwise;
- what the site does: feed, map, steps, triggers, titles, columns, configuration;
- any helper, and why, which D351 makes the exception;
- the tests;
- the walk rows, which join the end-of-plan walk (D331).
Written on 2026-10-14 from §14.2.1 and D353–D369, against the code on `edge` (bridge, rust-link and Module-Rust
at protocol 14 with §2, §11.2, §11.3 and §12 in), and **answered by the org lead before any code**. For §13 and
each of the six it says what hello reports, what goes on the wire, what the site does, which helper (none), the
tests and the walk rows. §14.3.9 gathers the points the plan settles itself, so they can be overruled, and
§14.3.10 asks Q26–Q32.
It also proposes a build order. The working assumption is §13 first (no dependencies), then TruePVE, then Dynamic
PVP, then the other four.
**Everything here is protocol 14 (D357)**, specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20 as each
piece lands, and released with the rest after the end walk. **No helper plugin is needed (D351):** every route
below is a public hook, a callable method or a console command that §14.2 ran. **The installer and the egg are
not touched:** `requires_plugins` stays Kits, ZoneManager and RunicNPC, and none of the six is shipped (D351).
#### 14.3.1 Rust's own world events (§13; D349, D353–D356, D366, D367)
**Hello.** Nothing new: the events need no plugin. Hello gains one fact the steps below need, **`console`**:
`{ plugins: true | false }`, found at load by running one of the bridge's own read-only commands through
`ConsoleSystem.Run`, the route a plugin command takes. On Carbon 2.0.262 it is `false` (D363), and the site
refuses a step that needs another plugin's console command, with the reason and Rust-Plugins#34. When Carbon
fixes it, the next load says `true` and the steps work, with no new code.
**One new event kind, `world.event`, with a `type`.** It is rare (a few an hour), so it is one frame per event.
Positions are `x`, `z`; **the site computes the grid**, as the map already does, so the grid has one source. A
player is `steamId` and `name`.
| `type` | Raised from (§14.2.1) | Carries |
|---|---|---|
| `heli.arrived` | `OnEntitySpawned(PatrolHelicopter)` | `x`, `z` |
| `heli.left` | `OnHelicopterRetire` | `x`, `z` (last seen) |
| `heli.downed` | the first `OnPatrolHelicopterKill` that names a player, else D174's last-hitter at the crash (Q28) | who, `x`, `z` |
| `bradley.spawned` | `OnBradleyApcInitialize`. **Road Bradleys come twelve at once, so their spawns in one second are one frame with `count`** | `where: launch_site \| road`, `count` |
| `bradley.destroyed` | `OnEntityDeath(BradleyAPC)` (D366) | who, `where`, `x`, `z` |
| `cargo.arrived` / `cargo.docking` / `cargo.docked` / `cargo.undocked` | `OnEntitySpawned(CargoShip)`, `OnCargoShipHarborApproach`, `…HarborArrived`, `…HarborLeave` | `x`, `z`; the three harbour types add `harbour` (the nearest `harbor_*`) |
| `cargo.left` | `OnCargoShipEgress`, which did not fire within §14.2's window; else the ship's `OnEntityKill`. Stage 1 confirms which | `x`, `z` |
| `chinook.arrived` | `OnEntitySpawned(CH47HelicopterAIController)`, **except the oil-rig reinforcement**, which is told by the crate hack it answers | `x`, `z` |
| `chinook.dropped` | `OnHelicopterDropCrate`, then the crate's `OnCrateLanded` | `monument` (nearest), `x`, `z` |
| `airdrop.called` | `OnCargoPlaneSignaled` (who = the signal's thrower) or `OnAirdrop` (the schedule: nobody) | who if any, `x`, `z` (the drop point) |
| `airdrop.landed` | `OnSupplyDropLanded`, **once per drop** (it fired twice) | `x`, `z`, the caller remembered from plane to drop |
| `airdrop.looted` | the **first** `OnLootEntity` on a drop | who, `x`, `z` |
| `crate.hack.started` | `CanHackCrate` (who) joined to the `OnCrateHack` that follows for the same crate | who, `where`, `x`, `z` |
| `crate.unlocked` | `OnCrateHackEnd` | `where`, `x`, `z`, the hacker |
| `crate.looted` | the first `OnLootEntity` on an unlocked crate | who, `where` |
| `vendor.arrived` / `vendor.left` | `OnEntitySpawned` / `OnEntityKill` (`TravellingVendor`) | `x`, `z` |
| `satellite.crashed` | the `SatelliteCrashRemains` spawn at impact (no hook of its own) | `x`, `z` |
| `deepsea.opened` / `deepsea.closed` | `OnDeepSeaOpened` / `OnDeepSeaClosed` | **No position:** the Deep Sea is off the map |
- **`where` for a locked crate** is `oilrig_large`, `oilrig_small`, `cargo`, `ghostship` or `chinook`, each told
positively as §14.2.1 found (the `_oilrig` prefab and nearest rig, parent `CargoShip`, the `_ghostship` prefab,
a crate landed after `OnHelicopterDropCrate`). **Anything else is `other` and raises no frame**, so a
plugin's crate (§14.2.1: Guarded Crate's is an ordinary `HackableLockedCrate`) is never reported as Rust's own.
A crate one of our steps placed is in the world ledger and is skipped as well.
- **"Oil rig scientists called" is not a separate type.** Starting a hack at a rig is what calls them, at once, so
`crate.hack.started` with `where: oilrig_*` says it.
- **Hooks not subscribed:** `OnHelicopterOutOfCrates` (every tick), `OnPatrolHelicopterTakeDamage` (every hit;
D174's hit list already comes from `OnEntityTakeDamage`), `OnDeepSeaTeleport`, the F-15 (D367).
- **Bounded state.** The joins above (plane → thrower, crate → hacker, looted-once sets) are keyed by net id,
dropped at the entity's kill, and swept every ten minutes; nothing is written to disk.
**The tally** gains `cratesHacked` and `dropsLooted` (D356): a hack counts for its starter at `crate.hack.started`,
once per crate, from any location except `other`; a drop counts for its first looter. New columns
`crates_hacked`, `drops_looted` in `rust_player_wipe_stats`; title categories **`crateshacked` (*Safecracker*)** and
**`dropslooted` (*Scavenger*)**, `sum` sources, renamable (D175). `heli.downed` and `bradley.destroyed` keep
counting `heliKills` and `apcKills` through the same credit, so the feed line and *Rotorbane* never disagree
(Q28).
**The step that calls one: `world.call`, site → game (D354, D367).**
```json
{"cmd":"world.call","reqId":"r-40","key":"9:2","event":"airdrop","count":3,"x":-412.0,"z":1180.5}
{"kind":"world.called","type":"reply","reqId":"r-40","key":"9:2","repeat":false,"called":3}
```
- `event` is one of the schedule keys §14.2.1 found (`patrol_heli`, `cargo`, `chinook`, `airdrop`,
`travelling_vendor`, `bradley_road`, `f15`) or `satellite`. It runs the game's own `TriggeredEvent.RunEvent`
(`satellite.random` for the satellite), so it works the same on Carbon: **no plugin console command is used.**
- **A position** is offered only where the game has a route to one. §14.2 ran the events where the game puts
them; placing them was not tested. The first build stage starts with a short spike (research, D331) on the
airdrop's drop point, the patrol helicopter's first destination and the Chinook's drop target. An event whose
route does not hold is offered without a position, and the step's form says the game decides where.
- **Kept (`reversible: 'none'`)**: nothing is removed at teardown, because a cargo ship or a drop may already hold
players or their loot (D354). `risk: 'change'`.
- **Bounds:** `count` 1–5 (1 for cargo, the vendor and road Bradleys, which the game runs one at a time), at
most `EventsMaxWorldCalls` per run on the plugin. A new budget, **`rust.world.calls`**, counts each call.
Refusals are `world.error`: `events-disabled`, `bad-event`, `bad-count`, `not-placeable`, `too-many`.
- The sidecar adds `POST /world/call`, a thin forward.
**The website (Module-Rust).**
- **The feed.** `world.event` joins the catalogue, filtered **per `type`**. Each line is written from the type, e.g.
*"Patrol helicopter downed by Alice at G7"*, *"Bob started hacking the locked crate on Large Oil Rig"*,
*"12 road Bradleys are out"*, *"The Deep Sea is open"*. **Public with grid and name, staff adds the Steam id
(D353)**; how that meets the presence rule is Q32.
- **Triggers (D355).** One engagement trigger per type, `rust.world.<type>` (`rust.world.heli.downed`,
`rust.world.crate.hack.started` …), `kind: 'event'`, ceiling `everyone`, cooldown subject the server and type
(the crate types add `where`), with typed variables (`server`, `grid`, `player`, `where`, `harbour`,
`monument`, `count`). Because a phase gate's `{ on: … }` reads the same catalogue, **"advance when Bradley is
destroyed" works with no new code**. Starting a run from a trigger is not something core does today (Q26).
- **The step `rust.world.call`**, *"Call a world event"*: server, event, count, and the location params of the
place verbs where the event allows a position (above).
- **Titles and columns:** *Safecracker* and *Scavenger* (above); the leaderboard gains the two columns.
- **The map** already draws these entities; nothing changes there.
**Tests.** Bridge: `checkPlugin.js` (hook names, the console probe). Module-Rust: the catalogue holds every type
against PROTOCOL.md's table (default deny for an unknown type); each feed line; the trigger declarations and
their examples; the `world.call` verify and bounds; the two title categories and columns; the ingest of the two
tally fields.
**Walk rows (D331).** On both rigs, with timers shortened as §14.2 did: force each type and read its feed line,
staff line and trigger; down a helicopter with a crash long after the hit; hack a crate at each location and
see *Safecracker* count; loot a drop first and see *Scavenger*; call an airdrop at a position and a patrol
helicopter from a run, then cancel the run and see both stay; a Guarded Crate crate raising no Rust frame.
#### 14.3.2 TruePVE in the zone step (D362, D368)
**Hello.** `integrations.truePve`: `{ loaded, version }`.
**The wire.** `world.zone` gains an optional **`truePve`**: `{ "exclude": true }` or `{ "ruleset": "pvp-arena" }`
(exactly one). The plugin makes the zone, then calls `AddOrUpdateMapping(zoneId, "exclude" | ruleset)`.
- **All or nothing.** A `false` from TruePVE (no such ruleset) erases the zone just made and answers
`world.error` `truepve-ruleset` with the name. TruePVE absent answers `truepve-missing`.
- **The unmapping lives with the zone, on the plugin (D362).** Every path that erases one of our zones (the
zone's own deadline, which runs with the site down; teardown; the boot sweep of PROTOCOL.md §15.3) calls
`RemoveMapping` first. When TruePVE is not loaded at that moment, the id is kept in
`data/RunicGateway/truepve.json` and removed at the next `OnPluginLoaded(TruePVE)`. The registry entry
remembers that the zone was mapped, so nothing else of TruePVE's table is ever touched.
- `OnUpdatedMappings` / `OnRemovedMappings` are not subscribed: the call's own return says what happened.
**The website.** `rust.zone.open` gains **`noPvpProtection`** (D334's three-button boolean, *"No PvP protection in
this zone"*) and **`pvpRuleset`** (a string, *"or this TruePVE ruleset"*). Setting both is refused at verify. Either
one refuses at verify where TruePVE is absent ("TruePVE is not installed on this server"). A name TruePVE does not
know fails the step at perform, with the name, because only the call can tell (D368).
**Tests.** The two params' verify (both set, absent TruePVE, blank); the wire shape; a `truepve-ruleset` reply
classified as a non-retryable failure.
**Walk rows.** Map `exclude` and a typed ruleset on both rigs and hurt a stand-in inside; type a wrong name; let
a zone expire on its deadline with the site stopped and see the mapping go; unload TruePVE, end a run, load it
and see the leftover removed.
#### 14.3.3 Dynamic PVP
**Hello.** `integrations.dynamicPvp`: `{ loaded, version }`.
**The wire.** Two events, `pvp.zone.created` `{ zoneId, event, x, z, radius, durationSec }` (−1 for "as long as
the ship") and `pvp.zone.deleted` `{ zoneId }`, from `OnCreatedDynamicPVP` and `OnDeletedDynamicPVP`. The radius
is read from ZoneManager, since the hook does not carry it. `map.live` gains a **`pvp`** layer of the live ones,
kept by the same two hooks and seeded at load from ZoneManager's zones whose ids TruePVE maps for Dynamic PVP.
`OnPlayerEnterPVP` / `ExitPVP` are not subscribed: they would say who stands where, and exit fired twice.
**The website.** The map draws the `pvp` layer as a red ring with its event's name. The feed's public line is
*"A PvP zone opened around the downed patrol helicopter at G7"*, which names nobody. Its deletion is a staff
line only. No trigger and no step: Dynamic PVP's zones are its own to make.
**Tests.** The layer's ingest and the viewer filter; the feed lines; the default deny until the kinds are
classified.
**Walk rows.** With a radius set for the helicopter and Bradley: down each, see the ring and the line, and see
both go when the zone lasts out; cargo's ring for the ship's life; one of our zones beside theirs, neither touched.
#### 14.3.4 Dangerous Treasures (D365)
**Hello.** `integrations.dangerousTreasures`: `{ loaded, version }`.
**The wire.**
- Events **`dt.started`** `{ x, z, key? }`, **`dt.won`** `{ steamId, name, invaders: [steamId…], x, z }`,
**`dt.ended`** `{ x, z }`, from `OnDangerousEventStarted`, `…Won` and `…Ended`. `key` is set when the start
answers one of our steps: the first `OnDangerousEventStarted` within ten seconds of our `dtevent`.
- Command **`dt.start`** `{ key, level }` (0 Easy, 1 Medium, 2 Hard) runs `dtevent <level>` through
`ConsoleSystem.Run`. An empty reply is success. *"Unable to obtain a valid position"* within the first
fifteen minutes after DT loaded is **`dt-not-ready`**; after that, `dt-no-spot`. `console-unavailable` when hello's
`console.plugins` is false (D363). The key is remembered like a payment's, so a retry never starts a second.
- `map.live`'s `world` gains `kind: "treasure"` for each running event, from the same hooks.
- The tally gains **`treasuresWon`** for the looter of `dt.won`.
**The website.**
- **The step `rust.dt.start`**, *"Start a Dangerous Treasures event"*: server and difficulty; DT picks the spot.
`reversible: 'none'` (DT despawns its own), `risk: 'change'`, a new budget `rust.dt.events`. It refuses at
verify where DT is absent or `console.plugins` is false, naming Rust-Plugins#34 for the second.
- **D365.** `dt-not-ready` is reported as *"Dangerous Treasures is not ready yet"*. **The retry after ten minutes is
Q27**: core's step retry today is a flat 60 s, three times, so ten minutes needs either a small core addition or
the bridge holding the request. The manual retry is core's own: the step's `on_failure` pauses the run, and
staff press **Retry** on the paused step ([EVENTS.md](../../website/EVENTS.md) §I), which re-sends the same key.
- **Feed and triggers.** *"A Dangerous Treasures event started at K12"*; *"Alice won the treasure at K12"*;
ended is staff-only. `rust.dt.started` and `rust.dt.won` are triggers (§14.3.9).
- **A column and a title**, `treasureswon`, default *Treasure Hunter* (Q31).
- The map draws a treasure marker.
**Tests.** The step's verify (absent, console unavailable), each error's wording, the key join on `dt.started`,
the column and category.
**Walk rows.** On Oxide: start each difficulty from a run; start one in the first minutes after a restart and
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).
#### 14.3.5 Guarded Crate, with NpcSpawn (D361, D364)
**Hello.** `integrations.guardedCrate`: `{ loaded, version, npcSpawn: { loaded, version }, events: […] }`.
`events` are the event names in Guarded Crate's config file (Easy, Medium, Hard and Elite by default), read as
the bridge already reads Kits' and Economics' (PROTOCOL.md §16.3), and absent when the file cannot be read.
**The wire.**
- Events **`gc.started`** `{ x, z, key? }`, **`gc.ended`** `{ steamId?, name?, x, z }` (no player = no winner),
**`gc.failed`** `{ x, z }`, from the three hooks. `key` as for DT.
- Command **`gc.place`** `{ key, event, x, y, z }` runs `gcrate position <event> x,y,z` (the handler checks
`IsAdmin`, which the server console passes). It refuses with `npcspawn-missing`, `bad-event`,
`console-unavailable` or `gc-missing`.
- `map.live`'s `world` gains `kind: "guarded"` while one runs.
- The tally gains **`guardedWon`** for the player `gc.ended` names (Q31).
**The website.**
- **The step `rust.gc.place`**, *"Place a guarded crate"*: server, event (the names from hello), and the place
verbs' location params (monument, grid or point). **Kept: nothing is removed at teardown (Q29).** There is no
way to end one instance, and `gcrate stop all` would end other people's and raise no hook. A new budget
`rust.gc.crates`.
- **D364:** it refuses at verify where NpcSpawn is absent: *"Guarded Crate is loaded but NpcSpawn is missing: its
events cannot start."* The servers page shows NpcSpawn beside Guarded Crate.
- **Feed and triggers.** *"A guarded crate landed at M4"*; *"Alice cleared the guarded crate at M4"*; failed is a
staff line. `rust.gc.started` and `rust.gc.ended` are triggers (§14.3.9).
- **RunicNPC needs nothing:** its guards are `scientists` to factions (§14.2.1).
- **The operator docs** ([OPERATING.md](OPERATING.md)) gain a section on Guarded Crate: install NpcSpawn from
Codefling, its `data/Images/` files, and that **it contacts its author's server at load** (D364).
**Tests.** Verify (NpcSpawn, a name not in `events`, console unavailable), the null-winner frame, the column.
**Walk rows.** On Oxide: place one at a monument and at a point from a run, clear it and see the line, the
column and the trigger; let one time out; cancel the run and see the crate stay; unload NpcSpawn and see the
step refused with the reason. On Carbon: refused (D363).
#### 14.3.6 ZLevels Remastered (D369)
**Hello.** `integrations.zLevels`: `{ loaded, version }`.
**The wire.** The tally gains **`levels`**: `{ acquire, crafting, mining, skinning, woodcutting, fishing }`, each a
value read with **`GetLevel` only** (never `api_GetPlayerInfo`, which creates records). It is read when a player
connects and at each flush for the players in it (six calls of about 3 µs each), and sent when one moved.
There is no level-up hook, so a level is as fresh as the last flush.
**The website.**
- **Stored per server, not per wipe (D369):** a new table `rust_player_levels` (`server_id`, `steam_id`, the six
levels, `updated_at`), a value written, never summed, and nothing done at a wipe.
- **The leaderboard and titles** are Q30. Whatever the answer, the column is "current level", on servers that
report ZLevels, and shown for every wipe view because it does not belong to one.
- **XP boosts need no code** (§14.2.1): `rust.perm.lend` already lends `zlevelsremastered.vip1`–`vip3`, and its
picker lists them once ZLevels registers them (§12.2).
**Tests.** The tally's `levels` ingest as values; the new table's upsert; the columns and categories Q30 decides.
**Walk rows.** Gain a level and see it after the next flush; lend `vip2` from a run and see the XP rate change;
wipe and see the levels stay.
#### 14.3.7 Server Rewards (D369)
It mirrors §11.2.2 (Economics) wherever Server Rewards allows, and the code shares Economics' path where the two
are the same.
**Hello.** `integrations.serverRewards`: `{ loaded, version }`. Server Rewards has no starting balance, so a new
player is at 0.
**The wire.**
- **`points.pay`** `{ key, amount, steamIds[], message, delivery }`, answered by `points.paid` with `paid` (before
and after) and `failed`, the key remembered seven days, the message told once, exactly as `economics.pay`.
**Whole numbers only, 1 to 1,000,000.** The bridge refuses anything else itself, because the id overloads
accept a negative amount and invert it (§14.2.1).
- **`points.balances`** `{ steamIds[] }` → `{ balances: [{ steamId, points }] }`, bounded like `economics.balances`.
- **`points.adjust`** `{ key, steamId, op: add | take, amount }` → `{ before, after }`. There is no `set`, because
Server Rewards has no call for it. `take` above the balance is refused with `insufficient`, as Server Rewards
refuses it.
- The tally gains **`points`**, a value, read at connect and from `OnPointsUpdated(ulong, int)`.
- The sidecar adds `POST /points/pay`, `/points/balances`, `/points/adjust`.
**The website.**
- **The step `rust.points.pay`**, *"Pay RP"*: kept like coins (D335's reasoning), the same recipient modes, a new
budget `rust.points`.
- **Balances** in a new table, **`rust_player_points`** (`server_id`, `steam_id`, `points`),
per server and not per wipe (D369). The player's Rust page, the user's admin page and the app (R10) show it beside
the coins.
- **Staff add and take** from the user's admin page, each an activity row, like Economics.
- **A column and a title** (Q31).
**Tests.** The same set as Economics': the amount bounds (0, negative, fractional), a repeat key, `insufficient`,
the value ingest, the admin route and its activity row, the app's screen.
**Walk rows.** Pay three players, one offline, and see the popup on wake; retry the step and see no second
payment; add and take from the admin page, including more than the balance; read the balance on the site and
in the app; wipe and see the points stay.
#### 14.3.8 The wire, all together
| Kind | New in protocol 14 |
|---|---|
| Hello | `console.plugins`; `integrations.truePve`, `dynamicPvp`, `dangerousTreasures`, `guardedCrate` (with `npcSpawn`, `events`), `zLevels`, `serverRewards` |
| Events | `world.event` (23 types); `pvp.zone.created` / `deleted`; `dt.started` / `won` / `ended`; `gc.started` / `ended` / `failed` |
| Commands | `world.call`; `dt.start`; `gc.place`; `points.pay` / `balances` / `adjust`; `world.zone`'s `truePve` |
| Tally | `cratesHacked`, `dropsLooted`, `treasuresWon`, `guardedWon`; `levels` and `points` (values) |
| `map.live` | `world` kinds `treasure` and `guarded`; the `pvp` layer |
| Sidecar | `POST /world/call`, `/dt/start`, `/gc/place`, `/points/pay`, `/points/balances`, `/points/adjust`, thin forwards that keep nothing |
`PROTOCOL_VERSION` stays 14 (D332); `overlay.toml` already says 14.
#### 14.3.9 Settled in this plan, and overrulable
- **One `world.event` kind with a `type`**, filtered per type (§13.2's shape), rather than twenty-three kinds.
- **The site computes the grid** from `x`, `z`; the Deep Sea's events carry no position.
- **Road Bradleys' spawn is one line with a count**; Launch Site's is its own line.
- **Oil-rig reinforcements are `crate.hack.started` at a rig**, not a type of their own.
- **A crate is Rust's own only when its location is told positively**; anything else raises no frame.
- **A hack counts for *Safecracker* when it starts**, for the player `CanHackCrate` named, once per crate.
- **Dangerous Treasures' and Guarded Crate's starts and wins are triggers too**, as world events are (D355).
Dynamic PVP's zones are feed and map only.
- **A run's DT or guarded crate is told from the plugin's own** by the first start within ten seconds of our
command, which is the only join the hooks allow.
- **The DT and GC steps refuse on a server whose `console.plugins` is false**, rather than being hidden.
- **Server Rewards' wire is `points.*`, beside `economics.*`**, not one verb with a currency field: protocol 14
is unreleased, but Economics' verbs are built and specified, and two names keep each plugin's refusals its own.
#### 14.3.10 Questions for the org lead
- **Q26 — Can a world event start a run?** D355 makes every world event a trigger, and §13.4's example was
*"when anyone destroys Bradley, start the Launch Site Lockdown event"*. Today a trigger can **advance a phase**
of a run already going (`{ on: 'rust.world.bradley.destroyed' }`) and can send a notice, but **core cannot start a
run from a trigger**: a run starts by its schedule or by hand. **Proposed:** add it in core as a definition's
*"start when"* (a trigger, conditions and a cooldown, so Bradley dying twice in a minute starts one run), on
the website's own plan, in its own PR with the Module-uo proof, as §2 was. Or leave it out for now, and an
event that needs it opens with a phase that waits for the trigger.
- **Q27 — DT's ten-minute retry (D365).** Core retries a failed step every 60 s, three times, so a step cannot
say "try me again in ten minutes". For example, a *Treasure Hunt* at 20:00 after a 19:55 restart fails at
20:00, 20:01 and 20:02, then pauses the run for staff. **Proposed:** a small core addition, `retryAfterMs` on a
step's failure (bounded to 30 min, counted as one attempt), a MODULE_API minor bump. The other way is the bridge
holding the request and starting DT itself ten minutes later, which means a step that "succeeded" before
anything started.
- **Q28 — Who downed the helicopter.** D174 credits the last player to hurt it within 60 s of the crash. The test
found the game names the player at the **crippling hit**, and a crippled helicopter flew 34.7 s on Carbon before
crashing. One that glides 70 s would credit nobody today. **Proposed:** credit the player named at the crippling
hit, falling back to D174's rule, for the feed line and *Rotorbane* alike (amending D174).
- **Q29 — A guarded crate outlives its run.** There is no way to end one guarded crate: `gcrate stop all` ends
every one on the server, and raises no hook. For example, a run cancelled five minutes after placing one
leaves it, with its eight guards, for up to 30 minutes, until it is cleared or times out. **Proposed:** accept
that. The step says so, and the run log notes the crate left standing.
- **Q30 — ZLevels on the leaderboard.** Six skills. For example: **(a)** one *Level* column, the total of the six,
and one title *Grandmaster* for the highest total; **(b)** six columns (*Woodcutting*, *Mining* …) and six
titles; **(c)** one *Level* column whose skill the viewer picks, with six title categories an admin may use or
not. **Proposed:** (c).
- **Q31 — Titles for the plugins' counts.** For example: *Treasure Hunter* for most DT treasures won, *Warden* (?)
for most guarded crates cleared, *Hoarder* (?) for the most RP. **Proposed:** all three as title categories
with those default names, renamable (D175), and a leaderboard column for each on servers that report the
plugin. RP ranks only above 0.
- **Q32 — A public line that names a player, and the presence rule.** The rule of 2026-09-22 says nothing tells who
is online by default, and the public feed already hides `npc.boss.died` (it names the killer) below the
operator's presence audience. D353 says the public line names the player. For example, with presence left at
staff, does a signed-out visitor see *"Patrol helicopter downed by Alice at G7"*, *"Patrol helicopter downed at
G7"*, or nothing? **Proposed:** the middle one. The event is public, and the name shows only to the presence
audience, so a line with nobody in it (the cargo docking, the Deep Sea) is always public.
#### 14.3.11 Order of building
Each stage is built on `edge`, compiled on both rigs, and PR'd with its PROTOCOL.md section; nothing is walked
until the end (D331, D359). The org lead is asked before each stage starts.
1. **§13, the world events**, starting with the placement spike for `world.call`. It has no plugin dependency
and carries the console probe the later stages use.
2. **TruePVE** in the zone step.
3. **Dynamic PVP** (it needs TruePVE).
4. **Dangerous Treasures**, after Q27's core change if that is the answer.
5. **Guarded Crate**.
6. **ZLevels**.
7. **Server Rewards**.
Q26's core work, if wanted, is a website PR of its own and can be built alongside any stage.
### 14.4 Questions for the org lead