docs(rust): phase 15 as built — the map and events on a phone (PLAN.md §31.5–§31.7)

- PLAN.md §31.5: what was built in website and Android-app, where the
  build departed from §31.2, and the seven-step emulator walk.
- §31.6: what the walk found: the zoom lost twice (fixed), the tally a
  stand-in cannot feed, the off-grid row the frame clamps, the "wiped
  Dec 31, 1" header, the card that outlives its marker.
- §31.7: what is not proven (a real player, a counted tally, a UO site).
- Phase row 15 and the Android plan's M17 marked built and walked.
- EVENTS.md § API surface: the public calendar's run entries carry
  runId (D125). BACKEND_DESIGN defers event routes to that table.
- PLAYER_WALK.md: the map and events on a phone, for the person-parts.

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 12:30:49 -05:00
parent a3fcc42a57
commit 42a0482a9e
4 changed files with 143 additions and 3 deletions

View File

@@ -1007,7 +1007,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 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). ✅ **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. 📐 **Plan of record in §31 (D121–D125), 2026-09-25.** The map is a fifth tab drawn natively in Compose over the module's projected answer, so the app has no gate of its own to get wrong. "Events" is a site-event marker opening the app's own event page, plus M13 walked against a Rust run. That needs one field on core's public calendar (D125) | Android-app + website + docs | The map renders on a phone with the same layer gates |
| 15 | **Android leg D** (R10). Map and events. ✅ **Built and walked on an emulator 2026-09-25 (§31.5); the counted tally and a real player are in `PLAYER_WALK.md`.** 📐 **Plan of record in §31 (D121–D125), 2026-09-25.** The map is a fifth tab drawn natively in Compose over the module's projected answer, so the app has no gate of its own to get wrong. "Events" is a site-event marker opening the app's own event page, plus M13 walked against a Rust run. That needs one field on core's public calendar (D125) | Android-app + website + docs | 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 |
| 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, **the sidecar configured from egg variables** (R22), the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework |
@@ -5580,6 +5580,125 @@ None of these was asked. They are written down so that review can overrule them
- **The admin Map card is not in the app.** It is operator configuration, the same exclusion as
M14 made for the rest of the Rust admin surface.
### 31.5 As built, 2026-09-25
[Android-app][aa] on `feat/rust-phase-15-map`, into `edge`, and [website][web] on
`feat/public-calendar-run-id`, into `main`. **Module-Rust did not change, and `MODULE_API` did not
move.** None of §31.4's readings was overruled, and each is built as written.
| Repo | What |
|---|---|
| website | `runId` on a public calendar entry whose `kind` is `run` (`publicRunEntry`), the `PublicEventEntry` schema and the regenerated spec. The exact-keys test now lists the field. A new test pins that a run entry's id is its run's, that the event page publishes the same id, and that a projection has none (D125) |
| Android-app | `RustTab.MAP` and `Capability.MAP`; `map` and `map/live` in `RustApi` with every layer nullable; `MapFrame` (§30.3 ported, with the web's fifteen probed grid cases) and `MapTransform` (fit, pinch, pan and resize as pure functions); `MapMarkers` (one flat list for drawing, tapping and the card); `EventRunResolver`; `RustMapViewModel` (keyed on the signed-in account); `RustMapPanel` (the canvas, the legend and the card); `runId` on `EventCalendarEntryDto` |
**Where the build departed from §31.2, and why:**
- **The run id is matched as text.** The plugin holds a run's id as a string and `map/live` sends
`"41"`, while core's shapes send `41`. §31.1 said the item carries "core's run id", which is true
of its value and not of its type. The resolver keys on `runId.toString()`, and a test decodes both.
- **A viewer sent nothing that moves is not polled at all**, as the web does. §31.2 said "every
`pollMs` … while RESUMED and on the Map tab"; with no moving layer there is nothing to ask for.
- **An answer asked under one account is dropped when it lands under another.** Keying the state on
the account (§31.2) clears what is drawn. A request already in flight when the account changes
still answers, so the view model counts account changes and drops an answer from an earlier one.
- **The reader's zoom survives leaving the screen and a change in the box's size** (§31.6).
- **The tap card of a site-event marker always says "Site event"** under its title, linked or not.
The title is a zone's name when the layer carried one. The link is the only difference between a
listed run's marker and a rehearsal's.
**Walked on an emulator (`s22_ultra`, API 36) against real core** on the D125 branch, which is
`main` plus the one field, with the `rustp12` database and this module as phase 14 shipped it. The
Oxide rig ran its protocol-11 sidecar. There is no player on the rig, so phase 14's rig-only probe
(`RgProbe14c`) made stand-ins again: sleepers for walkp1 (`…01`, *Brannock*), their clan mate
walkp2 (`…02`, *Wren*, moved into the game's online list) and walkp4 (`…04`, outside the clan), and
a vending machine. Positions and gates were read on the phone and, for each step, from the API as
the same viewer.
1. **The picture (step 1).** The Map tab rendered the rig's picture with the game's grid, the 85
monuments on the monuments, the locked crates and a supply drop. Double tap and pan work, and grid
labels appear once a cell is 30 dp across. Tapping the Outpost gave *Outpost · Grid I8*, drawn in
the I8 cell. **The frame's label equals the game's for 83 of the 85 monuments.** The other two are
the oil rigs, north of the grid, where the game writes `I-3` and `A-3` and the frame clamps to row
0 (§31.6). The web has the same clamp.
2. **The gates (step 2).** Signed out: world and site events drawn, *Players* and *Bases* listed
disabled as *Shown to staff only*. walkp1, signed in and not staff: the same, plus their own dot
and their mate's. walkadmin: all four switches, the three sleepers and the vending machine, and
the mates row offering *Link your Steam account* to this unlinked admin. With the players layer
widened to `public` and presence at `staff`, a signed-out viewer saw *Shown to staff only. Limited
by who may see who is online.* and no players. An override opening bases on the Oxide rig alone
drew its vending machine signed out, while the Carbon rig listed bases as staff only. The settings
were put back afterwards.
3. **Own dot and a mate (step 3).** walkp1 saw their own sleeper (*You · Asleep, offline · Grid I9*)
and Wren (*Wren · Sleeping · Grid I8*), and nobody else. Signed out and in as walkp4: only their
own dot. **On a phone the server screen cannot outlive an account switch.** It has no drawer, so
signing out means leaving it, and the next account opens a new screen. The retained-view-model
case events phase 14b found is therefore covered by the unit tests, which switch the account
under a live view model and under an answer still in flight.
4. **Live (step 4).** A cargo ship and a patrol helicopter spawned by console were on the map at the
next poll, and moved between frames 20 s apart. The cargo ship picked up its locked crate. Core's
request log showed **three asks in 25 s on the Map tab, none in 30 s on another tab, none in 30 s
with the app in the background, and one immediately on return**. The picture was fetched once
across six openings of the tab. Coil's disk cache served the rest.
5. **Events (step 5).** A run of a listed definition (13a's walk 1) placed a zone, three elite
crates and two NPCs, and a rehearsal of 13b's walk 6 placed a second zone beside it. The public
calendar listed the first run with its `runId` and not the rehearsal. The first zone's card read
*Walk 1 zone · Site event · Grid G12* with **Open event**, which opened the app's event page with
`?run=23` (core's log). The rehearsal's card had no button. For the results half, a new listed
definition opened a zone and a participation tally, then collected and ran
`core.results.publish`. **The stand-ins cannot be counted** (§31.6), so the tally was seeded by
phase 13b's recipe with fixed seconds and one kill, scored `both`. The published table read
*Brannock 17.57, Wren 6.67, Outsider 1.5*: names from the module's meta and fractional scores.
*My events* for walkp1 showed the run with *Rank 1 · Score 17.57*, and its page the same table.
6. **A new map (step 6).** The rig restarted on seed 1235 with the phone on the Map tab. While it was
down, the picture stayed and the line read *The server is offline, so nothing is moving on its
map.* When the live answer named `3000.1235.1`, the app re-read `/map` on each poll until the
module had fetched the new picture. It then drew the new map in place, refitted, within one poll.
The rig is back on seed 1234.
7. **Nothing else moved (step 7).** A site whose module does not declare `map` shows no Map tab, and
`?tab=map` there opens the feed: both are tests. The website change adds one field, which the web
calendar ignores. The app's other screens are untouched apart from one nullable field on the
calendar entry. **A UO core was not walked**: none was running here. The UO paths do not touch the
changed files.
Tests: Android-app **732 unit tests** (72 new or changed in the Rust package), `lintDebug` clean,
debug APK built. website: `eventPublic.test.js` 27/27. The full server suite has two local failures,
both route-manifest checks caused by this machine's git-ignored module installs; a clean worktree
passes them.
### 31.6 What the walk found that the plan did not say
- **The reader's zoom was thrown away twice.** First on the way back from **Open event**: the
canvas's transform lived in `remember`, which leaving the screen discards. The card, held in the
view model, came back over a whole-world view. Then **whenever the status line under the map
changed length**, because the map's box grew or shrank by a line and the canvas treated every new
size as a rotation. The transform is now `rememberSaveable`, and a new size keeps the picture
point at the centre (`MapTransform.resized`, tested). Both were invisible to the unit tests, which
never render.
- **The participation tally cannot be fed by a stand-in.** It counts a player who is connected,
alive and awake (`RunicGateway.cs`, the tally's presence test), and a probe-made sleeper is none of
those. `rgq.online` puts a sleeper in the online list for the players layer, but it is still asleep
and has no connection. The results half of step 5 was walked on a seeded tally, as 13b's step 5
was. A counted tally needs a person, in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md).
- **Off the grid the game writes a negative row, and the frame writes row 0.** The oil rigs are
`I-3` and `A-3` to `MapHelper`, and the frame (the web's and the app's alike) clamps to `I0` and
`A0`. A monument's card uses the game's own string, so no monument is mislabelled. A world event
north of the grid would be labelled as row 0. Nothing was changed: the two maps agree with each
other, and following the game means changing the web's tested function first.
- **The server header read "wiped Dec 31, 1"** for the minute after the rig came up on a new seed,
before the new wipe's time arrived. It is the M14 header formatting a placeholder date. It is
outside this phase and is recorded here only.
- **A marker's card outlives the marker.** When the server goes offline the live layers go and the
card stays up, describing a marker no longer drawn. It is a snapshot of what was tapped. Nothing
was changed.
### 31.7 What is not proven here
- **A real player** on the map (a moving own dot, a mate logging off) and **a counted tally** both
need a person. They are in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md).
- **A UO site**, for want of a running UO core. The change does not reach its screens.
- **Carbon.** As §31.3 said, the app reads the module, and phase 14 proved the module's answer on
both frameworks.
---
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app