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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user