From 9eb6a2edc474150f190135c686c13bf11bc85b45 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 16 Oct 2026 19:29:02 -0500 Subject: [PATCH] =?UTF-8?q?docs(rust):=20RaidableBases=20built=20(PROTOCOL?= =?UTF-8?q?=20=C2=A720.12,=20PLAN=5FREDESIGNS=20=C2=A711.4.6,=20D391,=20MO?= =?UTF-8?q?DULE=5FAPI=201.16.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust-link/PROTOCOL.md §20.12: RaidableBases — hello's raidableBases, the five raid.* events, the tally's raidsCompleted, map.live's raids layer (and its §17.3 row), raid.spawn and raid.check with their refusals, the raids helper, the profiles in the configuration editor, the sidecar's two forwards and the website's step, feed, map layer and Warlord. - modules/rust/PLAN_REDESIGNS.md: D391 (the editor's spot check is advice through core), §11.4.6 (what was built, the rig tests on both rigs, the Oxide rig's RaidableBases fault that a restart cleared, twelve points settled in code, the end walk's rows), and the order of work. - website/MODULE_API.md, EVENTS.md: 1.16.0, a dry run's detail.warnings. - website/STEAM_SIGNIN.md: its contract moves to 1.17.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 109 ++++++++++++++++- rust-link/PROTOCOL.md | 206 +++++++++++++++++++++++++++++++++ website/EVENTS.md | 2 +- website/MODULE_API.md | 28 ++++- website/STEAM_SIGNIN.md | 2 +- 5 files changed, 340 insertions(+), 7 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index fc0e32c..06b08e2 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -11,7 +11,7 @@ chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned [`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D314), and the rest of this plan waits for it.** RunicNPC v1.0.0 shipped on 2026-10-09, and the plan resumed with §2 the same day (D331–D334); §2 was built on 2026-10-12, and §11's Economics spike the same day raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). The Backpacks spike followed and raised D337–D339 (§11.3.1), and Backpacks was built the same day (D340, D341, §11.3.4). The org lead then added permission rewards (§12, -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). **Every item left is built, tested and PR'd first; every walk — +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. **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).** This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this @@ -925,7 +925,7 @@ pulls) is research, not a walk, so it still comes first: 7. **The first-class optional plugins** (§11): Economics, then Backpacks, then RaidableBases, each starting with its source pull and rig spike. Economics and Backpacks were built on 2026-10-12. **RaidableBases waited for the current paid copy (D347)** and is built for both editions (D348). **The copy arrived on 2026-10-16 - and was spiked on both rigs the same day (§11.4.2)**, with D386. Q33–Q36 were answered the same day (D387–D390). + and was spiked on both rigs the same day (§11.4.2)**, with D386. Q33–Q36 were answered the same day (D387–D390). **Built the same day (§11.4.6)**, with D391. 8. **Permissions as event rewards** (§12), built on 2026-10-12. 9. **Rust's own world events and six more optional plugins** (§13, §14). First comes one test phase covering all of them on both rigs (§14.2, D352); **it ran on 2026-10-14 (§14.2.1)**. Then one integration plan, @@ -1109,6 +1109,7 @@ The org lead answered §14.3's Q26–Q32 on 2026-10-14: | **D388** | **The area check refuses for buildings, deployables, raid bases, players and water; rocks and cliffs are a warning (Q34).** The plugin's own reason is read from its message queue and shown in the editor and the run log, and a spot with only rocks or cliffs near it still spawns. This amends D208. | Refusing on every reason the plugin gives (D208 as written); advice only. | 11.4.2, 11.4.5 | | **D389** | **"In water" is refused unless the chosen profile is built for it (Q35):** its seabed chance is above 0, or it allows bases to float above water. Both are read from the profile the step picked. This amends D208. | Always refusing water. | 11.4.2, 11.4.5 | | **D390** | **Lockouts are dropped from the feed (Q36).** Neither edition raises a hook for them. On the paid edition the feed gains a line when a player buys a base (`OnRaidableBasePurchased`) and when an admin locks one to a player (`OnRaidableBaseLocked`), naming the player only for the presence audience (D376). This amends D202. | Dropping them with no new lines; reading lockouts out of the plugin's data by reflection. | 11.4.2 | +| **D391** | **The editor's spot check is advice through core (2026-10-16, asked during the build).** Core's dry run gains a warning channel, MODULE_API 1.16.0: a module answering `verify: true` with `{ ok: true, detail: { warnings } }` has each line shown as a warning finding that never fails the dry run. A raid base's spot refused now, and the rocks and cliffs beside it (D388), show there as advice; the run itself checks again. Steam sign-in moves to 1.17.0. | A blocked spot failing the dry run, with rocks and cliffs only in the run log; no editor check. | 11.4.6 | Console commands as event steps and rewards, added on 2026-10-16, were decided as D382–D384, and D385 during the build; they are tabled with their section, in §16.7. @@ -1969,6 +1970,110 @@ The org lead answered §11.4.4's three questions the same day: runs the same check through the bridge when the step is saved. That is advice only, since the world can change before the event runs. +#### 11.4.6 Built (2026-10-16) + +Built the day the paid copy was spiked, on the org lead's go, in one pass across the repos. The branches are +`feat/raidable-bases` in Rust-Plugins, Rust-Link and Module-Rust (into `edge`) and in the installer (into +`edge`), `feat/dry-run-warnings` in the website (into `main`), with this record on `docs/raidablebases-build`. +The specification is [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.12. The walk rows join the end walk (D331, +D359), below. + +**D391 was asked during the build.** D208 makes the editor's spot check advice, and D388 shows rocks and cliffs +as a warning in the editor; core's dry run took only errors from a module. So core gained a warning channel: +MODULE_API 1.16.0 (`detail.warnings` under `verify: true`), and Steam sign-in moves to 1.17.0. §10 has the +decision. + +**What was built:** + +- **The raids helper, `RunicGatewayRaids.cs`**, beside the bridge and installed by default. Three Harmony + patches (the spawn queue's `Add`, `OpenEvent`, each base's `BypassUseOwners()`), a spot route that checks + and pastes, one base's despawn, and the profiles' watched reload. Both editions: the paid spawn call where it + is, the free edition's private typed one otherwise, every member looked for by name and the state reported. +- **The bridge:** hello's `raidableBases`; five `raid.*` events from the lifecycle hooks; the tally's + `raidsCompleted`; the map's `raids` layer; `raid.spawn` and `raid.check`; a run's base in the world registry + so `world.revert`, `world.owned` and `world.expired` serve it; and the profile folder in the editor. +- **The sidecar:** two thin forwards. +- **Module-Rust:** the step `rust.raidbase.spawn`, its option sources and budget, the feed's lines, the map + layer with its own audience, Warlord (title category, leaderboard column), the servers-page line, and the + profile files in Mod configuration. +- **Core:** the dry run's advice (D391). **The installer:** the helper's line in `doctor`. + +**Tested on the rigs** (Rust 25902425, Oxide 2.0.7830, Carbon 2.0.262, paid RaidableBases 3.2.2, CopyPaste 4.3.0, the +tier-3 pack), through a throwaway plugin calling the bridge's handlers (no sidecar): + +| Row | Result | +| --- | --- | +| Hello: edition, the spawn call, difficulties and profiles as the pack names them, the helper patched, the grid | ✓ | +| A spawn refused while the grid builds (`rb-not-ready`), and an unknown difficulty (`bad-difficulty`) | ✓ | +| The plugin's pick, Hard, PvP off, 2 NPCs, despawn 30 min, locked: tagged, on the registry and the map with its run; PvP off despite the pack's *Convert PVE To PVP*; `BypassUseOwners()` false; despawn 30 min | ✓ | +| The shared profile untouched after it (PvP still on) | ✓ | +| A spot, `raidmed3`, +5 m, NPCs off, lock off: landed 1 m from the spot, blocks from 5.9 m on ground −0.3, no NPCs, the config's lock | ✓ | +| The spot check: a deployable refused with the plugin's reason; a rock a warning; 0.9 m of water refused for a profile not built for it | ✓ | +| A repeat key: nothing spawned | ✓ | +| `world.revert` of the run: its two bases despawned, an untagged base left standing | ✓ | +| A run's base the plugin despawned (`rbevent despawnall`): off the registry by the expiry path | ✓ | +| Profiles in the editor: listed under the virtual path; a good edit reloaded and settled; a bad one (`"Difficulty Level": "not a number"`) failed to load, rolled back and reloaded the old file | ✓ | +| A RaidableBases unload with a run's base out: the base expired with `raidablebases-unloaded` | ✓ | + +**A finding, and a change made on it.** The first build copied a spawn's profile through a JSON round trip, as +the spike had, into a fresh `BaseProfile`. That loses the profile's loot list, its spawn table and every value +the plugin computes after reading a profile, so the copy is now member by member, with the three objects a +spawn's settings write into copied again (`CopyProfile`). It was found while chasing the Oxide rig's fault +below, which turned out not to be ours. + +**The Oxide rig.** After several hours of uptime and the spike's own reloads, **paid RaidableBases broke on every spawn on the +Oxide rig, with none of our plugins loaded**: `rbevent Easy` and `rbevent Hard` each threw +`ArrayTypeMismatchException` from a coroutine and left a half-made base that made its damage, SAM site and +trigger hooks throw on every tick. The Carbon rig, the same plugin and pack, was clean. **A server restart +cleared it.** On the restarted rig the pick with every setting, a spot at an exact height of 30 m (blocks from +30.9), the spot check, revert and the profile editor (good and broken) all passed as on Carbon; the unload row +had passed on Oxide before the fault. The repeat key and the despawn-all expiry were run on Carbon only. The cause is RaidableBases', in that process; the end walk starts both rigs fresh. **`rb-busy` +was met for real**: a spot asked for while the plugin's pick was still pasting. + +**Not tested on the rigs, and owed by the end walk:** a raid completed by a player (the tally, *Warlord*, the +feed's names), a purchase and an admin lock (`raid.purchased`, `raid.locked`), the never-started expiry, the +free edition (D348: the end walk covers a rig with each edition), and the whole path through the sidecar and +the site. + +**Settled in code** (each can be overruled): + +1. **"Queued" is the step's success.** The plugin pastes in its own time, so the step answers when the base is + on its way; where it went reaches the map, the feed and the run's resource when it opens. A run's base not + open fifteen minutes after it was asked for never will be: its request is dropped and the resource expires, + `never-started`. +2. **A base not made by the helper is not the run's.** Without the helper the step records no resource and + the run log says the base despawns on its own timer; with no difficulty named, the paid call picks one of + the plugin's own at random. +3. **A spot is a world call's location** (a monument with offsets, or x and z), so the step's location fields + are the other world steps'; `y` is ignored, and the height is `heightAdjust` or `height`. +4. **The spot check looks first, then asks the plugin.** The plugin's check stops at the first thing it finds, + so a rock found first would hide a player's hut. The helper's own look (players, a player's building or + deployable, a raid base within the profile's protection radius) runs before it. **Every reason the plugin + gives other than rocks and cliffs refuses**, roads, rivers and monuments among them, beyond D388's list. + A refused spot is permanent: the run pauses on its own `on_failure` rather than placing the base elsewhere. +5. **NPC count** is 0 to 20 (the bridge's world bound), in the profile's own mix of scientists and murderers, + and turns the profile's random amounts off. **Despawn minutes** sets both the total and the inactive timer. +6. **The base's file name is public** on the feed and the map, as its difficulty is: it is the operator's name + for a building, and the map's projection cannot tell staff apart. The run's key never leaves for any viewer. +7. **Warlord's credit** is the plugin's own participants (`GetRaiders()`), each once; the lock holder alone + when none is listed. The unload trap's completions (no owner, no raiders) are neither counted nor told. +8. **No engagement triggers** for raid bases: §11.4 asked for none. They can follow §14.3.9's pattern if wanted. +9. **`OnRaidableBaseDespawn` and `PrivilegeDestroyed` are not used**: `Ended` says a base is gone, and the + feed's completion carries the raid. +10. **The profile editor needs the patched helper and a built grid**, refuses a write that mixes profiles with + other files, and reloads with no player: the plugin then skips its base loot tables and its log line, + which a profile edit does not touch. +11. **`rb-busy`** (it pastes one base at a time) retries on core's ordinary delay; **`rb-not-ready`** in ten + minutes. +12. **A run's base id is the bridge's own**, `raid--`, so a zone's `rg-` id is never mistaken for one. + +**The end walk's rows for §11.4**, added to §9's list, on both rigs and both editions: spawn by difficulty, +profile and base, where the plugin picks and at a spot with each height mode; each setting and the lock felt +in game; the dry run's advice for a blocked spot and a rocky one; teardown takes the run's base and no other; a +base despawned or raided first is marked `expired`; a raid completed by a player (feed, *Warlord*, the column); +a purchase and an admin lock on the paid edition; the map layer and its audience switch; a profile edited from +the site, good and broken; and the free edition without the helper (`rbevent`, which needs `console.plugins`). + ### 11.5 Protocol These are new verbs, events and hello fields. **They join protocol 14 (D332)**, which §2 opens: protocol 13 diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index cbd4a1d..7e4863c 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -1810,6 +1810,7 @@ drawing a second time. A new map key makes the file stale, and it is ignored. | `players` | `steamId`, `name`, `x`, `z`, `sleeping`, `online`. Connected players, then offline sleepers | | `bases` | `kind` (`tc`, `vending`), `x`, `z`. **Positions only**: no owner, no authorised list, no shop name | | `pvp` | Protocol 14 (§20.7): Dynamic PVP's own zones, `zoneId`, `event`, `x`, `z` (where the zone is now), `radius`, `shape` (`sphere`, `cube`). Empty without Dynamic PVP | +| `raids` | Protocol 14 (§20.12): RaidableBases' live bases, `x`, `z`, `level`, `difficulty`, `pvp`, `base`, `despawnAt`, and a run's `runId` and `key`. Empty without RaidableBases | It also carries `mapKey` and `tookMs`, the plugin's own time for the ask. `playersTruncated` and `basesTruncated` are set when a bound cut a layer short. @@ -3500,3 +3501,208 @@ is not "it worked". Nothing that follows reaches the feed, the map, a title or a placeholder itself. Priced commands × people, the 100-recipient bound for a mode that is not a count. - `unknown-command`, `malformed`, `interrupted`, `failed` and `events-disabled` are permanent. The error says how many commands ran before it. A game that is down retries on core's ordinary delay. + +### 20.12 RaidableBases — `raid.*` events, `raid.spawn`, `raid.check`, the raids layer, profiles in the editor (PLAN_REDESIGNS §11.4, D202–D208, D386–D391) + +nivex's **RaidableBases** (3.2.2, free on uMod or paid on Codefling), optional like Economics (D199) and not in +`overlay.toml`, with its dependency **CopyPaste** (4.3.0), which pastes every base. A base is a stocked, +guarded CopyPaste building that players raid; it despawns on its profile's own timer. The bridge reports its +bases, spawns one for a run's step, and opens RaidableBases' profiles to the site's configuration editor. The +design is [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §11.4, and what the build found is §11.4.6 +there. + +**Names are the plugin's (D386).** Every difficulty, profile and base name below is read from the installed +copy and carried as its own string, with the plugin's level int beside a difficulty. The paid edition's +difficulties are the operator's data (each profile's *Difficulty* and *Difficulty Level*); the free edition +has one, *Normal* at 512. Nothing on the wire, in the bridge or on the site lists them. + +**The raids helper, `RunicGatewayRaids.cs`** (D168, D204–D207, D387), ships beside the bridge and is installed +by default. It reaches RaidableBases' internals by reflection and three Harmony patches: a prefix on its +spawn queue's `Add` tags the request the helper's own call made; a postfix on `OpenEvent` sees the base that +request became; a prefix on each base's `BypassUseOwners()` answers the run's own lock. Without it the bridge +still reports bases, but a step's base is not the run's (below). A RaidableBases reload drops the patches with +the old assembly, and the helper patches again. `rgr.status` prints its state. + +**Hello and status.** `integrations` gains `raidableBases`: + +```json +"raidableBases": { "loaded": true, "version": "3.2.2", "premium": true, "spawnCall": true, + "copyPaste": { "loaded": true, "version": "4.3.0" }, + "difficulties": [{ "name": "Easy", "level": 0 }, { "name": "Hard", "level": 2 }], + "helper": { "state": "patched", "version": "0.1.0" }, + "gridReady": true, + "profiles": [{ "name": "Hard Bases", "difficulty": "Hard", "level": 2, "enabled": true, + "pvp": true, "bases": ["raidhard1", "raidhard2"], "seabed": false, "float": false }] } +``` + +- `premium` is the plugin's own `IsPremium()`. **`spawnCall`** is whether the paid edition's private + `SpawnRandomBase(string, Vector3, int, int, bool)` is there, looked for by name (D348): "paid" is never + taken to mean a call exists. +- `difficulties` comes from its public `GetAllDifficulties()`, each name once with its level, by level. +- `helper.state` is `missing`, `patched`, `unsupported` (with `reason`) or `no-raidablebases`. `helper` is sent + even while RaidableBases is absent. +- **`gridReady` and `profiles` only with a patched helper**, which is what can read them. `bases` are a + profile's own name and its additional bases, **only those with a CopyPaste file**. `seabed` is a seabed + chance above 0 and `float` the profile's *Allow Bases To Float Above Water* (D389). +- Hello is sent again when RaidableBases, the helper or CopyPaste loads or unloads. `rg.world` prints + `raidableBases=`, `raidsHelper=`, `raidGrid=`, `raids=` (live bases the bridge knows) and `raidBases=` (runs' + bases on record). + +**Five events**, from RaidableBases' own lifecycle hooks, which both editions raise alike (the 17-argument +`Started`, `Completed` and `Ended`; the paid edition's `Purchased` and `Locked`): + +```json +{"kind":"raid.started","type":"event","x":650.0,"z":2037.0,"level":2,"difficulty":"Hard","pvp":true,"base":"raidhard2", + "radius":50.0,"despawnAt":1792180000000,"id":"raid-1792178807764-4","runId":"12","key":"12:4"} +{"kind":"raid.completed","type":"event","x":650.0,"z":2037.0,"level":2,"difficulty":"Hard","pvp":true,"base":"raidhard2", + "raiders":[{"steamId":"76561198000000001","name":"Alice"}],"raiderCount":1,"ownerId":"76561198000000001","ownerName":"Alice"} +{"kind":"raid.ended","type":"event","x":650.0,"z":2037.0,"level":2,"difficulty":"Hard","pvp":true,"base":"raidhard2"} +{"kind":"raid.purchased","type":"event","steamId":"76561198000000001","name":"Alice","x":650.0,"z":2037.0,"level":2,"difficulty":"Hard","pvp":false,"base":"raidhard2"} +{"kind":"raid.locked","type":"event","steamId":"76561198000000001","name":"Alice","x":650.0,"z":2037.0,"level":2,"difficulty":"Hard","pvp":false,"base":"raidhard2"} +``` + +- **`id`, `runId` and `key` are set when the base is a run's** (the helper's tag, joined by the base's + position), on every kind. +- **`raid.completed`'s `raiders` are the plugin's own participants** (its `GetRaiders()`), at most 32 named, + all counted in `raiderCount`; `ownerId` and `ownerName` are the player the base was locked to, when it was. +- **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.** +- `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. + +**The tally** gains **`raidsCompleted`**: a completed base counts once for each of its raiders, or for its +owner alone when none is listed (*Warlord*, D202). + +**`map.live`** (§17.3) gains **`raids`**, a layer of its own: every live base, `x`, `z`, `level`, +`difficulty`, `pvp`, `base`, `despawnAt`, and `runId` and `key` for a run's. The bridge keeps the set by the +hooks, and finds it again from the plugin's public `GetAllEvents()` at its own load and RaidableBases' (the +tuple's position, mode, level, PvP, base name and despawn time; a shape it cannot read leaves the set to the +hooks). It is cleared when RaidableBases unloads. A run's base is not also in `events`. + +**`raid.spawn` — one base for a run (D203–D207, D387).** + +```json +{"cmd":"raid.spawn","reqId":"r-70","key":"12:4","runId":"12","difficulty":"Hard","profile":"Hard Bases","base":"raidhard2", + "x":650.0,"z":2037.0,"heightAdjust":5,"pvp":false,"npcs":true,"npcCount":6,"despawnMinutes":90,"lock":true} +``` + +- **Every field but `key` and `runId` is optional.** `difficulty`, `profile` and `base` are the plugin's + names; a base names its profile, a profile picks one of its bases at random, a difficulty alone lets the + plugin pick among its own. **A spot** is a world call's location (§20.5: `monument` with `offsetX`/`offsetZ`, + or `x` and `z`; `y` is ignored), with `heightAdjust` (metres added to the profile's paste height) **or** + `height` (the profile's forced height). The settings are `pvp`, `npcs` (on or off), `npcCount` (0 to 20, in + the profile's own mix of scientists and murderers, never a random amount), `despawnMinutes` (1 to 1440, + inactive and total) and `lock` (to the first attacker). +- **With a patched helper the base is the run's.** It is a world registry entry under the bridge's own id + (`raid--`, never a zone's `rg-`), so **`world.revert` despawns that base and no other** through its + own `Despawn()` (one still on its way is taken off the plugin's queue, or despawned the moment it opens); + **`world.owned`** asks the helper whether it is open or on its way; and when RaidableBases ends it first (its + own timer, raided, or its unload), **`world.expired`** says so, `what: "raidbase"`. A base not open fifteen + minutes after it was asked for never will be (its paste failed, or it found no room): its request is + dropped and it expires with `reason: "never-started"`. A base does not survive a restart, so one from an + earlier boot is gone. +- **Settings go on a copy of the base's profile**, member by member, with its `Setup`, `NPC` and + `DespawnOptions` copied again, so the profile every other base shares is never touched (§11.4.4). **PvP is + also set on the base after `OpenEvent`**, because the plugin's config switches are applied inside it. +- **At a spot** the helper checks, before anything is pasted (D208, D388, D389): its own look for a player, + a player's building or deployable, and a raid base within the profile's protection radius; then + RaidableBases' own area check, whose reason it reads from the plugin's message queue (**a rock or a cliff + is a warning, and the base spawns**); then the ocean level against the ground (**in water is refused unless + the profile has a seabed chance or allows floating**). A spot is pasted at once, as the plugin's own + `rbe` pastes. +- **Without the helper** a spot, a profile, a base or settings are refused (`helper-needed`). The base can + still be asked for, but it is **not the run's**: no registry entry, and it despawns on its own timer. The + paid edition's own `SpawnRandomBase` at the difficulty's level (a random one when none is named), or else + the free edition's console command `rbevent`, which needs hello's `console.plugins` and answers nothing. +- **A key already answered is answered again with its first spawn, `repeat: true`, and spawns nothing.** Keys + are kept seven days in `data/RunicGateway/raid-starts.json`. + +The answer: + +```json +{"kind":"raid.queued","type":"reply","reqId":"r-70","key":"12:4","route":"spot","repeat":false,"despawns":"teardown", + "id":"raid-1792178807764-4","difficulty":"Hard","level":2,"profile":"Hard Bases","base":"raidhard2","x":650.0,"z":2037.0, + "warnings":["Rock is too large: 640.10 3.20 2041.80"]} +``` + +`route` is `pick` or `spot` (the helper's), `plugin` (the paid call) or `console`. `despawns` is `teardown` +for the run's base and `own-timer` otherwise. **"Queued" is not "open"**: the plugin pastes in its own time, +and `raid.started` says where and when. Refusals are `{"kind":"raid.error","reason":…,"message":…,"warnings":[…]}`: + +| `reason` | When | +|---|---| +| `malformed` | No key or run, a key over 128 characters, a height adjustment and an exact height together, or a location that is both or neither (§20.5) | +| `rb-missing` | RaidableBases is not loaded | +| `copypaste-missing` | CopyPaste is not loaded | +| `helper-needed` | A spot, a profile, a base or a setting without `RunicGatewayRaids.cs` | +| `helper-unsupported` | The helper is loaded but could not patch this RaidableBases | +| `rb-not-ready` | Its spawn grid is still building (up to ten minutes after it loads, §11.4.2): it refuses every spawn | +| `rb-busy` | It is pasting another base; it pastes one at a time | +| `bad-difficulty`, `bad-profile`, `bad-base` | A name it does not have, or names that do not fit together | +| `spot-blocked` | A player, a player's building or deployable, a raid base, or the plugin's own refusal other than rocks and cliffs | +| `in-water` | The spot is under the ocean and the profile is not built for water | +| `no-monument`, `out-of-range`, `off-map` | The location, as §20.5 | +| `console-unavailable` | No helper, no paid call, and hello's `console.plugins` is false (D363, Rust-Plugins#34) | +| `refused` | RaidableBases did not queue a base, in its own words | + +`events-disabled` and the world's `not-ready` come first, as `world.error`, as for every world verb (§15). + +**`raid.check` — a spot's checks, spawning nothing (D208, D391).** `{ "cmd":"raid.check", "x", "z" }` (or a +monument), with the `difficulty`, `profile` or `base` whose radius and water settings apply. The answer is +`{"kind":"raid.checked","ok":…,"reason":…,"message":…,"warnings":[…],"base":…,"x":…,"z":…}`, or `raid.error` +`helper-needed` without the helper. The site's dry run asks it and shows the answer as advice. + +**RaidableBases' profiles in the configuration editor (D205).** The one named exception to the editor never +walking a data directory: `config.list` adds each `data/RaidableBases/Profiles/*.json` (not the plugin's +`_emptyfile`) under the **virtual path** `@data/RaidableBases/Profiles/.json`, `plugin: "RaidableBases"`, +`profile: true`. No path from the config tree can name one, and `config.read` and `config.write` resolve that +prefix to the one folder only, one file deep, never through a link. A write is **all profiles or none** +(`mixed` otherwise) and is refused before anything lands (`profiles-unavailable`) without RaidableBases, a +patched helper, or a built grid. It is version-checked and backed up like any config write, then **the helper +reloads the profiles as `rb.reloadprofiles` does**, and the reply is `pending` as a protocol 13 save's is +(§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. + +**Hooks.** `OnRaidableBaseStarted`, `OnRaidableBaseEnded`, `OnRaidableBaseCompleted`, `OnRaidableBasePurchased` +and `OnRaidableBaseLocked` join `rg.hooks`' list. + +**The sidecar** adds `POST /raid/spawn` and `POST /raid/check`, thin forwards that keep nothing, and stores and +forwards the five events like any other. + +**The website (Module-Rust).** + +- **`raid.started`, `raid.completed`, `raid.purchased` and `raid.locked` are public kinds; `raid.ended` is + staff's.** Each row is read per viewer by `raidableBases.shapeFor`: everyone gets `grid`, `level`, + `difficulty`, `base`, `pvp`, `despawnAt` and a completion's `raiderCount`; **the raiders' `names`, the lock + holder's `ownerName` and a buyer's `name` only for the presence audience** (D376, D390); **`x`, `z`, the + run's `id`, `runId` and `key`, and every Steam id only for staff**. The lines read *"A Hard raid base + appeared · G7 · PvP"*, *"Alice and Bob raided a Hard raid base"* (*"A Hard raid base was raided · G7 · by 2 + players"* below the presence audience), *"Alice bought a Hard raid base"*, *"A Hard raid base was locked to + Alice"*, and for staff *"A Hard raid base despawned"*; all but the last are in the feed's *World events* + filter. +- **The step `rust.raidbase.spawn`**, *Spawn a raid base* (`risk: 'change'`, `reversible: 'ledger'`, budget + `rust.raids`, one per step): the server; a difficulty, profile and base from option sources that list each + server's own names (`rust.options.raid_difficulties`, `raid_profiles`, `raid_bases`, the last searchable); + a spot by monument or x and z, with a height adjustment or an exact height; and the settings, PvP, NPCs and + the lock as on/off/blank (`rust.options.raid_switch`, blank keeps the profile's or the config's). The run's + base is a `world` resource (`:raid-…`), so `eventWorld`'s revert and reconcile serve it and + `world.expired` marks it `expired`. A base that is not the run's records no resource, and the run log says it + despawns on its own timer. It is refused at verify where the server said RaidableBases or CopyPaste is + missing, that a spot, profile, base or setting has no patched helper, or that it has no such name; a server + that has not said is left to the plugin. **A dry run with a spot asks `raid.check` and shows its answer as + advice** (MODULE_API 1.16.0, D391): *"the spot is refused now … It is checked again when the event runs"*, + *"at the spot now: Rock is too large"*. `rb-not-ready` retries in ten minutes (`retryAfterMs`, D365's + pattern), `rb-busy` and a game that is down on core's ordinary delay; every other refusal is permanent. +- **Warlord (D202):** `raidsCompleted` → `rust_player_wipe_stats.raids_completed`, the title category + `raidscompleted` (default *Warlord*), and the leaderboard's **Raid bases** column (`sort=raidsCompleted`), + shown where the server reports RaidableBases or a row counts one (`raidableBases: true` on the answer). +- **The map's `raids` layer** (D114), public by default with its own audience in Admin → Rust visibility: an + orange dot per base, yellow for one this site's event spawned, with its difficulty, base, PvP and despawn + time. A run's `key` is dropped before any viewer is sent the layer. +- Server settings' plugin line names the edition, the difficulties, CopyPaste and the helper, and what a step + may do with or without it. RaidableBases' profiles appear in Mod configuration under RaidableBases. diff --git a/website/EVENTS.md b/website/EVENTS.md index d09bcf9..168e9e0 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -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. | +| 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. | > **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 diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 2e149c8..b50bf54 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -26,13 +26,29 @@ here extends the contract first, in this file, before the module is written agai Core exports a single integer-major semver string from `server/src/modules/version.js`: ```js -const MODULE_API_VERSION = '1.15.0' +const MODULE_API_VERSION = '1.16.0' ``` The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first module chunk evaluates, which is earlier than any network round trip could answer. +**1.16.0 — a dry run carries a module's advice** (`website/EVENTS.md` §I; Rust +`modules/rust/PLAN_REDESIGNS.md` §11.4, D391). One optional reading of an existing member and no +removal, so minor. + +- **`{ ok: true, detail: { warnings: ["…"] } }`** from `perform({ verify: true })`: each line is a + **warning** finding, code `advice`, anchored to its step like every finding. It **never fails the + dry run**: advice is about the world as it stands now, which may change before the run does. A raid + base's spot is the first user: *"the spot is refused now: a player is within 50 m"*, *"Rock is too + large"*. +- **Strings only**, trimmed, the empty dropped, **at most ten** per step, each **cut at 500 + characters**. Anything else under `warnings` is ignored rather than refused. Read only on a step that + would run: a refusal is its own finding and carries no advice. +- A real run's `detail` is unchanged: whatever a module puts there still reaches the run log as it did. +- Module-uo's `^1.10.0` still resolves and says no warnings. `website/STEAM_SIGNIN.md`'s claim moves + to 1.17.0. + **1.15.0 — a failed step may say when to try again** (`website/EVENTS.md` §F; Rust `modules/rust/PLAN_REDESIGNS.md` §14.3.13, D371). One optional envelope member and no removal, so minor. @@ -51,7 +67,7 @@ minor. gave one; the run console reads *"retrying at 20:10"*. - `revert()` and `reconcile()` answers are unchanged: cleanup keeps its own retry counter. - Module-uo's `^1.10.0` still resolves and answers none of it. This took 1.15.0 ahead of - `website/STEAM_SIGNIN.md`, which had claimed it and now names 1.16.0. + `website/STEAM_SIGNIN.md`, which had claimed it and moved on (now 1.17.0, after 1.16.0's dry-run advice). **1.14.0 — a param that holds several values** (`website/EVENTS.md` §F; Rust `modules/rust/PLAN_REDESIGNS.md` §12.3, D344). One optional flag and no removal, so minor. @@ -275,6 +291,8 @@ field: means "this will never work" says `retry: false`. - **A retry may say when** (1.15.0): `{ ok: false, retry: true, retryAfterMs }`, clamped to one to thirty minutes, still one attempt. Without it, the flat `EVENT_STEP_RETRY_MS`. +- **A dry run may advise** (1.16.0): `verify: true` answered `{ ok: true, detail: { warnings } }` shows + each line as a warning finding that does not fail the dry run. Advice, never a refusal. - **`inForce()` is a fourth question, not a fourth spelling of `read()`.** Optional, and answering `{ ok: true, held: false }` is the only thing that takes a lease's ledger row out — everything else, including a throw and a lease that declares no `inForce()` at all, leaves the row alone. @@ -469,6 +487,10 @@ branches lacks — so it is the only place `permits` is true between two values between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise unchanged, and so is every rule about composition narrowing rather than widening. +**1.16.0 — a dry run's `detail.warnings`** (`website/EVENTS.md` §I; Rust PLAN_REDESIGNS §11.4, D391). +A module's advice on a step that would run, shown as warning findings that never fail the dry run; one +optional reading, so minor; see §1.1. Module-uo's `^1.10.0` still resolves. + **1.15.0 — a failure's `retryAfterMs`** (`website/EVENTS.md` §F; Rust PLAN_REDESIGNS §14.3.13, D371). A retryable failure may say when to try again, clamped to one to thirty minutes; one optional envelope member, so minor; see §1.1. Module-uo's `^1.10.0` still resolves. @@ -896,7 +918,7 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) // api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0 api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0 api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0 -api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0 +api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0; a dry run's detail.warnings 1.16.0 api.registerEventBudgets([{ id, label, unit }]) // 1.10.0 api.registerEventLeases([{ id, label, type, min, max, values, example, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0; example 1.13.0 api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0; a row's note 1.13.0 diff --git a/website/STEAM_SIGNIN.md b/website/STEAM_SIGNIN.md index 5c7f671..e1604fc 100644 --- a/website/STEAM_SIGNIN.md +++ b/website/STEAM_SIGNIN.md @@ -60,7 +60,7 @@ assertion is single-use, which the existing single-use tx cookie already enforce - Admin → Auth shows Steam with an enable switch and, per Q3, an optional Web API key stored like a client secret. - The account page lists Steam identities with a *Link Steam* button, and each linked Steam account is shown with its id (or name, Q3) and an *Unlink* button. -- **Module contract, MODULE_API 1.16.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs`, Rust +- **Module contract, MODULE_API 1.17.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs` and 1.16.0 by a dry run's advice, Rust PLAN_REDESIGNS §14.3.13):** a read of a user's verified identities for one provider, and a hook when one is linked or unlinked. Names are settled at build. Module-uo uses neither, and runs its usual frozen-manifest proof against the new core.