diff --git a/android/PLAN.md b/android/PLAN.md index 9ce26f1..2ab48ca 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1601,7 +1601,11 @@ push, and Play (M6–M8) follow the designed app. Excluded, as M14 excluded it: the admin Map card, which is operator configuration. -19. **M18 — a Rust server's next wipe** (post-v1; planned 2026-09-25). `module-rust` phase 16. +19. **M18 — a Rust server's next wipe** (post-v1; planned 2026-09-25, **built and walked + 2026-09-25**, `feat/rust-phase-16-next-wipe` into `edge`). `module-rust` phase 16. As built: + `RustServerDto.nextWipe`, `nextWipeWhen()` in `RustFormat.kt`, and a line under the world line on + the list row and in the server header. Walked on the emulator against the phase-16 module, and + against the phase-15 module, which sends no field and gets no line (`modules/rust/PLAN.md` §32.6). **Design of record: [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §32**, D130 and D131. The phase is mostly Discord commands, which are not the app's concern. The app's part is one field. diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index ee7bf1a..7e5146c 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -1008,7 +1008,7 @@ Each phase ends with its findings written down, as every workstream here does. | 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. ✅ **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. 📐 **Plan of record in §32 (D126–D132), 2026-09-25.** Five commands. Any answer narrower than public goes to the caller alone (D127), which is the reverse of the case the criterion names and the one core does not cover. Plus an operator-set **next wipe** (a rule and a one-off date) shown on the web, in the app and by `/wipe` | Module-Rust + Android-app + docs | A refusal does not go public in the channel, **and neither does a permitted answer narrower than public** | +| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral. ✅ **Built and walked 2026-09-25 — plan in §32 (D126–D132), as built and findings in §32.5–32.7 (D133).** Five commands. Any answer narrower than public goes to the caller alone (D127), which is the reverse of the case the criterion names and the one core does not cover. Plus an operator-set **next wipe** (a rule and a one-off date) shown on the web, in the app and by `/wipe` | Module-Rust + Android-app + docs | A refusal does not go public in the channel, **and neither does a permitted answer narrower than public** | | 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 | | 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change, and Pterodactyl is a hosting claim the site can now make); the Integration-kit question R2 raised; and whether the kit owes a reader anything about **supporting two mod frameworks at once** (R19) — a shape it has no chapter for either | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way | @@ -5888,6 +5888,156 @@ Each is taken as written unless review says otherwise. 7. **The next wipe is computed on every read and never stored.** Nothing has to roll it forward after a wipe, and nothing can go stale. +### 32.5 As built + +**One decision the build needed** (org lead, 2026-09-25): + +| # | Decision | +|---|---| +| **D133** | **Build the missing Admin → Rust servers page.** §32.0 and D131 say the schedule "is edited in the existing server form". There is no such form: `/admin/rust` is the Permissions page, a server row has only ever been written through `PUT /admin/rust/servers/:id` (D106 recorded that), and the README's "in Admin → Rust, add a server" described a page that did not exist. The org lead chose a new page at `/admin/rust/servers`: add, edit, test and delete a server, with the D130 schedule in the same form. Rejected: a Wipe schedule card on Admin → Rust visibility (D106's precedent), and an API-only setting. | + +**Module-Rust** ([Module-Rust][mr] `feat/phase-16-commands`, into `edge`): + +- **`server/commands/`**: `common.js` and one file per command, registered as one batch. Real core + accepted all five definitions at `register()`. +- **`model/servers/nextWipe.js`**: `nextWipe(row, now)` and `validateSchedule(body, now)`, pure. + - **The arithmetic** counts local dates as whole days since the epoch and converts to an instant + only at the end. It tries the zone's offset from a day either side and keeps the candidates that + read back. + - **Reading 6 falls out of that.** A skipped hour takes the pre-gap offset and moves forward. A + repeated hour takes the earlier candidate. + - **A row whose rule cannot be computed** (an unknown zone or time) still answers the forced wipe. +- **The six columns** are `ALTER … ADD COLUMN IF NOT EXISTS` on `rust_servers`. + - `wipe_anchor` is read with `DATE_FORMAT(…, '%Y-%m-%d')`, so a `DATE` never passes through a JS + `Date` and a zone. + - `wipe_once_at` is written as a `Date`, which core's `timezone: 'auto'` pool round-trips. +- **`PUT /admin/rust/servers/:id` writes the schedule only when the body has `wipeRule`.** A client + that predates the schedule and posts the rest of the row does not reset it to `none`. The + controller: + - keeps only the fields the rule reads (switching to `forced` clears the day); + - runs `validateSchedule` before anything is written, and answers a 400 with one sentence per + problem in `errors`; + - writes the schedule as saved into the audit line (`rust.server.save`). +- **The shapes.** The public server gains `nextWipe`. The admin server gains `schedule`: what the + operator typed, including a past one-off date. +- **The admin page** (`client/src/routes/admin/ServerSettings.jsx`, nav row *Rust servers*): + - The token field is always blank and write-only. + - An edit sends the stored `protocol` back, because the `PUT` otherwise re-stamps an omitted + protocol with this build's. + - A past one-off date left untouched is dropped on save, rather than sent back and refused. +- **The web** shows *next wipe …* on the server list and in the server page's header, in the reader's + own clock, marked *rescheduled* for a one-off date. +- **`server/commands` joined `ci/bundle.json`**, which `checkBundle` caught on the first test run. This + is the Module-uo v1.0.0 failure the check exists for. + +**Android-app** ([Android-app][aa] `feat/rust-phase-16-next-wipe`, into `edge`): M18 as planned. +`RustServerDto.nextWipe`, `nextWipeWhen()` in `RustFormat.kt`, and a line under the world line on the +list row and in the server header: *Next wipe Sat 3 Oct, 12:00 PM · in 1 week (rescheduled)*. + +**Tests.** +- **Module-Rust server: 368**, including 19 new command tests and 12 for `nextWipe`, the DST edges + among them. +- **Module-Rust client: 55.** +- **Android:** `testDebugUnitTest lintDebug assembleDebug` is green. + +### 32.6 The walk (§32.3), 2026-09-25 + +**What ran:** +- real core `main` (90ba8cc), with the phase-16 module copied into `MODULES_DIR`; +- the Oxide and Carbon rigs' sidecars; +- the bot's real `dynamicCommands`, fed a recording fake interaction. + +**Every step passed.** + +The walk's core was a **second** core (`:3270`, on `rustp16`, a copy of the walk database). The +standing walk core could not be stopped from this session. + +**The callers:** a provider `walk-dc` of kind `discord`, with four identities on the copy — a clan +member, a player outside the clan, a moderator, and a banned moderator. A fifth caller had no +identity. + +1. **Registration.** `GET /internal/commands` lists the five with their options, including `stat`'s + choices. Disabling the module empties the pull, and a dispatch from a bot still holding the old + set is a private *That command is no longer available.* +2. **Public answers** for an unlinked caller. Eight dispatches across the five commands each did + `deferReply({ ephemeral: false })` then `editReply`, and nothing else. They took 5–154 ms, and + none held a Steam id. +3. **Refusals.** Each of these was `deferReply`, `deleteReply`, then `followUp({ ephemeral: true })`: + - an unknown server; + - an ambiguous server (`rust` matches both rigs); + - a missing clan; + - `/top` with no server, on a fleet of two. +4. **D127.** Nobody was on either rig, so `/online` needed staged data. The presence board and the + count were written into the copy, and the commands were dispatched inside one 30 s refresh window. + The count was re-read afterwards to prove the stage held. + - **Presence `staff`:** + - The unlinked caller got *2 online* in public, with no names. + - The moderator got both names, *Walk Bob (sleeping)* included, **privately**. + - The banned moderator resolved as nobody and got the count. + - The signed-in player got the count and no link nudge, because linking cannot reach staff. + - **Presence `signed_in`:** the unlinked caller got the count in public plus a **private** link + nudge. The player got the names privately. + - **Presence `public`:** the names went public. + - **Clan roster** (Northwatch, roster `members`): outsiders got the public facts and no roster. + The member and the moderator got the roster **privately**. With the roster `public`, it went + public. +5. **The next wipe.** The API, `/wipe`, the web and the phone agreed each time: + + | Schedule | Next wipe | + |---|---| + | `forced` | 1 Oct 18:00Z, `forced` | + | `weekly`, Thursday 14:00 `America/Chicago` | 1 Oct 18:00Z, `forced` (the rule's own is 19:00Z) | + | `biweekly`, Monday 20:00 London, anchor 14 Sep | 28 Sep 19:00Z, `rule` | + | a one-off two days out, then 3 Oct | that date, `once`, each time | + + - **Refused, each with its sentence:** a past one-off date, a biweekly anchor on the wrong + weekday, and the zone `Central`. + - **A `PUT` without `wipeRule`** left the stored schedule alone. + - **The web** (browser on Chicago time): *next wipe Mon, Sep 28, 2:00 PM (in 3 days)*, and *Sat, + Oct 3, 12:00 PM (next week) — rescheduled*. + - **The phone** showed the same two, in its own zone. +6. **An offline server.** The Carbon row was pointed at a dead port in the copy, and the rig itself + was not touched. After the next refresh: + - `/status` said *Offline* with *Last seen*, in 8 ms; + - a moderator's `/online` named nobody. +7. **Android**, emulator `s22_ultra`: the list row and the server header show the next wipe. The + standing core runs the phase-15 module and sends no field. Against it, neither shows a line and + nothing breaks. + +**Not walked:** +- **The admin servers page in a browser.** Driving it means typing an admin password into the sign-in + form, which the walk does not do. + - Its `PUT` was walked through the API (step 5). + - Its build and registration are covered by the client suite. + - **This is the org lead's review.** +- **What Discord draws.** The walk proves what would be sent, and to whom (D129). + +### 32.7 Findings + +- **The plan's premise about a server form was wrong** (D133). The plan repeated D106's own finding, + that there is no per-server admin page, in the same sentence that assumed a form. It was caught only + when the code was opened. Reading the page that is to be changed, rather than the API it calls, + would have caught it at plan time. +- **Core's bot drops `notice` when the answer is itself ephemeral** (`dynamicCommands.execute`). A + nudge that belongs on a private answer has to go in its text. Here the nudge only ever rides on a + public count, so it is a `notice`. It is recorded for the next module that wants both. +- **`server/commands` was missing from the bundle list** until `checkBundle` failed. The check did its + job, on the PR that adds the directory. +- **`/wipe` and `/status` write times in UTC, with how far away they are.** + - For example: *Thu 1 Oct, 18:00 UTC (in 6 days)*. + - They never use Discord's `` markup. The envelope is platform-agnostic (§7.1), and a second + platform would print the markup literally. + - The web and the phone use the reader's own clock. +- **A one-off date applies under `none` too.** Readings 3 and 5 pull in opposite directions here: + - reading 5 says a future one-off date *is* the next wipe; + - reading 3 says `none` shows no forecast. + + The build takes an operator who typed a date as having stated a forecast, so the date shows. + Review may say otherwise. +- **Recorded, not fixed (from phase 15, seen again):** the Oxide rig's feed shows a wipe on 25 Sep, + while its header still says *wiped Sep 15*. The header reads the state row's `saveCreatedAt`. This + phase does not touch it. + --- [uo46]: https://gitea.whitlocktech.com/RunicGateway/Module-uo/issues/46