- 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
@@ -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
@@ -359,3 +359,22 @@ clan and builds a base. This walk needs two accounts.
**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.
## The map and events on a phone (phase 15, Android leg D)
Added 2026-09-25. The parts of [`PLAN.md`](../modules/rust/PLAN.md) §31.3 that need somebody in the
game (§31.7). The app's gates, polling, event link and results screens were walked on an emulator
with stand-ins and a seeded tally (§31.5). A stand-in is asleep and has no connection, so the game's
tally never counts one (§31.6). Use the two accounts and the clan from the map walk above, with A
signed in to the app and a staff member at the website.
| # | Do this | You should see |
|---|---|---|
| 1 | A opens the server's **Map** tab on the phone, in game with B nearby | A's own dot (cyan) and B's (green), and nobody else. Walk A somewhere; within ten seconds the dot follows. Tap it: *You · Grid* and the square A stands in |
| 2 | A player hacks a locked crate. Tap the crate | The card counts the hack down, refreshed every ten seconds |
| 3 | Staff start a run of a **listed** event whose first phase opens a zone and a participation tally and whose last collects and publishes the results. A stands in the zone for a few minutes and B stays outside | The zone is drawn on the ground. Its card has **Open event**, and it opens that event's page in the app |
| 4 | When the run completes, A opens *My events* | The run, with A's rank and a score in minutes present (with decimals). The event page's results table names A as the game knows A, and does not list B |
| 5 | Put the phone in your pocket for a minute, then open it | The map asked nothing while it was away, and asks at once on return (staff can see this in the site's request log) |
**What counts as a pass:** a player's own dot moves on the phone, a site-event zone opens its event in
the app, and a tally counted from a real person reaches *My events* with its fractional score.
| `DELETE /admin/events/series/:seriesId` | admin, editor | delete it, detaching its definitions; answers with how many |
| `GET /admin/events/calendar` | staff | the calendar for a window: materialised runs and projected occurrences (Phase 4) |
| `GET/PUT /admin/events/actions` | admin | which actions are enabled on this deployment, and their per-run caps (Phase 6). `admin` on the read as well as the write; the PUT takes one action at a time |
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. A run is an INTERVAL, not an instant: an entry is in the window when the run OVERLAPS it, so one that began before the window and has not ended is still "what is on" (Phase 16a — reading the start instant alone made this route serve only the first of its three words, while the event's own page said `live`). Defaults to seven days back through 31 days out — the tail is where "recent" lives — and the window may span at most 92; the anonymous surface is the one with no login in front of it. Projections are forecast from NOW, never into the tail, since a slot the runner has already passed did not happen |
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. A run is an INTERVAL, not an instant: an entry is in the window when the run OVERLAPS it, so one that began before the window and has not ended is still "what is on" (Phase 16a — reading the start instant alone made this route serve only the first of its three words, while the event's own page said `live`). Defaults to seven days back through 31 days out — the tail is where "recent" lives — and the window may span at most 92; the anonymous surface is the one with no login in front of it. Projections are forecast from NOW, never into the tail, since a slot the runner has already passed did not happen. A `run` entry carries its `runId` (Rust D125), the same id the event page publishes on each occurrence and `?run=` takes, so a client holding a run id from elsewhere (a module's map marker) can find its event; a projection has none, because nothing is committed to it |
| `GET /public/events/:slug` | — | **one event** (Phase 14a): storyline, arc, what is live, what is next, what happened recently, and a results table once one is published. Takes an optional `?run=`, which is what an announcement's link carries, so a mail about last Friday's occurrence does not open next Friday's; a run belonging to some other event is **ignored rather than refused**, because a stale link in a months-old mail should land on the event it was about. A draft, an archived definition and an unlisted one all answer 404 |
| `GET /public/events/series/:slug` | — | **the arc** (Phase 14a). A series with no listed events is a 404, not an empty page: the arc is a label on its definitions, so a page for an empty one would publish the fact that an operator has named something they have not announced |
| `GET /player/events/history` | auth | **this account's participation** (Phase 14a) — the run, when it was, the score a module reported, and the rank once results were published (null until then, which is a real state rather than an error). Self-scoped on the session with **no id parameter**, deliberately: a route that took one would be a middleware mistake away from publishing who attended what. Keyset-paged on the participation row's id. It obeys the calendar's two exclusions, so attending an unannounced event does not disclose that it exists |
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.