diff --git a/modules/rust/OPERATING.md b/modules/rust/OPERATING.md index b798c43..7d2dc10 100644 --- a/modules/rust/OPERATING.md +++ b/modules/rust/OPERATING.md @@ -5,8 +5,8 @@ configure and permission plugins. This is the surface our own deployment story h of, the way [`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of ServUO. > **Mirrored verbatim from uMod on 2026-09-15** — 6 pages. uMod is upstream and normative. -> The last section, [Guarded Crate and NpcSpawn](#guarded-crate-and-npcspawn), is Runic Gateway's own and -> is not part of the mirror. +> The last two sections, [Guarded Crate and NpcSpawn](#guarded-crate-and-npcspawn) and +> [RaidableBases](#raidablebases), are Runic Gateway's own and are not part of the mirror. --- @@ -19,6 +19,7 @@ of, the way [`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of - [Data Files](#data-files-plugins-data-files) — Where plugin data lives on disk - [Permissions](#permissions-plugins-permissions) — Granting permissions and managing groups in-game - [Guarded Crate and NpcSpawn](#guarded-crate-and-npcspawn) — Runic Gateway's note on an optional plugin that needs a Codefling extension +- [RaidableBases](#raidablebases) — Runic Gateway's note on changing RaidableBases' config and switching its edition --- @@ -410,3 +411,29 @@ not check and would drop a crate with no guards. Admin → Rust servers shows Np as it ships), not its config. The site offers whatever names that file holds. A crate placed by an event is **kept when the event ends** (D373): Guarded Crate has no way to end one event, and `gcrate stop all` ends every one on the server. + +--- + + + +## RaidableBases + +Runic Gateway's own section, not mirrored. Design: [PLAN_REDESIGNS.md](PLAN_REDESIGNS.md) §11.4; wire: +[PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.12; found in the end walk +([PLAN_LAST_FIXES.md](PLAN_LAST_FIXES.md) N1, N2). + +**RaidableBases** is one of the optional plugins Runic Gateway reports and drives, in either edition (D348). +Two of its own behaviours catch operators out. + +**Change its config with a server restart, never a reload (N2).** *Applying changes* above says to reload a +plugin after editing its config. Do not do that with RaidableBases while the server is running. After +`oxide.reload RaidableBases`, its list of live raids can hold an empty entry. Every hook that walks that list +then fails: damage to a base, its SAM sites, players' commands, and `/buyraid`'s cooldown check, so a purchase +does nothing and charges nothing. The end walk saw this after reloads with and without bases up, and only a +full server restart cleared it. It is RaidableBases' own bug. Carbon's `c.reload` was not tried in the end walk, so treat it +the same way: edit the config, then restart the server. + +**Switching from the paid edition to the free one rewrites the paid profiles (N1).** Putting the free +edition's `RaidableBases.cs` in place of the paid one rewrites the profiles a paid base pack installed, and +their bases are dropped from them. Going back to the paid edition does not bring them back. Back up +RaidableBases' config and data folders before you switch editions, and restore them if you switch back. diff --git a/modules/rust/PLAN_LAST_FIXES.md b/modules/rust/PLAN_LAST_FIXES.md index bd73f19..0c77994 100644 --- a/modules/rust/PLAN_LAST_FIXES.md +++ b/modules/rust/PLAN_LAST_FIXES.md @@ -1,7 +1,9 @@ # `module-rust` — the last fixes before the cutover -**Status:** plan, written 2026-10-21 after the end walk's in-game pass. Nothing in it is built yet. Each fix waits -for the org lead's go, and the questions in §5 are answered before the code they shape. It continues +**Status:** **stages 1–5 done, 2026-10-22.** Every fix in §3 is built, and §8 names the PR for each. The +questions in §5 were answered on 2026-10-21, and the stage 4 decisions on 2026-10-22. Stage 6, the release and +the cutover, waits for the org lead's go, and §7's clean-up comes after it. Written 2026-10-21 after the end +walk's in-game pass, as a plan. It continues [`PLAN_REDESIGNS.md`](PLAN_REDESIGNS.md) §9.1, which lists W1–W20 and N1 from the passes without a player. This document adds what the in-game pass found and puts everything in one list, in the order it should be built. @@ -142,6 +144,18 @@ retry a step that is still pending. site now has title rules for the richest, raids, treasures and guarded crates. Keep both, or remove them with the rest of §7? +**Answered 2026-10-21, each with the recommended option:** + +- **Q37: (c), and stricter at boot (2026-10-22).** One game module per site. The proposal was a clear message + when the second module's slot collides. The org lead chose a full stop instead: the first module directory by + name loads, and every other is refused before its code is required, as the installer already refuses one. + No MODULE_API change ([`MODULE_SYSTEM.md`](../../website/MODULE_SYSTEM.md) §2.5, + [`MODULE_API.md`](../../website/MODULE_API.md) §4.2). +- **Q38:** the release and the cutover do not wait for §6's second-player rows. They are walked when a second + player is free. +- **Q39: (a).** PLAN_REDESIGNS §14.3.4 drops "the manual retry". Docs only. +- **Q40: keep both.** BetterChat stays on the Oxide rig, and the four title rules stay on the walk site. + --- ## 6. Still owed, with a second player @@ -153,14 +167,30 @@ rows of 4.4. They are written in PLAYER_WALK and need no change. ## 7. Cleaning up after the walk -Done on the org lead's go, after §5's Q40: +Done on the org lead's go, after stage 6. Q40 keeps BetterChat on the Oxide rig and the title rules on the walk +site, so neither is in this list: - **Oxide rig:** remove A from the owners (`removeowner`, then `server.writecfg`). Remove the walk-only RgKitProbe plugin, which holds god mode and a teleport command, and the RgWalk stand-in plugin. Restore RaidableBases' config from the walk's backup, which takes away the 100-coin Easy and Medium buy costs, and **restart** the server rather than reloading (N2). -- **Walk site:** remove A's test grants (`backpacks.use` on Oxide; the `backpacks.use` given on Carbon), and - the title rules if Q40 says so. +- **Walk site:** remove A's test grants (`backpacks.use` on Oxide; the `backpacks.use` given on Carbon). - **Steam:** rotate the Steam Web API key, which was pasted into the chat that ran the walk, and put the new one in Admin → Auth → Steam. -- **W21** stays patched only on the walk site until §4 step 1 lands. Nothing else depends on the walk site's copy. +- **The rigs' bridge:** both rigs still run stage 3's branch (Rust-Plugins#43), now merged to `edge`. The release + puts the released bridge on them. +- **W21** landed in stage 1 (Module-Rust#54), so the walk site's hand patch is no longer needed. + +--- + +## 8. What was built + +One PR per repo per stage, each merged into `edge` (the Rust repos and website) or `main` (docs) by the org lead. + +| Stage | PR | Fixes | +|---|---|---| +| 1, the two bugs (2026-10-21) | Module-Rust#54 | **W21**: the push sends `notices`. **W4**: the credit rule is stamped on each helicopter and Bradley row at ingest and read from the stamp; a row from before the fix is read by the current rule | +| 2, Module-Rust's small fixes (2026-10-21) | Module-Rust#55 | W3 (reads the bridge's `already-out` as a success that called 0), W6, W7 and W11 (a "not loaded" refusal is confirmed against the sidecar, at most every 5 s and within 3 s), W8, W10, W13–W16. **W17**: the sync state only, *"in sync · N wait for their player to join"*; the run log is unchanged, because core has no way for a module to add a line to it. **W18**: the number is dropped from the note | +| 3, the bridge (2026-10-21) | Rust-Plugins#43 | W3 (`world.error` `already-out`), W9 (RaidableBases' own log lines since the write), W12 (`raid.ended` carries `id`, and `runId` and `key` for a run's base). No protocol bump, because PROTOCOL already listed W12's fields. Walked on both rigs | +| 4, core (2026-10-22) | website#219, Module-Rust#56 | **W19**: one module per site at boot (Q37). W1: a zone reports `until`, and a reconcile files a resource gone past it as `expired`. W5: a draft's passing dry run carries over by spec fingerprint. W2: *"Phase p1: steps done, waiting to advance"*. No MODULE_API bump. Walked on the walk site | +| 5, docs (2026-10-22) | docs (this stage) | N1 and N2 in OPERATING.md. W20 in PLAN_REDESIGNS §14.3.4 (Q39). W22 and W23 in PLAYER_WALK, PROTOCOL §20.12 and PLAN_REDESIGNS §11.4. The wire changes stages 2–3 made without a doc edit (PROTOCOL §20.5's `already-out`, §20.12's rollback log). Stage 4's core docs: MODULE_SYSTEM §2.5, MODULE_API §4.2 and the resource `until`, EVENTS §D, §I, §K and §L, and BACKEND_DESIGN's three new columns | diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 595be03..70dfc01 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -14,8 +14,8 @@ raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). D342–D346), planned and built the same day. On 2026-10-14 RaidableBases was deferred until the current paid copy arrives (D347), with both editions to be supported (D348, §11.4.2). The same day the org lead added Rust's own world events to the base module (§13, D349) and six more first-class optional plugins (§14, D350–D352), to be tested together in one phase before their integration plan. Their questions were answered the same day (D353–D359, the end walk now waiting for RaidableBases), and Steam sign-in stays on the website's plan, linked as §15 (D360). **The §14.2 test phase ran the same day on both rigs (§14.2.1)**: the org lead ruled D361–D362 during it and answered its Q19–Q25 the same day (D363–D369). On 2026-10-16 the org lead added console commands as event steps and rewards (§16, D382–D384), to be built before RaidableBases; **they were built the same day (§16.10)**, with D385. The same day the paid RaidableBases copy arrived; **its spike ran on both rigs (§11.4.2)**, with D386 and four questions, Q33–Q36, answered the same day (D387–D390). **§11.4 was built the same day (§11.4.6)**, with D391, core's dry-run advice. On 2026-10-19 the org lead answered Steam sign-in's questions (D392–D396, §15), and it is built on the website's `edge` to join the cutover. **Every item left is built, tested and PR'd first; every walk — theirs and the ones still owed by §3 and §5 — happens together at the end (D331, §9).** **The end walk's rows that need no player ran on 2026-10-16 and 2026-10-19, and all of them are done (§9.1)**: W1–W20 and N1 found, W4 -and W19 the real ones. **The in-game pass ran on 2026-10-21** and found W21–W23 and N2; every fix left is listed, -in building order, in [`PLAN_LAST_FIXES.md`](PLAN_LAST_FIXES.md). +and W19 the real ones. **The in-game pass ran on 2026-10-21** and found W21–W23 and N2. **All of them were fixed +by 2026-10-22** ([`PLAN_LAST_FIXES.md`](PLAN_LAST_FIXES.md)); the protocol 14 release and the cutover come next. This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**. @@ -986,8 +986,8 @@ lead, 2026-10-19).** - **§5.7 and §3.4** passed in RunicNPC's 9d (stand-ins on 2026-10-07, the org lead in game on 2026-10-09), so they are not walked again. -**Found** (none fixed yet; each waits for the org lead; the whole list, with the in-game pass's W21–W23 and N2, is -[`PLAN_LAST_FIXES.md`](PLAN_LAST_FIXES.md)): +**Found** (all fixed by 2026-10-22, in [`PLAN_LAST_FIXES.md`](PLAN_LAST_FIXES.md)'s stages 1–5, which also list the +in-game pass's W21–W23 and N2 and the PR that fixed each): | # | Where | What | |---|---|---| @@ -1189,7 +1189,7 @@ The org lead answered the phase's Q19–Q25 on 2026-10-14: |---|---|---|---| | **D363** | **Carbon's console route (Q19): watch Carbon's updates and keep an issue open in Rust-Plugins so it is not forgotten** ([Rust-Plugins#34](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/issues/34)). No workaround is built for now; each new Carbon production build is re-tested. | Running commands through Carbon's `CommandManager` by reflection now. | 14.2.1 | | **D364** | **A missing NpcSpawn is reported and refused (Q20):** the hello reports NpcSpawn beside Guarded Crate, the site refuses a guarded-crate step without it and says why, and the operator docs say what NpcSpawn does on load. | Documenting it only. | 14.2.1 | -| **D365** | **Dangerous Treasures' warm-up (Q21):** a step that fails because DT is not ready says so (*"Dangerous Treasures is not ready yet"*), retries once on its own after ten minutes, **and offers staff a manual retry** on the run. | Failing with DT's own text; the bridge pre-warming DT at boot. | 14.2.1 | +| **D365** | **Dangerous Treasures' warm-up (Q21):** a step that fails because DT is not ready says so (*"Dangerous Treasures is not ready yet"*), retries once on its own after ten minutes, **and offers staff a manual retry** on the run. *(The manual retry was dropped on 2026-10-21, Q39: core retries only a failed step, and the automatic retry is enough. §14.3.4.)* | Failing with DT's own text; the bridge pre-warming DT at boot. | 14.2.1 | | **D366** | **One Bradley event, two places (Q22):** `bradley.destroyed` carries `where: launch_site | road`, and both count toward *Tankbreaker*. | Two separate events; Launch Site only. | 14.2.1, 13 | | **D367** | **The newer events join §13 (Q23):** the satellite crash, the Deep Sea opening and closing, and the ghost ship's crate (a fourth crate location) are feed events and triggers. **The F-15 flyover is not an event; it can be added to an event as decoration**, a step that calls the flyover (D354). | The F-15 as a feed event or trigger. | 14.2.1, 13 | | **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 | @@ -1865,6 +1865,10 @@ still there. It is private and not a `[HookMethod]`, but **`Call` reaches it on despawn) and `OnRaidableBasePurchased` (8 arguments, the same without the load time) come only from `TrySetPayLock`. That runs when a player buys a base, or when an admin runs `rbevent setowner [lockout]`. `OnRaidableBaseUnlocked` (8 arguments) is its undoing. + **Which of the two it raises** follows `paylock = !args.Contains("lockout")`: a purchase and the plain + `rbevent setowner ` raise `OnRaidableBasePurchased`, and only `rbevent setowner lockout` + raises `OnRaidableBaseLocked`. *(Corrected 2026-10-22: this read as if the plain command locked. The end + walk's in-game pass showed it reads as "bought", [PLAN_LAST_FIXES.md](PLAN_LAST_FIXES.md) W23.)* - **Locking to the first attacker raises no hook**, and neither does a **lockout** (the per-player cooldown after a raid, kept in the plugin's own data). So D202's "the feed carries lockouts" still has nothing to listen to (Q36). @@ -2984,9 +2988,11 @@ both go when the zone lasts out; cargo's ring for the ship's life; one of our zo 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. + the run tries once more ten minutes later. `dt-no-spot` retries on core's ordinary 60 s. **There is no manual + retry** (Q39, 2026-10-21). Core's **Retry** ([EVENTS.md](../../website/EVENTS.md) §I) is for a step that has + failed, and a step waiting out `retryAfterMs` is still pending ("a pending step cannot be retried", even with + the run paused). It is not needed either: DT's warm-up is fifteen minutes, so the automatic retries succeed by + the third attempt (W20, found in the end walk). - **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). @@ -2996,7 +3002,7 @@ both go when the zone lasts out; cargo's ring for the ship's life; one of our zo 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. +see *"not ready yet"* and the automatic retry; win one and see the line, the column and the title. On Carbon: the step refused with Rust-Plugins#34 (until D363 clears). > **Built on 2026-10-15** (§14.3.18), specified in PROTOCOL.md §20.8. The key's join, how a start that opened diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index 332eca9..b819098 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -451,6 +451,12 @@ live walk ([`STEAM_SIGNIN.md`](../website/STEAM_SIGNIN.md) §6) comes first beca the rest. Linking A's Steam account on the site makes it A's Rust link (D392), and every reward row below needs a linked account. +> **Walked 2026-10-21** with A, on Rust 25902425, Oxide 2.0.7830 and Carbon 2.0.262. The rows A could walk +> alone passed, except that 4.1 stopped at steamcommunity.com by choice and 4.4 was not run. The results, and +> what is still owed with B, are in +> [`PLAN_LAST_FIXES.md`](../modules/rust/PLAN_LAST_FIXES.md) §1 and §6. Rows 2.2, 2.8 and 2.9 and *Before you +> start* item 8 were corrected afterwards (W22, W23), so they read as the game behaves. + **Before you start:** 1. **Both rigs on the latest builds.** Rust, Oxide and Carbon production builds are checked and @@ -475,6 +481,11 @@ below needs a linked account. `127.0.0.1`). 7. **For row 2.5:** A's Steam name changed for the length of that row to one with a space and a quote, such as `Walk "Q" A`. Rust reads the name at connect, so A rejoins after changing it. +8. **Titles in chat, for rows 2.1, 2.4, 2.12 and 2.13.** **BetterChat** 5.2.15 on both rigs, because titles + reach chat only through it (PROTOCOL §18). On the walk site, a title rule on each server for the four columns + those rows read: raids (*Warlord*), the richest (*Magnate*), `treasureswon` (*Treasure Hunter*) and + `guardedwon` (*Warden*). *(Added 2026-10-22, W22: the first copy of this list named neither, and the Oxide + rig's BetterChat and the title rules were set up during the walk.)* ### Part 1 — Steam sign-in, on the web @@ -493,14 +504,14 @@ below needs a linked account. | # | Do this | You should see | |---|---|---| | 2.1 | **RaidableBases.** Staff spawn a raid base from a run near A. A raids it to completion | The feed names A, the raids column counts it, and *Warlord* is earned | -| 2.2 | A buys a base with RaidableBases' buy command, then staff lock one to A (`rbevent setowner A`) | A feed line for the purchase and one for the lock (D390) | +| 2.2 | A buys a base with RaidableBases' buy command, then staff lock one to A (`rbevent setowner A lockout`) | A feed line for the purchase and one for the lock (D390). Without `lockout`, `rbevent setowner A` reads as a second purchase, not a lock (W23) | | 2.3 | Staff spawn a base from a run with PvP off, a set NPC count, and locked to A. A raids it | Each setting holds in the game: no PvP damage, that many NPCs, and nobody but A can raid it | | 2.4 | **Economics.** A pay step pays the top three of a tally. A is one of the three (the other two are stand-ins) and is **offline** at the payout. A then joins | A popup on waking with the amount. A's balance shows on the Rust account page. Once A is the richest above the starting balance, *Magnate* shows in chat | | 2.5 | **Command reward**, online: a *Reward by console command* step with `{steamid}` and `{name}` to A, under the name from item 7 | The command runs with A's id and the whole name, quote included, and A gets the reward | | 2.6 | **Backpacks.** A fills a backpack and a second page. Staff open the staff view | Both pages, full | | 2.7 | A opens their own backpack on the Rust account page, then requests a stand-in's | A's own shows. Another player's is refused | -| 2.8 | A *Bigger backpack* step for A. A puts items on the extra page, and the run ends. Repeat with A's inventory full | First the items move to A's inventory, then they land on the ground | -| 2.9 | **Permission rewards (§12.9, D333).** A *Permissions until the event ends* step with `autodoors.use`, A online. Repeat with A offline at the grant, then A joins | The message in chat and as a popup, online and on the next wake. Doors close by themselves while the run lasts and stop when it ends | +| 2.8 | A *Bigger backpack* step for A. A puts items on the extra page, and the run ends. Repeat with A's inventory full | Backpacks first moves the items into free slots on the pages A keeps. Only what does not fit there goes to A's inventory, and with the inventory full it lands on the ground. To see the inventory and the ground, fill the pages A keeps first (W22) | +| 2.9 | **Permission rewards (§12.9, D333).** A *Permissions until the event ends* step with `autodoors.use`, A online. Repeat with A offline at the grant, then A joins | The message, online and on the next wake. It comes in chat **or** as a popup, whichever the step chose, not both (W22); run it once each way to see both. Doors close by themselves while the run lasts and stop when it ends | | 2.10 | Staff grant A `autodoors.use` for good first, then repeat 2.9 | At the run's end A **keeps** it (one already held is kept) | | 2.11 | **Carbon:** A joins the Carbon rig once, then a *Bigger backpack* step and a §12.9 lend and give to A | All three **land** in the game. A Steam id Carbon has never seen cannot get them (§29.8, W17), which is why stand-ins could not prove this | | 2.12 | **Dangerous Treasures:** a chest started from a run, and A wins it | The feed line, the `treasureswon` column, *Treasure Hunter* | @@ -528,9 +539,11 @@ below needs a linked account. ### Carbon's share of the first player walks -[`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) §8 asks for the Carbon subset of the walks above. What RunicNPC's -9d and the step-2 walk already covered is left out. That leaves: the events walk's steps 1–5, and the rewards walk's -steps 1, 3 and 6, on Carbon with A. The map walk's Carbon steps are in 3.4. +**On Carbon, only row 2.11 is walked** (the org lead, 2026-10-21). The bridge file is the same on both +frameworks, and Carbon's halves of every integration passed without a player on 2026-10-19 +([`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §9.1), so the rest of the Carbon subset that +[`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) §8 asked for (the events walk's steps 1–5 and the rewards walk's +steps 1, 3 and 6) was dropped. The map walk's Carbon steps are still in 3.4, which needs B. **What counts as a pass:** a Steam account linked on the site signs its owner in and is their Rust link, an unlinked one signs nobody in, and someone else's Rust link cannot be taken. A real player is paid, rewarded, diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 7e4863c..a939a4f 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -2989,8 +2989,12 @@ credit block. A protocol-13 plugin still sends them, and the site still adds the **`EventsMaxWorldCalls`** (config, default and ceiling 10) world events per run, counted over every call the run made; behind `EventsEnabled`. A count the game partly ran answers `called` below `count`. - Refusals are `world.error`: `not-ready`, `events-disabled`, `malformed` (no key or run, or a key over 128 - characters), `bad-event`, `bad-count`, `not-placeable`, `too-many`, §15's location refusals, and `refused` - (the game had nothing to run). + characters), `bad-event`, `bad-count`, `not-placeable`, `too-many`, §15's location refusals, `refused` + (the game had nothing to run), and **`already-out`**: a `travelling_vendor` call while a Travelling Vendor is + already out. The game runs one at a time, so nothing is called, counted toward `EventsMaxWorldCalls` or kept + (W3, Rust PLAN_LAST_FIXES; before, the call answered `called: 1` and spawned nothing). Module-Rust reads it as + a success that called 0, and the step's note says *"a Travelling Vendor is already out on Main, and the game + runs one at a time; nothing new was called"*. - `world.monuments` gains `maxWorldCalls`. **Hello and status gain `console`: `{ "plugins": true | false }` (D363).** Whether this framework runs a plugin's @@ -3568,8 +3572,11 @@ the old assembly, and the helper patches again. `rgr.status` prints its state. - **The unload trap (§11.4.2):** a RaidableBases unload raises `Completed` for every live base with no owner and no raiders. **Such a completion is neither sent nor counted.** - `raid.purchased` and `raid.locked` come from the paid edition's `OnRaidableBasePurchased` and - `OnRaidableBaseLocked`: a player bought a base, or an admin ran `rbevent setowner` (D390). **Lockouts have no - hook in either edition and are not sent.** + `OnRaidableBaseLocked` (D390). A player buying a base raises `raid.purchased`. An admin's + **`rbevent setowner lockout`** raises `raid.locked`. **The plain `rbevent setowner ` raises + `raid.purchased`**, as if the player had bought it, because RaidableBases' `TrySetPayLock` takes + `paylock = !args.Contains("lockout")` (W23, found in the end walk). **Lockouts** (the per-player cooldown + after a raid) **have no hook in either edition and are not sent.** - `despawnAt` is the plugin's despawn time, in unix milliseconds. - **None is sent while the world is not live** (§20.5's rule). A completion is still counted. @@ -3666,7 +3673,12 @@ reloads the profiles as `rb.reloadprofiles` does**, and the reply is `pending` a (§19.1). The `config.outcome` (`reload: "RaidableBases"`) settles when the helper's reload finishes: every written profile in the plugin's table is `reloaded`; one that is not is **rolled back** and the old file reloaded. Thirty seconds without a finished reload rolls back too. RaidableBases rewrites every profile file as -it reloads, so the outcome carries each file's version as it is on disk afterwards. +it reloads, so the outcome carries each file's version as it is on disk afterwards. **A rollback's log is +RaidableBases' own** (W9, Rust PLAN_LAST_FIXES): the bridge marks the server log's position when the job +starts, and reads back only RaidableBases' entries written since (tagged `[RaidableBases]`, which is how it +logs on both frameworks, or its title), without their stacks. When the failed profiles are named, only the +entries that name them are kept. When RaidableBases said nothing, there is no log, and the reason line stands +alone. Before, the outcome carried the tail of the server log, which is mostly other plugins' lines. **Hooks.** `OnRaidableBaseStarted`, `OnRaidableBaseEnded`, `OnRaidableBaseCompleted`, `OnRaidableBasePurchased` and `OnRaidableBaseLocked` join `rg.hooks`' list. diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index 4389d39..653e291 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -1192,6 +1192,8 @@ arrives with the calendar. | concurrency_key | VARCHAR(190) NULL | stored as the **template** (`invasion:{region}`), rendered from a run's own params at materialisation. A flat definition-id key would wrongly stop one definition running in two regions at once | | grace_seconds | INT NOT NULL DEFAULT 900 | a schedule that passed this long ago while the process was down is `missed`, never a late silent start. Validated 60..86 400 | | timezone | VARCHAR(64) NOT NULL DEFAULT 'UTC' | IANA, and it belongs to the **event**: every listing this replaces is written in the shard's local zone, and a recurrence computed in UTC puts a Friday-8pm event at 7pm for half the year. Validated against the platform's own tzdata via `Intl.DateTimeFormat` | +| draft_verified_at / draft_verified_by | DATETIME NULL / INT NULL | the last dry run that **passed on the draft**, and who ran it (W5, Rust `PLAN_LAST_FIXES.md`). No foreign key on `_by`, as `event_versions.verified_by` has none | +| draft_verified_hash | CHAR(64) NULL | the sha256 of that draft's validated spec as canonical JSON (keys sorted). Publish compares it with the spec it freezes; equal, and the new version is stamped verified with the two columns above, so a pass on the draft counts for the version made from it. An edit in between is a different hash, so nothing stale carries ([`EVENTS.md`](EVENTS.md) §K) | | created_by / updated_by | INT NULL FK→users(id) ON DELETE SET NULL | | | created_at / updated_at | DATETIME | | @@ -1723,7 +1725,7 @@ file a route sits in — that is the property the route manifest freezes. | POST | `/events` | **admin, editor.** Create a draft. The slug is derived from the title once and frozen: the public event page lives at it, so a retitle must not break a posted link. A step naming an action no module registers is refused | | PUT | `/events/:id` | **admin, editor.** Editing never touches a published version — a live run keeps the one it pinned. A step whose module has since been uninstalled is **kept and marked dormant**, not refused: the rule `engagement_rules` established for a dormant trigger, because an uninstall must not be destructive after the fact. `409` on an archived definition | | GET | `/events/:id/versions` | the version history. Nothing edits a version; the row flagged `current` is what a new run pins | -| POST | `/events/:id/publish` | **admin only** ([`EVENTS.md`](EVENTS.md) §N2) — publishing commits a definition that a schedule will later start unattended, which is deliberately not the same gate as the live run controls. Snapshots the working spec into an immutable version. **Re-validates against the registries as they stand right now**, not from the save that wrote it: `409` naming the action when a step went dormant in between, `400` when no phase has any steps | +| POST | `/events/:id/publish` | **admin only** ([`EVENTS.md`](EVENTS.md) §N2) — publishing commits a definition that a schedule will later start unattended, which is deliberately not the same gate as the live run controls. Snapshots the working spec into an immutable version. **Re-validates against the registries as they stand right now**, not from the save that wrote it: `409` naming the action when a step went dormant in between, `400` when no phase has any steps. Answers `verified: true` when a dry run that passed on this exact draft (`draft_verified_hash`) made the new version verified, so it needs no second dry run before it starts unattended | | DELETE | `/events/:id` | **admin only.** Archive — there is no hard delete at all, because a run pins a version and a run that could not be explained afterwards defeats the audit this system exists to provide. `409` while a run of it is still in flight | | POST | `/events/:id/runs` | **admin only**, on the same reasoning as publish. Creates an occurrence. `INSERT IGNORE` against `UNIQUE(definition_id, scope, scheduled_for)`, so asking twice answers `200` with `created: false` and the existing row rather than creating a second. Optional `scope`, `scheduledFor`, `rehearsal`, `params` | | GET | `/events/runs` · `/events/runs/:runId` · `/events/runs/:runId/log` | the run list, the run console (steps, their params and their idempotency keys, plus status counts) and the diagnostic log | diff --git a/website/EVENTS.md b/website/EVENTS.md index 168e9e0..8f9d30a 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -420,7 +420,7 @@ trigger in 2026-10, [§E](#starting-a-run-from-a-trigger).) (The count said "nin | Table | Holds | Why a table | | --- | --- | --- | -| `event_definitions` | id, title, slug, summary, storyline body, image, `owner_module` **nullable**, `state` `ENUM('draft','ready','archived')`, `current_version_id`, `series_id`, `series_order`, `spec` (the working copy — see below), `concurrency_key`, `grace_seconds`, timezone, `listed` (Phase 14a), created/updated by. | The thing that is listed, searched, scheduled and audited. Three states, not five: an admin publishes their own work, so there is nobody to submit it to. **`listed` is announcement, not permission** — see below. | +| `event_definitions` | id, title, slug, summary, storyline body, image, `owner_module` **nullable**, `state` `ENUM('draft','ready','archived')`, `current_version_id`, `series_id`, `series_order`, `spec` (the working copy — see below), `concurrency_key`, `grace_seconds`, timezone, `listed` (Phase 14a), `draft_verified_at`/`_by`/`_hash` (a dry run that passed on the draft and its spec's fingerprint, W5, [§K](#k--security-model)), created/updated by. | The thing that is listed, searched, scheduled and audited. Three states, not five: an admin publishes their own work, so there is nobody to submit it to. **`listed` is announcement, not permission** — see below. | | `event_series` | id, name, slug, description, ordering. Definitions optionally belong to one, at their own `series_order` within it. | **The arc.** "Royal Spy Mission → Risky Partner → Message From the Void" is continuity that exists nowhere in the tooling this replaces. One small table buys it. | | `event_versions` | `definition_id`, `version`, `spec` JSON — phases, steps, schedule, conditions, announcements — `published_at`, `published_by`, and (Phase 6) `verified_at` / `verified_by`. Immutable. | A run pins one. This is what makes a run reproducible and an audit answerable after an edit. The two `verified_*` columns are the exception that proves the immutability: they describe something that happened **to** the version afterwards — a review — rather than changing the plan reviewed, which is also why a pass recorded against one stays true for ever ([§K](#k--security-model)). | | `event_runs` | `definition_id`, `version_id`, `scope` (module-opaque), `status`, `health`, `current_phase`, `scheduled_for`, `timezone`, `started_at`, `ended_at`, `cleanup_status`, `claimed_by`, `claim_expires_at`, `started_by`, `origin` JSON (the firing that started it, when a trigger did). **`UNIQUE (definition_id, scope, scheduled_for)`** | The unique index — not the claim — is what makes "one run per occurrence per scope" true under two instances. `scope` is in the key so a worldwide event fans out to many servers without colliding with itself. | @@ -1812,7 +1812,7 @@ controller stamps it from the session. | Enablement defaulting off | Nothing that changes the world is invocable until an admin turned it on deliberately. | | The shard's own switches | `AdminWriteEnabled` is off by default in `Bridge.cfg`, lives on the shard host outside the website's reach, and `AdminAccessFloor` refuses any target at or above CoOwner. | | The ledger | Every world write is recorded with its run, step and actor, and every ledgered resource has a declared revert. Damage is attributable and, by construction, undoable. | -| Dry run before anything unattended | **Built and enforced in Phase 6, for SCHEDULED starts only** (org lead, 2026-09-03). A version records who verified it and when; the scheduler refuses to materialise an occurrence of one that never passed, and says so once per definition per tick rather than silently. A human pressing *Start now* is not gated, because that human **is** the review the gate exists to require. A failing dry run records nothing, so it cannot unlock anything. **A module may also advise** (MODULE_API 1.16.0, Rust PLAN_REDESIGNS §11.4, D391): a step that would run, answered `{ ok: true, detail: { warnings } }` under `verify: true`, shows each line as a **warning** finding (code `advice`) that never fails the dry run, because it describes the world as it stands now (a raid base's spot with a player on it, a rock beside it), which may change before the run. | +| Dry run before anything unattended | **Built and enforced in Phase 6, for SCHEDULED starts only** (org lead, 2026-09-03). A version records who verified it and when; the scheduler refuses to materialise an occurrence of one that never passed, and says so once per definition per tick rather than silently. A human pressing *Start now* is not gated, because that human **is** the review the gate exists to require. A failing dry run records nothing, so it cannot unlock anything. **A pass on a draft carries over to the version published from it** (2026-10-22, W5 in Rust [`PLAN_LAST_FIXES.md`](../modules/rust/PLAN_LAST_FIXES.md)): it is kept on the definition with the sha256 of the validated spec's canonical JSON, and publish stamps the new version verified, with that pass's reviewer and time, when the spec it freezes has the same fingerprint. An edit to the spec in between means nothing carries; a title edit is not in the spec and keeps it. Before, an author who dry-ran a draft and then published found v1 "not dry run", and its scheduled and trigger starts held. **A module may also advise** (MODULE_API 1.16.0, Rust PLAN_REDESIGNS §11.4, D391): a step that would run, answered `{ ok: true, detail: { warnings } }` under `verify: true`, shows each line as a **warning** finding (code `advice`) that never fails the dry run, because it describes the world as it stands now (a raid base's spot with a player on it, a rock beside it), which may change before the run. | > **Stated plainly.** The module boundary is **not** a security boundary — MODULE_SYSTEM §2.2 says so > explicitly, and a module runs in the same Node process with full access. None of the above defends @@ -1829,7 +1829,7 @@ controller stamps it from the session. | **A step retries at all** | The run goes `degraded` on the FIRST retry, not on the eventual failure — an event whose announcements are landing on the second attempt is having trouble now, and now is when an operator wants to know. `health` is not `status`: the run is still genuinely running (§E). | | **Core restarts mid-run** | Nothing is held in memory. The next tick finds steps in `running` with expired leases, reclaims them *without resetting `attempts`*, and continues. A step whose ack was lost is re-dispatched with the *same* idempotency key. | | **Core is down when a run should start** | Within `grace_seconds` it starts late and the log says so. Past it the run is `missed` — a terminal state a human can see. An event that begins three hours after its announcement is worse than one that visibly did not. | -| **Game server restarts mid-run** | `server.hello` arrives with a changed `bootId`, which module-uo already uses to tell a shard restart from a sidecar reconnect. The run goes `degraded`, world-write steps park, announce steps continue. On reconnect the runner asks each ledgered resource's module to **reconcile**; a resource the game no longer has becomes `orphaned`, never silently `reverted`. | +| **Game server restarts mid-run** | `server.hello` arrives with a changed `bootId`, which module-uo already uses to tell a shard restart from a sidecar reconnect. The run goes `degraded`, world-write steps park, announce steps continue. On reconnect the runner asks each ledgered resource's module to **reconcile**; a resource the game no longer has becomes `orphaned`, never silently `reverted`, unless it is past the `until` it was reported with, when it is `expired` (W1, below). | | **The sidecar is unreachable** | Every client call returns `{ ok: false }` rather than throwing — `uoLinkClient` never throws, by contract. World-write steps retry on a flat backoff; the run degrades; the public page renders last-known state. | | **The module is disabled or fails to boot** | `installed_modules` records it and the dispatch guard 503s its routes. Its actions go dormant: a step naming one fails `terminal` with the module named, and the run degrades rather than claiming success. **Never a silent skip.** | | **An action succeeds but the ack is lost** | The step retries with the same idempotency key; the plugin recognises the repeat and re-answers the original result. *This is the only mechanism that makes it safe*, and as of protocol 6 (Phase 11a) it exists: proved on the rig with three broadcasts under two keys producing **two** rows in the shard's own audit trail. It has one bound worth knowing — the plugin remembers a key for an hour, capped at 4096, and says so loudly if the cap ever evicts a live one. | @@ -1962,6 +1962,15 @@ sweep no longer scans. A run still going when its last resource expires is settl ends: finished, `pending`, and no unresolved row is `complete` — the scan that joins on an unresolved row could not see it, and the step-2 walk's run 46 sat `pending` until it could. +**A reconcile can reach `expired` too** (2026-10-22, W1 in Rust +[`PLAN_LAST_FIXES.md`](../modules/rust/PLAN_LAST_FIXES.md); no MODULE_API change). A resource whose +answer carried `until` keeps it on its row. On a reconnect the module's reconcile could run before the +game's backlog told it the zone had ended, so the run log read `resource.orphaned` five seconds before +`resource.expired`. Now a reconcile that finds a row gone **past** its `until` files it `expired`, with the +same `resource.expired` line and the same run settling as `expired()`. Gone before its `until`, or with no +`until`, is still `orphaned`. A row past its `until` that the game still holds stays in force, because the +game is the one that ends it. Module-Rust's zones send `until` (Module-Rust#56). + --- ## Versioning, and editing a live event @@ -1996,7 +2005,7 @@ no URL moved. | `POST /admin/events/:id/publish` | admin | snapshot a version and go `ready` | | `DELETE /admin/events/:id` | admin | archive — never a hard delete while runs reference it | | `GET /admin/events/:id/versions` | staff | version history | -| `POST /admin/events/:id/verify` | admin, editor | **dry run** (Phase 6) — dispatch every step with `verify: true`, report the findings and the whole-plan cost against the caps. Verifies the PUBLISHED version of a `ready` definition and the working spec of a draft; a pass against a version is recorded and is what unlocks scheduled starts (§K) | +| `POST /admin/events/:id/verify` | admin, editor | **dry run** (Phase 6) — dispatch every step with `verify: true`, report the findings and the whole-plan cost against the caps. Verifies the PUBLISHED version of a `ready` definition and the working spec of a draft; a pass against a version is recorded and is what unlocks scheduled starts (§K). A pass on a draft is kept by fingerprint (`draftRecorded`), so publishing that spec unchanged makes a verified version (W5) | | `POST /admin/events/price` | admin, editor | **the live cap meter** (Phase 13) — price the spec IN THE BODY against the per-run caps. Dispatches nothing and records nothing, which is what separates it from the dry run and what makes it safe to call on a debounce while an author types; the spec is in the body because the plan being priced is unsaved between keystrokes. It therefore knows nothing a module knows — whether the landmark exists, whether the shard is up — and the screen says so under the numbers. A step it cannot price is reported in `unpriced` rather than counted as free; an **undeclared dimension is still counted**, because the action really will try to spend it and it is the enforcement rather than the amount that is missing. Over the caps is a `200`, for `verify`'s reason | | `POST /admin/events/:id/runs` | admin | start now; optional `rehearsal: true`, optional `scope` — and optional `params`, which **Phase 13 was the first thing to send**: the concurrency key is a `{placeholder}` template rendered from the run's own params, so a UI posting an empty body rendered the same key for every manual run and had the second refused as an overlap with the first | | `GET /admin/events/runs` | staff | run history across definitions | @@ -2084,7 +2093,10 @@ definition's state**, and the answer says which: a `ready` definition is checked PUBLISHED version, because that is the only thing that ever actually runs and it is that pass §K's gate is about; a draft is checked against the working spec the author is still holding. Only a pass against a version is recorded — a version is immutable, so a pass on one stays true, while a pass on -a draft would be a claim about a spec that changes under the author's hands. **Findings come back +a draft would be a claim about a spec that changes under the author's hands. **A pass on a draft is +kept with the fingerprint of the spec it passed on** (W5, §K), and the answer says so with +`draftRecorded`; `publish` answers `verified` when that fingerprint matches the spec it freezes, and +writes `version.verified` on the occurrences it re-pins. **Findings come back with a 200**: the request succeeded, the plan has problems, and answering 4xx would make "this event asks for 45 and you allow 30" indistinguishable from "you sent a bad event id". @@ -2147,8 +2159,13 @@ and there is a piece of reuse here better than it first looks. The condition gra `present` is *"is present"*. The same labels render the diagnosis. **Built in Phase 5**, on the server, from those labels — the caps line arrives with the budgets in Phase 6. +*(The console's words since 2026-10-22, W2 in Rust +[`PLAN_LAST_FIXES.md`](../modules/rust/PLAN_LAST_FIXES.md): the box is headed **"Why this phase has not +advanced"**, and the waiting phase reads **"Phase p3: steps done, waiting to advance"**. It said "has not +started", but the phase is running and its steps are done; it is the advance that waits.)* + ``` -Phase 3 — "The Boss" has not started. +Phase p3: steps done, waiting to advance. waiting on: uo.champ.boss_up where region is "Yew" seen so far: 0 of 1 since: 20:31:04 (28 min) diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 24b111c..76831b0 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -383,6 +383,16 @@ field: made a promise core has no way to keep. Anything that is not an explicit `{ ok: true, inForce: [...] }` leaves the ledger alone: **"I do not know" is never read as "it is gone"**, and a resource a module reports missing becomes `orphaned` rather than `reverted`, because nobody asked for it to go. +- **A resource may carry `until`**, the instant the game will end it by itself (an ISO string or + epoch milliseconds; a value that does not parse drops the resource with a logged reason, like any badly shaped + one). Core keeps it on the ledger row. **A reconcile that finds a resource gone past its `until` + files it `expired`, not `orphaned`** (W1 in Rust `PLAN_LAST_FIXES.md`, 2026-10-22): the deadline + worked, nobody lost it. It writes the same `resource.expired` line as `ctx.events.expired()`, and a + finished run whose last unresolved row it was is marked cleaned up. Gone before its `until`, or with + none, is still `orphaned`, and a row past its `until` that the game still has stays in force. This + is why a module should report `until` for anything with its own timer: after an outage, the + reconcile can run before the game's own "it ended" reaches core. No version change: `until` was + already accepted, and only core's reading of it at reconcile is new. **A third joined in Phase 15, also on an envelope:** @@ -1063,7 +1073,8 @@ one authoritative source by construction, and two modules answering "what Teams two disjoint sets under one table with no rule for merging them. A second registration is a collision, reported against the module that holds it — and it fails that module's whole load, which is why core's installer admits **one module per site** (`MODULE_SYSTEM.md` §2.5, decision 35) rather -than leaving the collision to be discovered on the next restart. Three methods are required — a provider +than leaving the collision to be discovered on the next restart, and why, since 2026-10-22, the loader +refuses a second module directory at boot before it is required (§4.2). Three methods are required — a provider that could list Teams but not their members would leave core holding Teams it can never populate, which is not the same as a call that fails. The fourth is optional; see below. @@ -2305,6 +2316,16 @@ Alphabetical by `id`, deterministically. There is no dependency resolution betwe the design of record puts it out of scope) and alphabetical order is the honest way to say so — any other order would imply a precedence that is not being computed. +**The first module in that order is the only one that starts** (`MODULE_SYSTEM.md` §2.5, decision 35; +W19, 2026-10-22). It holds the site whether or not it then loads. Every later directory with a +`module.json` is recorded `startup_failed` at the `manifest` stage before step 1 below, and before any +of its code is required, with a reason that names both modules and says the one that sorts first by +name is the one that starts. The installer already refused a second module; this closes the +hand-copied directory. No `MODULE_API_VERSION` change: nothing a module calls or declares is different, +and a site with one module loads exactly as before. The collision checks below still reject a module +that collides with **core**. Against another module they can no longer be reached, and they stay as a +second line behind the rule. + ### 4.3 Validation, in this order 1. `module.json` parses; no unknown keys; `id` matches the directory. diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index 7016e65..9b57f44 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -471,8 +471,18 @@ with `409` and before a byte is downloaded, any install whose id differs from a volume. **An upgrade of the installed module is the only install a populated site accepts**; to change game, remove the module first. The rule is enforced on the install path both surfaces share, so a `MODULES` declaration naming two modules installs the first and reports the second as refused, without -failing the boot. It does not police a directory an operator places by hand, which remains the -developer's escape hatch. +failing the boot. + +**The loader enforces it too** *(org lead, 2026-10-22; W19 in Rust +[`PLAN_LAST_FIXES.md`](../modules/rust/PLAN_LAST_FIXES.md))*. A directory copied onto the volume by hand +used to get past the installer's rule, and Module-uo beside Module-Rust then failed the second at register +with *"extension slot … is already filled"*, which named neither the rule nor the fix. Now the first +module directory in the loader's alphabetical scan holds the site, **whether or not it then loads**, and +every other is recorded `startup_failed` at the `manifest` stage **before any of its code is required**: +no routes, nav, tables or registrations. Its reason, in the log and on Admin → Modules, reads *"this site +already runs the module "rust", and a site runs one module. Remove "uo" or "rust" from the modules +directory; the one that sorts first by name is the one that starts."* The site comes up with the first. +The module-against-module collision checks stay in the code, as a second line behind this rule. **Uninstall** (default, non-destructive): row set to `disabled`, directory removed, restart. The module's tables and data are **retained**. **Purge** is a separate, explicit, destructive action that @@ -2286,4 +2296,4 @@ check in this repo can see. | 32 | The kit is a **draft on its own front page** until a person outside the org passes its acceptance; a cold-agent run is the interim proxy | §2.11.1 d5 | | 33 | `rust-dryrun.md` is **corrected with a dated note**, not silently rewritten — a dry run's value is the record of what it found | §2.11.1 d6 | | 34 | Core owns the page **body** as well as the chrome: `PublicLayout` takes a `shell` width, and its CSS class names stay core's private business | API §3.4, §2.11.1 slice 3 | -| 35 | **A site runs one module.** The installer refuses a module whose id differs from the one installed (`409`); only upgrades are accepted. Forced by the one-per-deployment Team provider, which a second module would collide with and fail to load | §2.5 | +| 35 | **A site runs one module.** The installer refuses a module whose id differs from the one installed (`409`); only upgrades are accepted. Forced by the one-per-deployment Team provider, which a second module would collide with and fail to load. **Since 2026-10-22 the loader refuses a second module directory at boot too**, before requiring it (W19) | §2.5 |