feat(bridge): protocol 11 — the map (phase 14) #11

Merged
whitlocktech merged 3 commits from feat/phase-14-map into edge 2026-09-25 07:00:23 -05:00
Member

The bridge's part of the live map (PLAN.md §30, PROTOCOL.md §17). Protocol 11, and overlay.toml moved to match.

Four verbs. None of them is behind EventsEnabled, because they only read.

  • map.info: the map key, where the picture comes from (companion, rendered or none), its size, SHA-256 and slice count, the game's own grid, and the monuments the in-game map shows.
    • The Rust+ cache is read by reflection, by name only. CompanionServer types hold the Rust+ secret, and a probe that enumerated one printed it into the log.
    • The hash is computed on the thread pool.
  • map.fetch: one 512 KiB slice, base64-encoded and serialized on the thread pool. It is refused stale when the key or hash has moved.
  • map.render: the admin fallback (D109). It answers accepted at once and renders half a second later. The result is kept in data/RunicGateway/map.jpg beside its key, so a restart on the same map serves it again.
  • map.live: every layer in one answer.
    • The world and base sets are kept by OnEntitySpawned and OnEntityKill, and seeded by one sweep.
    • Bases are positions only: no owner, no authorised list, no shop name.
    • MapMaxSleepers and MapMaxBases bound the two unbounded layers.
    • rg.map prints the state and what an ask costs.

Also in this PR:

  • D119, asked mid-build: Grid() now calls MapHelper.PositionToString. Phase 3's 146.3 m cell put a position up to a row too far south. The game fits a whole number of cells to the world, so a 3000 map has 20 cells of exactly 150 m.
  • Two walk fixes:
    • map.live reads the Steam id from userID, because UserIDString was empty for a server-made sleeper.
    • rg.map re-reads the picture instead of printing the last answer.

Walked on both rigs, Oxide and Carbon, from this source with no conditional compilation. On the rig:

  • The sweep took 5 ms and an ask under 1 ms.
  • A render stalled the game for 8 141 ms and came out byte-identical to the Rust+ cache.
  • The same seed gives the same picture on both frameworks.

Full walk: docs PR, §30.7. checkPlugin.js: 32 hooks, all void and all listed.

  • AI-assisted: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

The bridge's part of the live map ([PLAN.md §30](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust/PLAN.md), [PROTOCOL.md §17](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/rust-link/PROTOCOL.md)). Protocol 11, and `overlay.toml` moved to match. **Four verbs.** None of them is behind `EventsEnabled`, because they only read. - **`map.info`**: the map key, where the picture comes from (`companion`, `rendered` or `none`), its size, SHA-256 and slice count, the game's own grid, and the monuments the in-game map shows. - The Rust+ cache is read by reflection, **by name only**. `CompanionServer` types hold the Rust+ secret, and a probe that enumerated one printed it into the log. - The hash is computed on the thread pool. - **`map.fetch`**: one 512 KiB slice, base64-encoded and serialized on the thread pool. It is refused `stale` when the key or hash has moved. - **`map.render`**: the admin fallback (D109). It answers `accepted` at once and renders half a second later. The result is kept in `data/RunicGateway/map.jpg` beside its key, so a restart on the same map serves it again. - **`map.live`**: every layer in one answer. - The world and base sets are kept by `OnEntitySpawned` and `OnEntityKill`, and seeded by one sweep. - Bases are **positions only**: no owner, no authorised list, no shop name. - `MapMaxSleepers` and `MapMaxBases` bound the two unbounded layers. - `rg.map` prints the state and what an ask costs. **Also in this PR:** - **D119, asked mid-build: `Grid()` now calls `MapHelper.PositionToString`.** Phase 3's 146.3 m cell put a position up to a row too far south. The game fits a whole number of cells to the world, so a 3000 map has 20 cells of exactly 150 m. - Two walk fixes: - `map.live` reads the Steam id from `userID`, because `UserIDString` was empty for a server-made sleeper. - `rg.map` re-reads the picture instead of printing the last answer. **Walked on both rigs**, Oxide and Carbon, from this source with no conditional compilation. On the rig: - The sweep took 5 ms and an ask under 1 ms. - A render stalled the game for 8 141 ms and came out byte-identical to the Rust+ cache. - The same seed gives the same picture on both frameworks. Full walk: docs PR, §30.7. `checkPlugin.js`: 32 hooks, all void and all listed. - [x] AI-assisted: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 3 commits 2026-09-25 06:33:05 -05:00
Four verbs (PLAN.md §30.2), all outside EventsEnabled because none of them
changes the world:

- map.info: the map key, the picture's source (the Rust+ cache, a render
  kept on disk, or none), its size, SHA-256 and slice count, the game's own
  grid, and the monuments the in-game map shows. The cache is read by
  reflection by name only, never enumerated (it holds the Rust+ secret).
  The hash is computed on the thread pool.
- map.fetch: one 512 KiB base64 slice, encoded on the thread pool, refused
  `stale` when the key or hash moved.
- map.render: the admin fallback (D109). Answers `accepted` at once and
  renders on a later frame, keeping the result in data/RunicGateway/map.jpg
  beside its key so a restart within the map does not render again.
- map.live: every layer at once (world events, event-owned zones/crates/NPCs,
  players and sleepers, TCs and player vending machines as positions only).
  The sets are kept by OnEntitySpawned/OnEntityKill and seeded by one sweep
  at OnServerInitialized. MapMaxSleepers/MapMaxBases bound the two layers.

Also fixes Grid() (D119): it used a 146.3 m cell, but the game fits whole
cells to the world (20 x 150 m on a 3000 map), so rows could be one off.
It now asks MapHelper.PositionToString, the in-game map's own labeller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The phase 14 walk read `steamId: null` for every sleeper made on the
server: UserIDString is a cached string the game fills for a player it has
seen connect. An id the map cannot name is an own dot and a clan mate the
site can never match, so the map reads the id from userID itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
fix(bridge): rg.map re-reads the picture rather than printing the last answer
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m35s
ef78da8ce6
The walk emptied the Rust+ cache and rg.map still said `companion`: it
printed the picture the last map.info had seen. It now re-reads while the
world is ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit b5b13161c7 into edge 2026-09-25 07:00:23 -05:00
whitlocktech deleted branch feat/phase-14-map 2026-09-25 07:00:24 -05:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Plugins#11
No description provided.