docs(rust): the §14.3 integration plan for world events and six plugins (D370–D377) #347
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user