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.
|
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
|
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,
|
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, 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
|
**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
|
§14's rows; the in-game rows
|
||||||
@@ -1077,6 +1078,19 @@ 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 |
|
| **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 |
|
| **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 |
|
||||||
|
| **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)
|
## 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
|
The org lead named four plugins the module supports as first-class integrations. Kits already is one. The other
|
||||||
@@ -2334,17 +2348,471 @@ not "this wipe".
|
|||||||
|
|
||||||
### 14.3 The integration plan
|
### 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
|
Written on 2026-10-14 from §14.2.1 and D353–D369, against the code on `edge` (bridge, rust-link and Module-Rust
|
||||||
six it says:
|
at protocol 14 with §2, §11.2, §11.3 and §12 in), and **answered by the org lead before any code**. For §13 and
|
||||||
- what hello reports (`integrations` gains each plugin's `{ loaded, version }`);
|
each of the six it says what hello reports, what goes on the wire, what the site does, which helper (none), the
|
||||||
- the wire additions, in protocol 14 unless Q16 says otherwise;
|
tests and the walk rows. §14.3.9 gathers the points the plan settles itself, so they can be overruled, and
|
||||||
- what the site does: feed, map, steps, triggers, titles, columns, configuration;
|
§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.
|
||||||
- any helper, and why, which D351 makes the exception;
|
|
||||||
- the tests;
|
|
||||||
- the walk rows, which join the end-of-plan walk (D331).
|
|
||||||
|
|
||||||
It also proposes a build order. The working assumption is §13 first (no dependencies), then TruePVE, then Dynamic
|
**Everything here is protocol 14 (D357)**, specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20 as each
|
||||||
PVP, then the other four.
|
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 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) | 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` |
|
||||||
|
| `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).
|
||||||
|
|
||||||
|
**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).**
|
||||||
|
|
||||||
|
```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"*. **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.<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 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.
|
||||||
|
- **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; 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, 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.
|
||||||
|
|
||||||
|
#### 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"*, 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* (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`,
|
||||||
|
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: 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 (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
|
||||||
|
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, D374)
|
||||||
|
|
||||||
|
**Hello.** `integrations.zLevels`: `{ loaded, version }`.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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). A *Double XP weekend* is a lend step.
|
||||||
|
|
||||||
|
**Tests.** The hello field and the servers page.
|
||||||
|
|
||||||
|
**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)
|
||||||
|
|
||||||
|
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**, `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.
|
||||||
|
|
||||||
|
**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`; `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 |
|
||||||
|
|
||||||
|
`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
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
- **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, 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.
|
||||||
|
|
||||||
|
#### 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: <triggerId>, where: <conditions>, 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:<triggerId>`, 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
|
### 14.4 Questions for the org lead
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user