From d99c8e15b5fb880ab96bbe3db10c5e138a45ed71 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 14 Oct 2026 19:14:00 -0500 Subject: [PATCH 1/3] =?UTF-8?q?docs(rust):=20the=20=C2=A714.3=20integratio?= =?UTF-8?q?n=20plan=20for=20world=20events=20and=20six=20plugins=20(Q26-Q3?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes PLAN_REDESIGNS §14.3 from the §14.2 test phase and D353-D369: Rust's own world events as one world.event kind (23 types), a step that calls a vanilla event, Safecracker and Scavenger; TruePVE in the zone step; Dynamic PVP on the map; Dangerous Treasures and Guarded Crate steps, feed, triggers and counts; ZLevels levels and Server Rewards points as per-server values. No helper plugin, installer and egg untouched, all on protocol 14. Settled points are listed as overrulable, Q26-Q32 asked, and a seven-stage build order proposed. §9 item 9 points at it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 379 ++++++++++++++++++++++++++++++++- 1 file changed, 368 insertions(+), 11 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 4cf223c..b3bff4f 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -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.` (`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 ` 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 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 -- 2.49.1 From 2163bf7aebb59a2daa87d0e5f4791b1de9d5f8da Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 14 Oct 2026 21:26:25 -0500 Subject: [PATCH 2/3] docs(rust): record the answers to Q26-Q32 (D370-D376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D370 a trigger can start a run: core's `starts` (trigger, conditions, cooldown, one run at a time), written as §14.3.12. D371 a step's retryAfterMs, written as §14.3.13, used by DT's warm-up. D372 the operator picks the helicopter/Bradley credit rule (killing blow, last hit, most damage, everyone); the plugin sends every candidate and the site counts, amending D174. D373 a guarded crate outlives its run. D374 no leaderboard change for ZLevels, so its levels wire and table are dropped. D375 Treasure Hunter, Warden, Hoarder. D376 the public line names a player only for the presence audience. Build order is now nine stages. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 202 +++++++++++++++++++++++++-------- 1 file changed, 156 insertions(+), 46 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index b3bff4f..f4c8053 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -927,8 +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: **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. + 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. **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 @@ -1078,6 +1078,18 @@ The org lead answered the phase's Q19–Q25 on 2026-10-14: | **D368** | **TruePVE in the zone step (Q24):** a *"no PvP protection in this zone"* checkbox (`exclude`), plus an optional ruleset name typed by staff, which the bridge validates by `AddOrUpdateMapping`'s own `false`. | Checkbox only; reading TruePVE's config file. | 14.2.1, 3 | | **D369** | **Wipes mirror the plugin (Q25):** the site shows what ZLevels and Server Rewards hold and does nothing at a wipe; ZLevels columns are *current level*, not *this wipe*. | Per-wipe snapshots of our own; a site-side wipe reset. | 14.2.1 | +The org lead answered §14.3's Q26–Q32 on 2026-10-14: + +| # | Decision | Rejected | § | +|---|---|---|---| +| **D370** | **A trigger can start a run (Q26).** Core gains a definition's `starts` (a trigger, conditions and a cooldown), one run at a time per definition, `event.*` triggers excluded; core work on the website's own plan, its own PR with the Module-uo proof. | Only phase gates; an event that waits for the trigger in its first phase. | 14.3.12 | +| **D371** | **A failed step may name its retry delay (Q27):** `retryAfterMs`, 1–30 min, counted as one attempt, a MODULE_API minor bump. DT's warm-up retries ten minutes later through it. | The bridge holding the request and starting DT itself later. | 14.3.4, 14.3.13 | +| **D372** | **The operator chooses who is credited with a helicopter or a Bradley (Q28):** killing blow (the default; for the helicopter, the crippling hit, else the last hit), last hit, most damage, or everyone who hurt it. The plugin sends every candidate; the site applies the server's rule to the feed line and to *Rotorbane* / *Tankbreaker* alike. **This amends D174.** | The crippling hit as the one fixed rule; D174's 60 s last-hitter. | 14.3.1 | +| **D373** | **A guarded crate outlives its run (Q29):** nothing is removed at teardown; the step says so and the run log notes a crate left standing. | Ending every guarded crate on the server with `gcrate stop all`. | 14.3.5 | +| **D374** | **The leaderboard is not changed for ZLevels (Q30):** no column and no title, because most of its skills already line up with counts of ours. ZLevels is reported at hello, and its XP boosts are lent as permissions (§12). | One total column; six columns; one column with a skill picker. | 14.3.6 | +| **D375** | **Three new titles (Q31):** *Treasure Hunter* (DT treasures won), *Warden* (guarded crates cleared), *Hoarder* (the most RP, above 0), renamable, each with a column on servers that report the plugin. | No titles for the plugins' counts. | 14.3.4, 14.3.5, 14.3.7 | +| **D376** | **A public line names a player only for the presence audience (Q32):** the event and its grid are public (*"Patrol helicopter downed at G8"*), and the name shows only to the audience the operator chose for presence. | The name always public (D353 as written); the whole line hidden below the presence audience. | 14.3.1 | + ## 11. First-class optional plugins (D199–D202) The org lead named four plugins the module supports as first-class integrations. Kits already is one. The other @@ -2339,7 +2351,7 @@ Written on 2026-10-14 from §14.2.1 and D353–D369, against the code on `edge` 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. +§14.3.10 asks Q26–Q32, answered as D370–D376; §14.3.12 and §14.3.13 are the two pieces of core work they add. **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 @@ -2362,9 +2374,9 @@ player is `steamId` and `name`. |---|---|---| | `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` | +| `heli.downed` | the crash's `OnEntityDeath`, with the first `OnPatrolHelicopterKill`'s player kept as the killing blow | every credit candidate (D372, below), `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` | +| `bradley.destroyed` | `OnEntityDeath(BradleyAPC)` (D366) | every credit candidate (D372), `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` | @@ -2394,9 +2406,37 @@ player is `steamId` and `name`. **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). +**`dropslooted` (*Scavenger*)**, `sum` sources, renamable (D175). + +**Who gets the credit for a helicopter or a Bradley is the operator's choice (D372).** The plugin no longer +decides. For each patrol helicopter and Bradley it keeps a small damage table from `OnEntityTakeDamage`, the +hook that already feeds D174's last-hitter, after the type test it already does, and for those two classes +only. `heli.downed` and `bradley.destroyed` carry every candidate: + +```json +{"kind":"world.event","type":"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's player: for the helicopter, the player the first `OnPatrolHelicopterKill` names at + the crippling hit; for a Bradley, its death's `InitiatorPlayer`. `last` is the last player to hurt it, with no + 60 s window. `most` is the player who dealt the most damage. Each is absent when nobody qualifies. +- **The site applies the server's rule**, a setting beside the server's other Rust settings: + + | Rule | Credits | For example (Bob did 61 %, Alice the crippling hit) | + |---|---|---| + | **Killing blow** (default) | `blow`, else `last` | Alice | + | **Last hit** | `last` | whoever hit it last: Alice or Bob | + | **Most damage** | `most` | Bob | + | **Everyone who hurt it** | every `damagers` entry, one each | Alice and Bob, one each | + +- **The same rule writes the feed line and the count**, so the line and *Rotorbane* / *Tankbreaker* never + disagree. Under protocol 14 the site counts `heli_kills` and `apc_kills` from these frames, and the tally's + `heliKills` / `apcKills` are no longer sent. Under *Everyone*, the line reads *"Patrol helicopter downed by + Alice, Bob and 2 others at G7"*. +- Changing the rule applies from then on; past counts are not recounted. **The step that calls one: `world.call`, site → game (D354, D367).** @@ -2423,13 +2463,15 @@ counting `heliKills` and `apcKills` through the same credit, so the feed line an - **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. + *"12 road Bradleys are out"*, *"The Deep Sea is open"*. **The event and its grid are public; the player's name + shows only to the operator's presence audience (D376)**, so with presence left at staff a signed-out visitor + reads *"Patrol helicopter downed at G8"*. Staff see the name and the Steam id (D353). A line with nobody in it + (the cargo docking, the Deep Sea) is the same for everyone. - **Triggers (D355).** One engagement trigger per type, `rust.world.` (`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). + destroyed" works with no new code**. **Starting a run from one is new core work (D370, §14.3.12).** - **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. @@ -2438,10 +2480,12 @@ counting `heliKills` and `apcKills` through the same credit, so the feed line an **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. +tally fields; each of D372's four rules against one `credit` block (one with no `blow`, one with nobody at all), +for the line and the count alike; a presence-gated line with and without the name (D376). **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 +staff line and trigger, signed out and as staff; down a helicopter with a crash long after the hit, and a +Bradley with two players, under each of the four credit rules; 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. @@ -2512,13 +2556,14 @@ both go when the zone lasts out; cargo's ring for the ship's life; one of our zo - **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. +- **D365.** `dt-not-ready` is reported as *"Dangerous Treasures is not ready yet"*, and the step answers + `{ ok: false, retry: true, retryAfterMs: 600000 }`: **core's new per-failure retry delay (D371, §14.3.13)**, so + the run tries once more ten minutes later. `dt-no-spot` retries on core's ordinary 60 s. 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). +- **A column and a title**, `treasureswon`, default *Treasure Hunter* (D375). - The map draws a treasure marker. **Tests.** The step's verify (absent, console unavailable), each error's wording, the key join on `dt.started`, @@ -2541,13 +2586,15 @@ the bridge already reads Kits' and Economics' (PROTOCOL.md §16.3), and absent w `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 tally gains **`guardedWon`** for the player `gc.ended` names: a column and a title, `guardedwon`, default + *Warden* (D375). **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`. + verbs' location params (monument, grid or point). **Kept: nothing is removed at teardown (D373).** There is no + way to end one instance, and `gcrate stop all` would end other people's and raise no hook. The step's + description says so, and when a run ends with one still standing, the run log notes *"a guarded crate at M4 + was left standing; it ends when cleared or after its own time"*. 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 @@ -2562,27 +2609,25 @@ the bridge already reads Kits' and Economics' (PROTOCOL.md §16.3), and absent w 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) +#### 14.3.6 ZLevels Remastered (D369, D374) **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 leaderboard does not change (D374).** It gains no ZLevels column and no ZLevels title. Most of its six +skills already line up with counts of ours: woodcutting with *Timberwright*, mining with *Prospector*, crafting +with *Seamster* and *Armorer*, acquire (plants) with *Greenhand*, and skinning sits beside the animal kills +(*Beastbane*). Fishing has no count of ours, and stays that way. So **no levels go on the wire +and nothing is stored**: the `levels` tally field and the `rust_player_levels` table this plan first proposed are +dropped. D369 still holds in what remains: nothing is done at a wipe. -**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. +**What the integration is:** +- **Hello reports it**, and the servers page shows it beside the other optional plugins. - **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). + picker lists them once ZLevels registers them (§12.2). A *Double XP weekend* is a lend step. -**Tests.** The tally's `levels` ingest as values; the new table's upsert; the columns and categories Q30 decides. +**Tests.** The hello field and the servers page. -**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. +**Walk rows.** Lend `vip2` from a run and see the XP rate change at once, and back at the run's end. #### 14.3.7 Server Rewards (D369) @@ -2611,7 +2656,9 @@ player is at 0. 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). +- **A column and a title**, `points`, default *Hoarder*, a value like the richest (D375): ranked only above 0, on + servers that report Server Rewards, and shown for every wipe view because the points do not belong to one + (D369). **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. @@ -2627,7 +2674,7 @@ in the app; wipe and see the points stay. | 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) | +| Tally | `cratesHacked`, `dropsLooted`, `treasuresWon`, `guardedWon`; `points` (a value). **`heliKills` and `apcKills` leave the tally**: the site counts them from `world.event`'s `credit` (D372) | | `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 | @@ -2651,6 +2698,10 @@ in the app; wipe and see the points stay. #### 14.3.10 Questions for the org lead +**All seven were answered on 2026-10-14 (D370–D376, §10).** Q26, Q27, Q29, Q31 and Q32 as proposed (Q26 is +written as §14.3.12, Q27 as §14.3.13). **Q28 differs:** the operator chooses the credit rule, from four options +(D372). **Q30 differs:** the leaderboard is not changed for ZLevels at all (D374). + - **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 @@ -2693,15 +2744,74 @@ Each stage is built on `edge`, compiled on both rigs, and PR'd with its PROTOCOL 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**. + and carries the console probe the later stages use, and D372's credit rule. +2. **Core: a step's retry delay** (§14.3.13, D371), a website PR with the Module-uo proof. It is small and comes + before stage 5, which needs it. +3. **TruePVE** in the zone step. +4. **Dynamic PVP** (it needs TruePVE). +5. **Dangerous Treasures**, on stage 2's `retryAfterMs`. +6. **Guarded Crate**. +7. **ZLevels** (hello only, D374). +8. **Server Rewards**. +9. **Core: starting a run from a trigger** (§14.3.12, D370), a website PR of its own with the Module-uo proof, + as §2 was. It can be built alongside any stage after 1; it is last here only because nothing else waits on it. + Module-Rust needs no code for it, because the world events are already triggers. -Q26's core work, if wanted, is a website PR of its own and can be built alongside any stage. +#### 14.3.12 Core: starting a run from a trigger (D370) + +**What it gives.** *"When anyone destroys Bradley on Main, start Launch Site Lockdown"*; *"when the Deep Sea opens, +start Sea Raiders"*; on UO, *"when a champion's boss appears, start Champion's Call"*. Today a trigger can only +advance a phase of a run already going, or send a notice (§14.3.1). This is core work on the website's own plan +([EVENTS.md](../../website/EVENTS.md) gains it when it is built). It is written here because this plan asked for it, +and no module contract changes: the triggers it starts from are the ones modules already register. + +**The design, to be settled again in EVENTS.md at build time:** +- **A definition's version gains `starts`**: a list of `{ on: , where: , cooldown }`, + beside its schedule (a definition may have either, or both). `on` and `where` are a phase gate's own + (EVENTS.md §E), the same catalogue, operators and form, so *"`where: { server: 'main', where: 'launch_site' }`"* is + already a thing the editor can draw. +- **The emit path starts it.** When a trigger fires, core finds the `ready` definitions whose published + version starts on it and whose conditions match, and creates a run of that version **now**, with + `scheduled_for` = the firing's time, so the existing `UNIQUE (definition_id, scope, scheduled_for)` keeps one + firing from starting two runs. `started_by` is `trigger:`, and the firing's variables are stored on + the run, so its page says *"Started because Bradley was destroyed at G8"*, and the audit says who and what. +- **One run at a time, and a cooldown.** A firing while a run of that definition is going, or within its + `cooldown` (default 30 minutes, 0 to 7 days) of the last trigger-started run, starts nothing. It is counted, + and the definition's page shows *"2 firings ignored while running"*. This is what stops twelve road Bradleys dying + in a minute from starting twelve runs. +- **No loops.** Core's own `event.*` triggers cannot start a run, so a run starting can never start another. + A run whose own steps cause its start trigger (an event that calls a patrol helicopter and starts on one + being downed) is held by one run at a time and the cooldown. +- **Everything else is a run like any other:** caps and budgets, verify before publish, the same roles that may + schedule one (admin and editor), the same announcements and cleanup. A definition that starts from a trigger + whose module is not loaded is dormant, as a phase gate on one is. + +**Tests (website).** A matching firing starts one run; a second firing during it, and one in the cooldown, start +nothing and are counted; two instances emitting the same firing make one run; an `event.*` trigger is refused in +`starts`; a dormant trigger; the run's origin on its page and in the audit. **Module-uo proof:** one definition +starting on `uo.champ.boss_up`. + +**Walk rows.** On a Rust rig: *"start when Bradley is destroyed"* with a short cooldown. Destroy two in a minute and +see one run and one ignored firing; destroy one after the cooldown and see a second run. + +#### 14.3.13 Core: a step's retry delay (D371) + +**What it gives.** A failed step may say when to try again: `{ ok: false, retry: true, retryAfterMs }`. Today every +retry waits the flat `EVENT_STEP_RETRY_MS` (60 s). DT's warm-up (D365) is the first user: *"not ready yet; try in +ten minutes"*. + +- **Bounded** 60 000 to 1 800 000 ms (one to thirty minutes). Outside that it is clamped, because this is the + module's hint, not an author's value (D95 is about the second). Absent, the flat delay as today. +- **It counts as one attempt** of `EVENT_STEP_MAX_ATTEMPTS`, and the run goes `degraded` on it as on any retry + (EVENTS.md §L). The run console shows *"retrying at 20:10"*. +- **A run that is paused or cancelled while it waits** does not retry; the existing staff Retry on a paused step + still works at any time. +- **MODULE_API minor bump** (additions only). [`STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md) has claimed + 1.15.0; whichever lands first takes it, the other the next minor. Module-uo needs no change. The Integration Kit's + `coreApi` equality check turns red on the bump, as it is meant to, and its pin moves when the chapters are re-read. + +**Tests (website).** The delay honoured; clamped at both ends; counted as an attempt; no retry for a run paused +or cancelled in the wait; `MODULE_API.md` and the classify rules updated. ### 14.4 Questions for the org lead -- 2.49.1 From 3505eacdfcb91ef400a41b1855f540c025c16a86 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 14 Oct 2026 21:29:13 -0500 Subject: [PATCH 3/3] docs(rust): D377, the leaderboard changes only for new, substantial things The org lead confirmed that new titles may add their columns (Safecracker, Scavenger, Treasure Hunter, Warden, Hoarder) and that a small plugin's own numbers, such as ZLevels' levels, never become leaderboard columns. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index f4c8053..2b10439 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -1089,6 +1089,7 @@ The org lead answered §14.3's Q26–Q32 on 2026-10-14: | **D374** | **The leaderboard is not changed for ZLevels (Q30):** no column and no title, because most of its skills already line up with counts of ours. ZLevels is reported at hello, and its XP boosts are lent as permissions (§12). | One total column; six columns; one column with a skill picker. | 14.3.6 | | **D375** | **Three new titles (Q31):** *Treasure Hunter* (DT treasures won), *Warden* (guarded crates cleared), *Hoarder* (the most RP, above 0), renamable, each with a column on servers that report the plugin. | No titles for the plugins' counts. | 14.3.4, 14.3.5, 14.3.7 | | **D376** | **A public line names a player only for the presence audience (Q32):** the event and its grid are public (*"Patrol helicopter downed at G8"*), and the name shows only to the audience the operator chose for presence. | The name always public (D353 as written); the whole line hidden below the presence audience. | 14.3.1 | +| **D377** | **The leaderboard changes only for something new and substantial.** A new title may add its column (*Safecracker*, *Scavenger*, *Treasure Hunter*, *Warden*, *Hoarder* do), but a small plugin's own numbers, such as ZLevels' levels, never become columns (org lead, 2026-10-14, confirming D374 beside D375). | Adding a column for every number a plugin reports. | 14.3.1, 14.3.6 | ## 11. First-class optional plugins (D199–D202) @@ -2698,7 +2699,7 @@ in the app; wipe and see the points stay. #### 14.3.10 Questions for the org lead -**All seven were answered on 2026-10-14 (D370–D376, §10).** Q26, Q27, Q29, Q31 and Q32 as proposed (Q26 is +**All seven were answered on 2026-10-14 (D370–D376, §10), and D377 states the leaderboard rule behind Q30 and Q31.** Q26, Q27, Q29, Q31 and Q32 as proposed (Q26 is written as §14.3.12, Q27 as §14.3.13). **Q28 differs:** the operator chooses the credit rule, from four options (D372). **Q30 differs:** the leaderboard is not changed for ZLevels at all (D374). -- 2.49.1