docs(rust): phase 14 as built — the live map (PLAN.md §30.6–§30.9, PROTOCOL.md §17)

- PLAN.md §30.6: D119 (the grid label is the game's own, MapHelper) and D120
  (Leaflet as a lazy chunk), both asked mid-build.
- §30.7: what was built, where it departed from §30.2, and the eight-step
  walk on both rigs against real core.
- §30.8: what the walk found — UserIDString empty for a server-made player,
  the game now renders its map at every boot for Facepunch's map-image
  service, and `app.port -1` reaches the game as `1`.
- §30.9: what needs people in the game.
- PROTOCOL.md §17 for protocol 11, and a note in §8 that `grid` is the game's
  own label from protocol 11 (D119).
- PLAYER_WALK.md: the map walk.
- The phase 14 row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-25 06:31:54 -05:00
parent 1538b4e9cd
commit 6fdb081087
3 changed files with 261 additions and 1 deletions

View File

@@ -1006,7 +1006,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 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 |
| 13a | **Events: the world verbs** (§9, R17, **split by D80**). ✅ **Built and walked 2026-09-24 on both rigs — plan in §28 (D80–D95), as built and findings in §28.5–28.8 (D96–D97).** A restart mid-run found that the reconcile asked a world that had not loaded, and the plugin pruned live crates on the empty answer; fixed with `worldReady` (§28.7). The owner fix was walked before and after. The placing verb became two (D97), because core infers cap boxes from examples. The phase-7 owner fix (D85); `rust.zone.open` and `rust.prefab.place` (crates and NPCs, D88) at a monument or raw coordinates (split into `rust.crate.place` and `rust.npc.place` by D97); the monument and prefab option sources; the plugin's ownership registry, keyed by the idempotency key; and **`reconcile()` with the boot-id and wipe watch calling `ctx.events.reconcile()`** (§11.1). Protocol 9 | all 3 + docs | A wipe reconciles the ledger instead of stranding it, and each world verb's teardown is observed in the game |
| 13b | **Events: the rewards** (§9, R3, R16). ✅ **Built and walked 2026-09-24 on both rigs, without a player — plan in §29 (D98–D105), as built and findings in §29.6–29.9 (D106–D108).** Real core refused the module over one camelCase option-source id, which 310 green tests had not caught (§29.8). The steps that need a person are the rewards walk in `PLAYER_WALK.md`. The participation tally kept by the game (D81–D83), `rust.kit.entitle` through the site mirror's per-run rows (D84) with the kit option source flagging kits with no permission gate, and `rust.announce` with the announce leg (D90). Plan of record in §29, written before its code. Protocol 10 | 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 |
| 14 | **The live map** (R9). 📐 **Plan of record in §30 (D109–D118), 2026-09-24.** A rig probe found that the `.map` file is terrain, not a picture, and that Rust+ already caches the rendered JPEG; the game's own render stalls the main thread 8.5 s (§30.0). Protocol 11. *As first written:* 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 |
| 14 | **The live map** (R9). ✅ **Built and walked 2026-09-25 (§30.7), protocol 11, D119–D120 (§30.6); the person-parts are the map walk in `PLAYER_WALK.md`.** 📐 **Plan of record in §30 (D109–D118), 2026-09-24.** A rig probe found that the `.map` file is terrain, not a picture, and that Rust+ already caches the rendered JPEG; the game's own render stalls the main thread 8.5 s (§30.0). Protocol 11. *As first written:* 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 |
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
| 17 | **Optional mod integrations** (R15). **BetterChat** first — leaderboard titles through `API_RegisterThirdPartyTitle`, a pull with no drift — then the uMod **Clans** adapter (alliances and clan chat, beside the provider rather than under it, R5), then others as they prove useful | Rust-Plugins + Module-Rust + docs | A server missing every optional mod still runs the module, Teams included |
@@ -5284,6 +5284,122 @@ them before code.
- **Locked crates, supply drops and the helicopters are in "world events"**, not their own layers.
D112 grouped them, and one switch per group is what an operator reasons about.
Review merged this section (docs#271) with no comment, so all six readings stand.
### 30.6 Two more decisions the build needed
Both came from the org lead on 2026-09-25, asked mid-build.
| # | Decision |
|---|---|
| **D119** | **The plugin's grid label is the game's own.** A second probe on the rig asked `MapHelper.PositionToString` for fifteen positions. The game fits a whole number of cells to the world: a 3000 map is **20 × 20 cells of exactly 150 m** (A0 to T19, the origin K10). Phase 3's `Grid()` used a fixed 146.3 m cell, the number community tools quote, and put a position up to a row too far south (the ferry terminal read O17 where the game says O16). It was fixed in this phase: `Grid()` now calls `MapHelper.PositionToString`, and the map's grid comes from `MapHelper` as well. Stored history keeps its old labels. §30.3's "150 m cells" was right for this map by luck of arithmetic. The page never assumes a cell size; it draws `gridCells` cells of `gridCellSize` metres, both from `map.info`. |
| **D120** | **Leaflet is a lazy second chunk, not part of `entry.js`.** D116 said "bundled into the chunk". But `entry.js` loads on every page of the site, and Leaflet touches `document` the moment it is evaluated, which breaks the registration test that evaluates the chunk in Node. The Map tab does `import('../lib/leaflet.js')`, and Vite emits `dist/leaflet-<hash>.js` beside `entry.js`. Core serves that directory (MODULE_API §3.1). `release.yml` now copies every `dist/*.js`, and `checkExternals.js` and `build.test.js` hold both ends: every chunk `entry.js` imports exists and is clean, Leaflet is not in `entry.js`, and the release copies them all. |
### 30.7 As built, 2026-09-25
[Rust-Plugins][rp], [Rust-Link][rl] and [Module-Rust][mr] on `feat/phase-14-map`, into `edge`, as protocol 11
([`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §17). **Core is unchanged and `MODULE_API` did not move.**
| Repo | What |
|---|---|
| Rust-Plugins | `map.info`, `map.fetch`, `map.render` and `map.live`; the Rust+ cache read by name; hashing and slice encoding on the thread pool; the render kept in `map.jpg` and `map.json`; world and base sets kept by `OnEntitySpawned` / `OnEntityKill` and seeded at `OnServerInitialized`; `MapMaxSleepers` and `MapMaxBases`; `rg.map`; `Grid()` from `MapHelper` (D119) |
| Rust-Link | Four forwards and `PROTOCOL_VERSION` 11, with a test that a base64 slice fits the line cap |
| Module-Rust | `rust_map_images` and `rust_map_overrides`; `mapImages.js` (the D110 fetch, Render now); `mapLive.js` (the D111 cache); `model/map` (layers, D113, D115, D117, D118, the per-viewer projection, `DERIVATION_VERSION` 1); the three public routes and two admin routes; the Map tab and the Map card; capability `map`; Leaflet as a lazy chunk (D120) |
**Where the build departed from §30.2, and why:**
- **`map.info` also carries `gridCells` and `gridCellSize`**, from the game's `MapHelper` (D119), and
`rust_map_images` stores them. §30.2 had the page derive the grid from a constant.
- **A server whose game has no picture still gets a row.** `bytes` and `sha256` are NULL and
`source` is `none`, and the row keeps the geometry and monuments, so the page draws the layers on a
plain background. That background is sized as the Rust+ cache would be (half scale plus the margin),
so a picture that arrives later fills the same frame. The same map with a picture already stored
keeps it when the game later has none.
- **"One fetch in flight" is the module's rule, not the plugin's.** A slice is answered within one
frame on the main thread, so two can never overlap in the plugin. The plugin refuses slices with
`busy` only while a render runs. The module holds one fetch per server.
- **The live answer names who is asking.** `GET …/map` returns `mates.signedIn`, so the page offers
"link your Steam account" only to someone who has an account (a walk finding, §30.8).
- **Grid labels are shown only when a cell is at least 30 px wide on screen.** At the fitted zoom a
20-cell map's labels overlapped into a wall of text.
- **The Leaflet chunk is 76 KB gzipped**, not the ~40 KB D116 was weighed on. Vite's library build
mangles names but never strips whitespace, for the entry chunk too. It loads only on the Map tab
(D120), so it was left alone rather than changing how every chunk is built.
- **Positions are read from `userID`**, never `UserIDString` (§30.8).
**Walked against real core on `main`** (the `rustp12` database, this module staged from the branch)
and the two rigs, 2026-09-25. There is no player on either rig, so a rig-only probe plugin made
stand-ins: sleepers under Steam ids linked to five throwaway website users, one of them moved into
the game's online list for a minute; a vending machine; a patrol helicopter and a cargo ship. The
first-party clan *Northwatch* already on the Oxide rig (phase 9) holds two of those Steam ids.
1. **The cache, and its absence (step 1).** At boot the site fetched the Rust+ picture by itself on
first sight (D110): `companion`, 360 341 bytes, 2500 × 2500, stored at the first board poll.
**The rig could not be booted without a picture** (§30.8), so the probe emptied the cache field
by name and `map.info` answered `none`. Fetch again answered `current` and kept the stored picture,
because it is the same map. Render now answered 202 with an estimate of 9 s. The plugin drew the
map in **8 141 ms of stall**, byte-identical to the cache (the same SHA-256), and the site moved the
row to `rendered`. A second Render now while the first ran was refused 409. After a restart on the same map, with the cache emptied again, `map.info` answered **`rendered` from the file on disk**, and the boot drew nothing.
2. **The geometry (step 2).** In the browser, monument markers sit on the monuments in the picture.
The page's grid label equals the game's at all fifteen positions the probe asked
`MapHelper.PositionToString` for, and the client suite holds those fifteen answers.
3. **A wipe (step 3).** The rig restarted on seed 1235. `map.info` gave a new key, `3000.1235.1`, and
at the next board poll the site fetched the new picture (398 896 bytes) and replaced the old row.
A slice asked for with the old hash is refused `stale`, on the plugin and in the module's suite.
4. **The switches (step 4).** At the defaults, an anonymous viewer's `map/live` carries `world` and
`events` and **no `players` or `bases` key at all**, and a signed-in viewer gets the same. Staff
get all four. Widening the players layer to `public` with presence at `staff` showed an anonymous
viewer nothing more, and `/map` said `cappedByPresence`. Widening both showed the three sleepers.
A per-server override opened the Oxide rig alone; the Carbon rig stayed at staff.
5. **Own dot and mates (step 5).** walkp1, who is linked but not staff, with the players layer at
`staff`: with everybody asleep they saw only their own sleeper, marked `self`. With the clan mate
WalkTwo and the stranger WalkFour moved online, walkp1 saw WalkTwo and not WalkFour, while staff
saw all three. **With the clan roster widened to `public`** nothing about positions changed:
walkp4, outside the clan, still saw only their own dot, and an anonymous viewer saw none (D117).
**With the Oxide rig's mates switch off**, walkp1's own dot and their mate's both went.
6. **Live things (step 6).** A patrol helicopter and a cargo ship spawned on the rig were on the next
anonymous poll and moved across three polls ten seconds apart. A 13a zone and an elite crate for a
run appeared under `events`, and after the run's revert `events` was empty.
7. **Cost (step 7).** Twenty viewers polling once a second for thirty seconds made 600 requests. The
plugin's own counter moved from 13 to 19: **one `map.live` per five seconds**. The slowest ask took
1 ms.
8. **Carbon (step 8).** On the Carbon rig at protocol 11, the site fetched the picture by itself.
It is byte-identical to the Oxide rig's for the same seed (SHA-256 `28da6e8a…`). `map.live`
answered for anonymous and staff viewers, and `rg.hooks` showed `OnEntitySpawned` and
`OnEntityKill` firing on Carbon.
### 30.8 What the walk found that the plan did not say
- **`UserIDString` is empty for a player the game did not see connect.** Every stand-in sleeper came
back with `steamId: null`, which on a real server would be an own dot and a clan mate the site can
never match. `map.live` reads the id from `userID`. The players board (phase 3) still uses
`UserIDString` for connected players, where it has always been set.
- **The legend told an anonymous visitor to link a Steam account.** `/map` now says `signedIn`.
- **The game now draws its map at every boot, whatever Rust+ is doing.** The boot log reads
`[Rust.MapCache-Images] Image uploaded to backend: …/86b01198b36d…jpg`, and that is exactly the
SHA-256 `map.info` reported for the cache. Facepunch renders the map for its own map-image service,
and the Rust+ cache holds the same bytes. So **the render fallback (D109) is for a server that
somehow has no picture, and the walk could not make one by configuration**: `+app.port -1` on the
command line reached the game as `1`, and so did `app.port -1` and `app.port "-1"` in `server.cfg`.
The value parser drops the minus sign; at the console the convar takes `-1`. §30.0's open question
("is the cache empty when `app.port` is off?") is therefore unanswered, and on a current server it
probably does not arise. Render now stays, as the admin's way out of a server that answers `none`,
and phase 18's egg should not promise that `app.port -1` switches Rust+ off.
- **A tool cupboard placed by code is destroyed by the game within two seconds**, on terrain and on a
foundation, so the rig could not show a real TC on the Bases layer. The layer's tracking was walked
with a vending machine. A TC is in the map walk in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md).
- **The cargo ship and its crates were outside the picture**, past the world and the ocean margin. The
page places them anyway, and the view may pan half a map past the picture's edge.
### 30.9 What is not proven here
- **A real player.** A moving own dot, a clan mate dropping off the map when they leave the clan or
log off, a real base on the Bases layer and a crate being hacked all need people in the game. They
are in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md) as the map walk.
- **The admin Map card in a browser.** Its data was walked through the API: switches saved, the
picture's state, Fetch again. The page itself was built and its tests pass, but signing in as an
admin in the walk browser means typing a password, which was left to the org lead's review.
---
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app