From 941e4ae6e54912c06de46bd3eed7ffb4b5790911 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 6 Oct 2026 08:23:21 -0500 Subject: [PATCH] docs(runicnpc): stage 8 built and walked; MODULE_API 1.12.0 progress(), map.live's RunicNPC rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - website/MODULE_API.md: 1.12.0, an event action's optional progress(): its envelope, its two answer shapes, and the rule that it answers for one reader and never fails loudly. - website/EVENTS.md §I: a live occurrence's progress lines. - rust-link/PROTOCOL.md §17.3: map.live's RunicNPC rows (name, key, boss, health, add) and the world layer's boss. - runicnpc/PLAN.md: stage 8's design renamed live -> progress, and its "Built" section, with the branches, the tests and the walk on both rigs in the browser and on the emulator. Also recorded in the plan: a core finding outside this stage. The public page never shows a live run's phase label, because phaseLabel looks phases up by id while specs key them by key. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- runicnpc/PLAN.md | 53 +++++++++++++++++++++++++++++++++++++------ rust-link/PROTOCOL.md | 16 +++++++++++-- website/EVENTS.md | 17 ++++++++++++++ website/MODULE_API.md | 37 ++++++++++++++++++++++++++++-- 4 files changed, 112 insertions(+), 11 deletions(-) diff --git a/runicnpc/PLAN.md b/runicnpc/PLAN.md index 6031c29..639fe6d 100644 --- a/runicnpc/PLAN.md +++ b/runicnpc/PLAN.md @@ -1489,14 +1489,14 @@ in the Android app. **The design (D305).** -- **Core: `MODULE_API` 1.12.0, one optional function on an event action, `live`.** - - While a run is live, core calls `live` for each step whose action declares one, with the step's ledgered refs and - the viewer (signed in or not, staff or not). The answer is `null`, `{ label, left, of }` or `{ label, percent }`. - - Core puts the answers in the public run as `live`, in step order, and renders them as text it does not +- **Core: `MODULE_API` 1.12.0, one optional function on an event action, `progress`.** + - While a run is live, core calls `progress` for each step whose action declares one, with the step's ledgered refs and + the reader as `{ userId }` (the module re-reads the account, as its own pages do). The answer is `null`, `{ label, left, of }` or `{ label, percent }`. + - Core puts the answers in the public run as `progress`, in step order, and renders them as text it does not interpret: "Bandits: 3 of 8 left", "The Juggernaut: 62%". The app does the same from the same field. - A call has a deadline and never throws into the page: a module that is slow or down gives no line, not an error. The answers are cached per run for a few seconds, so a busy page does not reach the game once per reader. - - **This is an addition, so it is a minor bump.** Module-uo declares `^1.10.0` and registers no action with `live`, + - **This is an addition, so it is a minor bump.** Module-uo declares `^1.10.0` and registers no action with `progress`, so its pages and actions are unchanged. The integration kit's pin (an equality check) goes red on the bump by design, until the kit is read again and the pin moved. - **The bridge (protocol 13, still unreleased): `map.live`'s rows for RunicNPC.** @@ -1508,15 +1508,54 @@ in the Android app. there, and the site's map drops it (D303). - **Module-Rust.** - The map: a marker's tooltip is its name and its event's title; a boss is bigger (D303). - - `rust.npc.place`'s `live`: the step's NPCs still standing, of the count it placed, labelled with the profile's + - `rust.npc.place`'s `progress`: the step's NPCs still standing, of the count it placed, labelled with the profile's name; a boss step answers its health instead (D304). - Each follows the map's events layer visibility, so a page never tells a viewer what the map would hide. -- **The app.** The map's markers as the site's. The event screen renders the run's `live` lines. +- **The app.** The map's markers as the site's. The event screen renders the run's `progress` lines. - **Docs.** - `website/MODULE_API.md` 1.12.0 and `website/EVENTS.md` §I. - `rust-link/PROTOCOL.md` §17.3. - The integration kit, read again and its pin moved. +**Built (2026-10-06), five branches and this one.** + +| Piece | Branch | What it does | +|---|---|---| +| Core | `website` `feat/events-action-progress` (website#210) | MODULE_API 1.12.0: an action's optional `progress()`, asked per step of a live run with a 2 s deadline and a 5 s cache per run and reader; the public live occurrence's `progress`; the event page renders it and re-reads it every 15 s while live. Named `progress` rather than `live`: the public occurrence already has a boolean `live` | +| The bridge | `Rust-Plugins` `feat/runicnpc-stage8` | `map.live`'s RunicNPC rows (§17.3): `name`, `key`, `boss` and `health`; a boss's adds with `add: true`; a boss outside any event in the world layer | +| The site | `Module-Rust` `feat/runicnpc-stage8` | `rust.npc.place`'s `progress` (D304), behind the map's events-layer visibility; the map's names, event titles (from core's public calendar), bigger bosses and adds (D303); a boss's health removed from what a viewer is sent | +| The app | `Android-app` `feat/runicnpc-stage8` | The same markers and card (the event's title from the run resolver's calendar read); the event screen's progress lines, re-read every 15 s while live | +| Docs | this branch | `website/MODULE_API.md` 1.12.0, `website/EVENTS.md` §I, `rust-link/PROTOCOL.md` §17.3 | + +**Tested.** + +- **Core:** 2077 server tests and 401 client tests pass. The route-manifest checks fail on this machine's `main` too, because modules are installed locally; with no modules, as in CI, they pass. +- **Module-Rust:** 503 server and 73 client tests, and every `check:*` script. +- **The app:** 743 unit tests, `lintDebug` and `assembleDebug`. +- **Builds:** checked against the latest first: Rust 2634.289.1 (buildid 25681086), Oxide 2.0.7801, Carbon 2.0.262. + +**Walked, 2026-10-06.** In the browser, signed in as the walk admin, and on the emulator (`s22_ultra`). A probe plugin +stood in for a player. + +| Row | Result | +|---|---| +| An event authored in the editor (3 bandits; the Walk Juggernaut) on Oxide: its public page said "Bandit: 3 of 3 left" and "Walk Juggernaut: 100%" | pass | +| A bandit killed and the boss hurt: the open page moved to "2 of 3 left" and "59%" without a reload | pass | +| The boss below 50% summoned two adds: the bandit line still counted only the step's own, and the map listed the adds with `add: true` | pass | +| The web map: "Bandit · Stage 8 walk — the harbour raid", the boss bigger and in its own colour as "Walk Juggernaut (boss) · …", each add as "(a boss's add)"; the core calendar read once | pass | +| A boss placed from the placements page outside any event: in the world layer as `kind: boss` with its name; the placement's ordinary bandits not on the map (D302) | pass | +| The map's events layer set to staff only: a signed-out reader's page had no lines and its map no events layer; the admin's still had both. Set back to everyone | pass | +| Carbon: 2 guards and the Carbon Juggernaut: "Carbon Guard: 2 of 2 left" → "1 of 2 left", "100%" → "69%" | pass | +| The app: the event screen's two lines, moving to "1 of 3 left" while open; the map's bigger bosses; a world boss's card "Walk Juggernaut (boss)"; an add's card naming its event with Open event | pass | + +**Found while walking, not in this stage:** the public event page never shows a live run's phase label. Core's +`phaseLabel` looks phases up by `id`, but a spec's phases carry `key` (`events/spec.js`), so the page says "Under way" +for every event the editor authors. It has done so since Events Phase 14a; its test used an `id` fixture. A one-line +fix in `eventPublic.model.js`, raised with the org lead rather than folded into website#210. + +**Still to do when website#210 merges:** the integration kit's `ci/core-ref.json` pin moves to that sha, and its +`coreApi` with it, after the kit is read again for 1.12.0. + ### Stage 9 — Hardening and release - **Performance to the budget stage 1 measured:** 100 NPCs on a 6000 map without the frame time crossing it. diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index cb6b7f3..132f23b 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -1805,8 +1805,8 @@ drawing a second time. A new map key makes the file stale, and it is ignored. | 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` | +| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true`; a `boss` (protocol 13, RunicNPC D302) is one of RunicNPC's bosses placed outside any event, with its `name` | +| `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` and `key` (the step that placed it). Protocol 13 adds, for one of RunicNPC's NPCs, its `name`, and for a boss `boss: true` and `health` (0–1); and a boss's **adds**, which no step placed, under their boss's run with `add: true` | | `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 | @@ -1822,6 +1822,18 @@ 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. +**RunicNPC's rows (protocol 13, runicnpc stage 8).** One `RunicNpc_List` per ask is joined to the +ledger by net id: + +- **A ledgered NPC** gains its `name` and, for a boss, `boss` and `health`. +- **A RunicNPC NPC owned by a run** (`run:`) that no step ledgered is a boss's add. It joins + `events` with `add: true`. +- **Any other boss** joins `world` as `kind: "boss"`. +- **A placement's ordinary NPCs are not sent** (D302). + +`health` is for the event page's progress line. The site's map does not show it (D303) and removes +it before a viewer is sent the layer. + ### 17.4 Two bounds, and the console Two config keys are written into an existing config the first time protocol 11 loads: **`MapMaxSleepers`** diff --git a/website/EVENTS.md b/website/EVENTS.md index 61bb5d1..ac41417 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1472,6 +1472,23 @@ true about the server and nothing about the event. `paused` is the same argument direction: an operator holding a run for two minutes is not a state a public page should render, and one that said "paused" would invite a question whose answer is internal. +**A live occurrence says how it stands, in its modules' words (MODULE_API 1.12.0).** The spec stays +unpublished, but a visitor watching an invasion wants to know it is going somewhere. An action may +declare an optional `progress()`, and while a run is live core asks each such step. The live +occurrence carries the answers as `progress`, in step order. + +- **The shapes.** Each answer is a `label` with either `left` of `of` ("Bandits: 3 of 8 left") or a + `percent` ("The Juggernaut: 62%"). +- **Core interprets neither.** The page and the app render the text and nothing else; a game's words + never enter core (RunicNPC D305). +- **The reader.** The route takes an optional session, and `progress()` is told the reader's account + id. The module applies its own visibility settings: Rust's follows its map's events layer. +- **Failure.** A module that is slow (over 2 s), throws or answers any other shape gives no line, + never an error on the page. +- **Caching.** Answers are cached per run and reader for 5 s. The page re-reads every 15 s while the + event is live. +- **Only the live occurrence.** A finished run has results, and a scheduled one has nothing to count. + **The six public triggers gained `eventUrl` here, which is the version bump this file promised.** Until Phase 14a there was no page, so they declared no url at all — `news.post` had already paid for that mistake once, previewing a link in the template editor that was dead in every mail it sent. The diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 3f1514e..40a5e15 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -26,13 +26,37 @@ here extends the contract first, in this file, before the module is written agai Core exports a single integer-major semver string from `server/src/modules/version.js`: ```js -const MODULE_API_VERSION = '1.11.0' +const MODULE_API_VERSION = '1.12.0' ``` The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first module chunk evaluates, which is earlier than any network round trip could answer. +**1.12.0 — an event action's optional `progress(envelope)`** (`website/EVENTS.md` §I; RunicNPC +`runicnpc/PLAN.md` stage 8, D304, D305). One addition and no removal, so minor. + +- **What core does with it.** While a run is live, core asks each step whose action declares + `progress` how it stands, and publishes the answers on the public event as the live occurrence's + `progress`. +- **The envelope.** It carries the step's `runId`, `stepId`, `idempotencyKey`, `scope` and `params`. + It also carries `resources`, the step's own ledger rows that are still held, as `{ kind, ref, + payload }`. And it carries `viewer: { userId }`, the reader's account id or null, so the module + applies its own visibility settings. +- **The answer is one of two shapes, or `null`:** `{ label, left, of }` or `{ label, percent }`. + - `label` is at most 80 characters. + - `left` and `of` are integers, with `0 ≤ left ≤ of` and `of ≥ 1`. + - `percent` is 0 to 100, rounded. +- **Core interprets neither.** The page and the app render "label: left of of left" and + "label: percent%". Core never learns what a bandit is (D305). +- **Every failure is a line not shown, never an error.** A throw, an answer later than 2 s, or any + other shape is dropped and logged. Answers are cached per run and reader for 5 s, so a busy page + does not reach a game once per reader. +- **Module-uo is unaffected.** It declares `^1.10.0` and registers no action with it. +- **A module may declare it while still declaring an older `coreApi`.** An older core ignores the + unknown member, because the action shape is copied field by field, so the action still works with + no progress line. + **1.11.0 — `ctx.events.expired({ kind, ref })`** (`website/EVENTS.md` §L; Rust `modules/rust/PLAN_FIXES.md` F14, D183). One addition and no removal, so minor. A game that ends something at its own deadline — a Rust zone the plugin erases when its time is up — tells core, and core @@ -367,6 +391,10 @@ branches lacks — so it is the only place `permits` is true between two values between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise unchanged, and so is every rule about composition narrowing rather than widening. +**1.12.0 — an action's `progress()`** (`website/EVENTS.md` §I, RunicNPC D304). One optional member +on an action, so minor; see §1.1. Core asks it while a run is live and publishes the answer, one of +two shapes, on the public event. Module-uo's `^1.10.0` still resolves. + **1.11.0 — `ctx.events.expired`** (`website/EVENTS.md` §L, Rust PLAN_FIXES D183). One addition, so minor; see §1.1. The resource ledger gains an `expired` status, and nothing that existed changes meaning — `expired` joins neither the statuses that hold a target nor the ones that leave a run's @@ -778,7 +806,7 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) // api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0 api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0 api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0 -api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile }]) // 1.10.0 +api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0 api.registerEventBudgets([{ id, label, unit }]) // 1.10.0 api.registerEventLeases([{ id, label, type, min, max, values, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0 api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0 @@ -1183,6 +1211,11 @@ rather than implementation and belong here: actually happened"* for a verb whose answer is neither a resource nor a participant, and before Phase 15 there was no such answer: `EVENTS.md` §H named the member, `classify()` had never read one, and a module that used it wrote into nothing. +- **`progress()` is read by anybody, so it answers for one reader and never fails loudly** (1.12.0). + The envelope names the reader (`viewer.userId`, null for a stranger). A module whose own settings + would hide what the line describes answers `null` for that reader; a public page must never say + what the module's own pages would hide. Core gives it 2 s and drops a late answer. It is a read: it + must change nothing, and it may be called every few seconds for as long as a run is live. - **A module reports who took part on the envelope, and there is no other door.** `participants` rides back from `perform()` exactly as `resources` does, on both success shapes — including `await: 'human'`, because a cue's confirm finishes the step without a second dispatch and that is