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

@@ -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.

View File

@@ -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. `<worldSize>.<seed>.<salt>` for a procedural map, `custom.<sha256 of the level URL>` 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` | |