docs(rust): phase 12 as built and walked — §27.5-27.7, PROTOCOL.md §14
Walked on both rigs through real core: 21 keys observed live, the game's own deadline with core stopped, drift, reload and restart mid-hold, and the switch. Records two defects fixed in the phase and four found outside it and raised: the phase-7 grant owner bug, core restoring orphaned lease rows over a later run, core.lease dropping retry:false, and the 10s budget squeeze. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -998,7 +998,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 10 | **Notifications and engagement** (R7). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §25 (D59–D68).** The criterion walked on the Oxide rig (two authorised owners alerted once each; bystander, raider and an unlinked owner told nothing), and the same plugin loaded on Carbon; a live walk found that every generic notice never said which server (§25.6). Streams, triggers with `ceiling` and `subjectKey`, audiences and engagement seeds — **the catalogue is §10, as corrected by §25.2**. The announce leg and the post hook are **not** in it (D62 amends R7), and protocol 7 widens the raid frame, which brings the two bridge repositories in | all 3 + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||
| 11 | **Android leg C** (R10). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §26 (D69–D72).** The link check behind personal streams asked `module-uo` and locked the raid alert on every Rust site; it now asks the site's own module and holds back only push-on. A tickle is titled from the inbox row it names, and two links stopped opening the browser. **The relay hop was walked on `ntfy.sh` for the first time on any site**: a rig raid reached the emulator in 19 seconds, and after push was switched off in the app the next raid enqueued in-app only | Android-app + docs | A Rust notification arrives on a phone and can be switched off there |
|
||||
| 12 | **Events: option sources and the leases** (§9, **as corrected by §27**). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised. **Plan of record in §27 (D73–D79)**: the target names the server, game convars only (vanilla Rust has no gather/craft/smelt rate), the weekend-VIP lease is a group-wide permission, an `EventsEnabled` switch off by default, seven-day holds, and no budget dimension until phase 13; protocol 8 | Module-Rust + 2 + docs | A leased value is observed changing in the running game and restored, per key; `rust.group.permission` expires without core asking |
|
||||
| 12 | **Events: option sources and the leases** (§9, **as corrected by §27**). ✅ **Built and walked 2026-09-24 on both rigs — as built and findings in §27.5–27.7.** 21 keys walked live, two `spawn.max_*` left out; two defects of its own fixed (the Oxide grant owner, `EventsEnabled` not applying); four found outside it and raised. [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised. **Plan of record in §27 (D73–D79)**: the target names the server, game convars only (vanilla Rust has no gather/craft/smelt rate), the weekend-VIP lease is a group-wide permission, an `EventsEnabled` switch off by default, seven-day holds, and no budget dimension until phase 13; protocol 8 | Module-Rust + 2 + docs | A leased value is observed changing in the running game and restored, per key; `rust.group.permission` expires without core asking |
|
||||
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate, plus **`reconcile()` and the boot-id watch calling `ctx.events.reconcile()`** (§11.1) | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it; a wipe reconciles the ledger instead of stranding it |
|
||||
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
|
||||
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
|
||||
@@ -4252,6 +4252,110 @@ on the Oxide rig against real core, with an event run authored in the admin UI:
|
||||
6. **Carbon.** The same plugin gets a sidecar on the Carbon rig (`INSTALL_RIG.md`), and at least one
|
||||
key from each family is walked there. If that cannot be done, this section says so.
|
||||
|
||||
### 27.5 As built, 2026-09-24
|
||||
|
||||
[Rust-Plugins][rp], [Rust-Link][rl] and [Module-Rust][mr] on `feat/phase-12-leases`, into `edge`.
|
||||
**Core is unchanged and `MODULE_API` did not move**, which D73 bought. Walked against real core on
|
||||
`main` (a fresh `rustp12` database) with this module installed, on **both** rigs.
|
||||
|
||||
**The catalogue as shipped** is narrower than §27.2 planned, and each cut was made by the walk:
|
||||
|
||||
| Lease | Keys | Walked |
|
||||
|---|---|---|
|
||||
| `rust.decay.scale` | `decay.scale` | a lone twig foundation with `decay.tick` lowered on the rig lost 0.081 health per 32 s at 1, **0.404 at a lease of 5**, and 0.081 again after release |
|
||||
| `rust.population` | **18** animal and vehicle populations | every one: the game's own target in `spawn.report` moved and came back (bear 18 → 45 → 18, and so on) |
|
||||
| `rust.spawn.scalar` | `spawn.min_rate`, `spawn.min_density` only | `spawn.scalars`: the population rate 0.5 → 2 → 0.5, the density 0.5 → 1.5 → 0.5 |
|
||||
| `rust.group.permission` | any registered permission on any group | a synthetic player in `default` passed `UserHasPermission` while the lease was held and failed it after |
|
||||
|
||||
- **The two `spawn.max_*` scalars are not lent.** The game interpolates between min and max by how
|
||||
full the server is, so on an empty rig the effective values are the minimums and the maximums do
|
||||
nothing that can be seen. §9's rule is that a key which cannot be seen doing something is not
|
||||
advertised. A walk with players online is what would add them.
|
||||
- **Every population is per square kilometre, vehicles included.** The game's own help text says
|
||||
vehicles are "active on the server", but on the 9 km² rig a value of 3 is a target of 27.
|
||||
|
||||
**What the walk proved, against §27.4:**
|
||||
|
||||
1. **Each key's effect, observed**, as in the table. The first run held all four families through
|
||||
core and tore them down at the end: every ledger row `reverted`, every value back.
|
||||
2. **The game's own deadline, with the website gone.** A run took a 3-minute hold on a group
|
||||
permission and on `decay.scale`, then core was **stopped**. At the deadline the plugin restored both
|
||||
and emitted two `lease.expired` frames, which the sidecar stored. When core came back, its boot
|
||||
reconcile asked about both holds, was told `held: false`, and marked both rows `orphaned` with the
|
||||
module's reason.
|
||||
3. **Drift.** `decay.scale 3` typed at the console during a hold. At teardown the row read `drifted`,
|
||||
with *"the value is now "3" rather than what this run applied, so it was left alone"*, and the
|
||||
world was left at 3.
|
||||
4. **Reload and restart.** A plugin reload in the middle of three holds kept all three, deadlines
|
||||
intact (`leases: 3 held`). A **server restart** in the middle of a convar hold and a permission
|
||||
hold logged `leases: 1 held, 1 ended by a restart`: `decay.scale` came back at 1 and the permission
|
||||
stayed granted. At teardown the permission was given back and the convar release found it
|
||||
`already` restored. Both rows `reverted`.
|
||||
5. **The switch.** With `EventsEnabled` off, an apply was refused with the switch named. It was
|
||||
turned on **from the site's own config editor**, which is how an operator would do it (below).
|
||||
6. **Carbon.** The same plugin file, with a sidecar installed in the Carbon rig's container the way
|
||||
`INSTALL_RIG.md` describes. All 21 keys resolved there and none was `Saved`. Through core: bear
|
||||
target 54, `decay.scale` 5 (0.47 per 32 s), `spawn.min_rate` 2, and a group permission
|
||||
(`adminmodule.greet`, which Carbon registers itself) granted and then given back. All rows
|
||||
`reverted`.
|
||||
|
||||
The permission mirror's side of §27.3 was walked too. A `perm.sync` sent during a hold listed the
|
||||
pair under `leased` and not under `foreign`, and the site's wish was stored on the hold. At teardown
|
||||
the pair stayed granted (`already true`), because the site had asked for it.
|
||||
|
||||
### 27.6 What the walk found that the plan did not say
|
||||
|
||||
**Two defects in this phase's own code, both fixed before merge:**
|
||||
|
||||
- **Every group-permission grant was a silent no-op.** `GrantGroupPermission(group, perm, this)`:
|
||||
given an owner, Oxide first asks `PermissionExists(perm, owner)`, meaning *did this plugin register
|
||||
the name*, and returns without a word when it did not (confirmed in the decompiled
|
||||
`Oxide.Core.Libraries.Permission`). A lease only ever borrows another plugin's permission. The
|
||||
plugin's read-back caught it (*"the store did not grant …"*), and the owner is now `null`.
|
||||
- **`EventsEnabled` did not take effect when an operator changed it.** The site's editor saved the
|
||||
file and answered `changed: true`, and the plugin went on refusing. It never re-reads its own
|
||||
config, because it never reloads itself (§21.3). It is the first bridge setting an operator is
|
||||
*meant* to change from the site, so a write to the bridge's own file now re-reads that one key.
|
||||
The report says so: *"EventsEnabled took effect at once; the bridge's other settings apply on its
|
||||
next reload."*
|
||||
|
||||
**Four findings outside this phase, raised and NOT fixed**:
|
||||
|
||||
- **The phase-7 permission mirror has the same owner bug.** `EnsureGroupPermission` and
|
||||
`EnsureGrant` both call Oxide with `this` and count the grant without reading it back. On Oxide,
|
||||
then, every site-authored grant of another plugin's permission has probably never landed, while the
|
||||
report counted it as applied. It stayed hidden because every earlier walk used a permission that was
|
||||
`unresolved` (Kits was not installed) and so never reached the call. The fix is one token in two
|
||||
places plus a read-back. It needs a decision because it changes what the site has been told since
|
||||
phase 7.
|
||||
- **Core's cleanup restores `orphaned` lease rows, and that can overwrite a later run's world.** Run
|
||||
2's rows were orphaned at core's boot reconcile, and run 2 was cancelled. Cleanup then tried to
|
||||
move them back to `reverting`. That collided with run 3's live row for the same target on
|
||||
`uq_evres_target` and failed on every sweep (13 times). Once run 3 released, the stale `restore()`
|
||||
went through and **revoked the permission run 3 had deliberately left granted**. A lease's
|
||||
compare-and-set cannot catch this when the later holder applied the same value. For a spawn,
|
||||
reverting something that is gone is harmless. For a value, "no longer in force" plus a later
|
||||
restore is a write of an old baseline over the present.
|
||||
- **`core.lease` drops the module's `retry: false`.** Both of its failure returns rebuild the envelope
|
||||
as `{ ok: false, error }`, so an `events-disabled` or `unresolved` refusal is retried three times.
|
||||
The Carbon walk's step 27 did exactly that. Harmless, but it is noise and it makes
|
||||
`retry: false` unreachable in a lease callable.
|
||||
- **`core.lease` runs under the default 10 s with two module calls inside it.** The module fits by
|
||||
giving lease calls a 4.5 s timeout (asserted in a test), which is below the sidecar's 10 s reply
|
||||
timeout. A `budgetMs` on `core.lease` would remove the squeeze. §27.1 asked what a late-landing
|
||||
retry does; the module's release-after-timeout (§14.7) covers the world side, and the question
|
||||
stands for core.
|
||||
|
||||
### 27.7 What is not proven here
|
||||
|
||||
- **`spawn.max_rate` / `spawn.max_density`**, for want of players. They are left out, not guessed.
|
||||
- **A real player seeing a kit unlock.** Kits is not installed on either rig and there is no second
|
||||
client. The permission was observed through `UserHasPermission`, which is what Kits calls.
|
||||
- **A crash, as opposed to a restart**, for the holds file. The same load path runs either way.
|
||||
- **Walk 3's permission outcome as first observed.** It came out `false`. Walk 4 then showed the
|
||||
intended behaviour (`already true`), and the cause turned out to be the core cleanup finding above,
|
||||
not the plugin.
|
||||
|
||||
---
|
||||
|
||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||
|
||||
Reference in New Issue
Block a user