From 6fdb081087bab832276869796fb6e8445a568bd1 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 25 Sep 2026 06:31:54 -0500 Subject: [PATCH] =?UTF-8?q?docs(rust):=20phase=2014=20as=20built=20?= =?UTF-8?q?=E2=80=94=20the=20live=20map=20(PLAN.md=20=C2=A730.6=E2=80=93?= =?UTF-8?q?=C2=A730.9,=20PROTOCOL.md=20=C2=A717)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN.md | 118 ++++++++++++++++++++++++++++++++++++++- rust-link/PLAYER_WALK.md | 32 +++++++++++ rust-link/PROTOCOL.md | 112 +++++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+), 1 deletion(-) diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 21f3297..0d1fe0b 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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-.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 diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index fe882e9..556f5fa 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -327,3 +327,35 @@ landing a kill, and opening the Kits menu. of it; the kill lands on the tally; a reward granted while you were offline is in the menu when you come back; its extra use works exactly once; and a revert takes away the right to redeem but never what you already redeemed. + +## The map walk (phase 14, protocol 11) + +The parts of [`PLAN.md`](../modules/rust/PLAN.md) §30.4 that need somebody in the game. Everything +else was walked with stand-ins: sleepers made by a rig-only probe plugin, one of them moved into the +game's online list for a minute (§30.8). What a stand-in cannot be is a person who moves, joins a +clan and builds a base. This walk needs two accounts. + +**Before you start:** + +1. **Two Steam accounts, A and B, both linked on the site**, each signed in to its own browser + session. Neither is staff. +2. **A and B in the same first-party clan** on the server (the in-game clan menu; D115 reads the + game's own clans, not the uMod Clans plugin). +3. The map switches at their defaults (Admin → Rust visibility → *The live map*): players and bases + staff only, *You and your clan* on. + +| # | Do this | You should see | +|---|---|---| +| 1 | Both A and B in game. A opens the server's **Map** tab | A's own dot (cyan) where A stands, and B's dot (green) where B stands. **No other player**, and no Players layer. Walk A somewhere; within ten seconds the dot follows | +| 2 | B leaves the clan | Within one clan board (a minute) B's dot is gone from A's map. A's own dot stays | +| 3 | B rejoins the clan and logs off | A sees only A's own dot: a mate appears only while online (D115). B, signed in to the site while offline, sees B's own **sleeper** | +| 4 | Staff widen the clan roster to *Everyone* | Nothing about positions changes for anybody (D117) | +| 5 | Staff turn *You and your clan* off for this server | A's own dot and B's both disappear from A's map | +| 6 | A places a tool cupboard on a foundation, and a vending machine | A staff viewer sees both on the Bases layer within ten seconds. A does not | +| 7 | A player hacks a locked crate | The crate's tooltip on the map counts the hack down | + +**Run steps 1 and 6 on Carbon as well.** + +**What counts as a pass:** a player sees themselves and their online clan mates and nobody else; a mate +drops off the map when they leave the clan or the server; switching the view off removes it; and a +real base shows to staff only. diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 267e54e..f5e5f2b 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -473,6 +473,12 @@ Every kind protocol 2 defines, and the hook behind it. **`class` is not a field and every community site shows it; a **structure's** grid is where somebody lives, which is why `entity.destroyed` is staff-class here and why R9 makes the same distinction for map layers. +**From protocol 11 the label is the game's own** (`MapHelper.PositionToString`, PLAN.md D119). +Protocols 2 to 10 computed it in the plugin from a 146.3 m cell, the number community tools quote. +That was wrong: the game fits a whole number of cells to the world, so a 3000 map has 20 cells of +exactly 150 m, and the constant put a position up to a row too far south (the rig's ferry terminal read +O17 where the in-game map says O16). Rows already stored keep the label they arrived with. + **Three hooks are deliberately not in this wave, and none of them is an oversight:** `OnEntityTakeDamage` and `OnFrame`/`OnTick` fire at a rate that makes a bridge a performance regression, and nothing in phases 3–19 needs per-hit or per-frame fidelity. R17's warning about chatty zone transitions is the @@ -1721,3 +1727,109 @@ the game is down: | `POST /chat` | `chat.say` | Opaque object | `perm.sync`'s `credits` field passes through untouched, like the rest of that body. + +## 17. Protocol 11 — the map + +Added in phase 14 ([`PLAN.md`](../modules/rust/PLAN.md) §30). The picture of this wipe's map, and +everything that moves on it. **Four commands, two hooks, two plugin config keys and one console +command.** The plugin reads; nothing here changes the world, so **no map command is behind +`EventsEnabled`**. The website decides which layer a viewer may see. The plugin answers every layer +to anyone who asks, because the sidecar and the website are the only things that ask (§8.5). + +### 17.1 The commands + +| Command | Answers | | +|---|---|---| +| `map.info` | `map.info` or `map.error` | What this map is and where its picture comes from. See below | +| `map.fetch` | `map.chunk` or `map.error` | One slice of the picture: `mapKey`, `sha256` and `chunk` (from 0) in; `data` (base64), `chunk` and `chunks` out | +| `map.render` | `map.render` or `map.error` | The admin fallback: `accepted: true` at once, and the render on a later frame | +| `map.live` | `map.live` or `map.error` | What moves, every layer at once. See §17.3 | + +**`map.info`** carries: + +- `mapKey`: which map. `..` for a procedural map, `custom.` for a custom one. +- `source`: `companion` (the Rust+ cache), `rendered` (a render the plugin kept, §17.2) or `none`. +- For a picture: `bytes`, `width`, `height`, `chunks`, `chunkBytes` (524 288), `background` and `sha256`. +- `worldSize`, and `oceanMargin` (500, in **pixels** and never scaled). +- `gridCells` and `gridCellSize`: the game's own grid, from `MapHelper`. On the rig's 3000 map that is 20 cells of 150 m. +- `monuments`: those with a display phrase, which is what the in-game map shows. Each has `value` (the same `kind#n` token as `world.monuments`), `kind`, `label`, `x`, `z` and `grid`. +- `rendering`: whether a render is under way. + +**The hash is computed on the thread pool** the first time a picture is seen. Until it is ready the +answer carries `hashing: true` and no `sha256`, and the caller asks again in a few seconds. + +**`map.fetch` is refused `stale`** when `mapKey` or `sha256` no longer names the picture being served, +so a fetch that straddles a map change cannot splice two maps. The slice is base64-encoded and +serialized on the thread pool. A base64 slice of 512 KiB plus its envelope fits the game link's 1 MiB +line cap, and the sidecar has a test that holds that sum. + +**`map.error` reasons**: + +| `reason` | Means | Worth retrying | +|---|---|---| +| `not-ready` | the world has not finished loading (§15.5) | yes | +| `stale` | the map or picture moved since `map.info` | yes, from `map.info` | +| `no-picture` | this server has no picture to slice | no | +| `malformed` | a slice number outside the picture | no | +| `busy` | a render is under way, or a second render was asked for | yes, later | +| `has-picture` | `map.render` on a server that already has a picture | no | + +### 17.2 Where the picture comes from + +**Rust+ already keeps the picture.** When `app.port` is set, the game renders the map at boot and +holds the JPEG in `CompanionServer.Handlers.Map.ImageData`. The plugin reads that property and the +`_background` field **by name, by reflection, and nothing else of that type**: `CompanionServer` +types hold the server's Rust+ secret, and a probe that enumerated one printed it into the log (PLAN.md +§30.0). The picture's size is read from the JPEG's own header. + +**Without Rust+ there is no picture until an admin asks for one** (D109). `map.render` calls +`MapImageRenderer.Render` at half scale with a 500-pixel margin, which is the cache's geometry. **It +holds the main thread for about 8.5 seconds on a 3000 map.** The answer goes out first, and the render +runs half a second later. The picture is written to `map.jpg` beside `map.json` (its key) in the +framework's data directory, so a restart on the same map serves it again as `rendered` without +drawing a second time. A new map key makes the file stale, and it is ignored. + +### 17.3 `map.live` + +| Field | Rows | +|---|---| +| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true` | +| `events` | what this site's events placed (§15.2): `kind` (`zone`, `crate`, `npc`), `runId`, `x`, `z`, and a zone's `radius` and `name` or a thing's `prefab` | +| `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 | + +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. + +**There is no sweep per ask.** The world and base sets are kept by `OnEntitySpawned` and +`OnEntityKill`, which do two type tests per spawn, and are seeded by one sweep at +`OnServerInitialized`, because a hotload hears no spawns. A player's vending machine is followed, a +monument's (`NPCVendingMachine`) is not. On the rig the sweep of about 42 000 entities took 5 ms, and +an ask took under 1 ms. + +The Steam id comes from `userID`, not `UserIDString`. The phase 14 walk read the latter back empty from +a sleeper made on the server. + +### 17.4 Two bounds, and the console + +Two config keys are written into an existing config the first time protocol 11 loads: **`MapMaxSleepers`** +and **`MapMaxBases`** (2 000 each). Over a bound, the layer answers the first that many and sets its +`…Truncated` flag. **This is not D95's refuse-never-clamp rule**, which is about a value an author +typed. A read has nobody to refuse. + +**`rg.map`** prints the key, the source, the picture's size and hash, whether a render is running, the +game's grid, the size of each set, and how many `map.live` asks there have been, with the last and +slowest. + +### 17.5 The sidecar + +`PROTOCOL_VERSION` becomes 11. There are four routes, each a correlated round trip that fails when the +game is down, and **nothing is stored**: the picture passes through, and positions never touch the +database (D111). + +| Route | Command | | +|---|---|---| +| `GET /map` | `map.info` | | +| `GET /map/chunk?mapKey=&sha256=&n=` | `map.fetch` | All three required | +| `POST /map/render` | `map.render` | Opaque object; `cmd` and `reqId` written over the caller's | +| `GET /map/live` | `map.live` | |