From 42a0482a9e6361208d8ace770ed82d2888fece72 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 25 Sep 2026 12:30:49 -0500 Subject: [PATCH] =?UTF-8?q?docs(rust):=20phase=2015=20as=20built=20?= =?UTF-8?q?=E2=80=94=20the=20map=20and=20events=20on=20a=20phone=20(PLAN.m?= =?UTF-8?q?d=20=C2=A731.5=E2=80=93=C2=A731.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- android/PLAN.md | 4 +- modules/rust/PLAN.md | 121 ++++++++++++++++++++++++++++++++++++++- rust-link/PLAYER_WALK.md | 19 ++++++ website/EVENTS.md | 2 +- 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index 91dda12..b860761 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1582,7 +1582,9 @@ push, and Play (M6–M8) follow the designed app. 18. **M17 — the Rust map and events on a phone** (post-v1; planned 2026-09-25). `module-rust` phase 15, R10's leg D. **Design of record: [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §31**, with its five decisions - (D121–D125). + (D121–D125). ✅ **Built and walked on an emulator 2026-09-25** (§31.5), all seven steps. The + walk found the reader's zoom lost on the way back from an event page and whenever the status + line changed length, both fixed (§31.6). - **The map is a fifth tab on the Rust server screen**, between Online and Wipes, shown when the module declares `map`. It is drawn natively: Coil loads the picture and a Compose `Canvas` diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index adc7c4d..7508d73 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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 diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index 556f5fa..c7279bc 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -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. diff --git a/website/EVENTS.md b/website/EVENTS.md index 6a44a9f..ec75136 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1847,7 +1847,7 @@ no URL moved. | `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 |