From d914a505234e3708e5275722e676e5c35ec9955d Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 16 Oct 2026 02:43:27 -0500 Subject: [PATCH] =?UTF-8?q?docs(rust):=20console=20commands=20built=20?= =?UTF-8?q?=E2=80=94=20command.run=20(PROTOCOL=20=C2=A720.11,=20PLAN=5FRED?= =?UTF-8?q?ESIGNS=20=C2=A716.10,=20D385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust-link/PROTOCOL.md §20.11: command.run in its three shapes (once with a stored teardown, per online recipient with {steamid}/{name}, teardownOf), the at-most-once key file, the frame budget, command.ran / command.error and their reasons, how unknown-command is told (the game's log line, not Run's return), POST /commands/run, and the website side. - modules/rust/PLAN_REDESIGNS.md: D385 (the bridge keeps a step's teardown with its key, asked during the build), §16.10 the build record (three spike findings, the rig run, twelve settled-in-code points), a D385 walk row in §16.9, pointers from the status paragraph, §9 and §10. - website/EVENTS.md: an action can be text the module cannot read: what core already bounds, and the two things a module copying it needs (a failed step's revert has only the key; `multiple` drops duplicates). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 69 +++++++++++++++++++++-- rust-link/PROTOCOL.md | 100 +++++++++++++++++++++++++++++++++ website/EVENTS.md | 15 +++++ 3 files changed, 180 insertions(+), 4 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 7814744..928dc89 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -11,7 +11,7 @@ chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned [`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D314), and the rest of this plan waits for it.** RunicNPC v1.0.0 shipped on 2026-10-09, and the plan resumed with §2 the same day (D331–D334); §2 was built on 2026-10-12, and §11's Economics spike the same day raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). The Backpacks spike followed and raised D337–D339 (§11.3.1), and Backpacks was built the same day (D340, D341, §11.3.4). The org lead then added permission rewards (§12, -D342–D346), planned and built the same day. On 2026-10-14 RaidableBases was deferred until the current paid copy arrives (D347), with both editions to be supported (D348, §11.4.2). The same day the org lead added Rust's own world events to the base module (§13, D349) and six more first-class optional plugins (§14, D350–D352), to be tested together in one phase before their integration plan. Their questions were answered the same day (D353–D359, the end walk now waiting for RaidableBases), and Steam sign-in stays on the website's plan, linked as §15 (D360). **The §14.2 test phase ran the same day on both rigs (§14.2.1)**: the org lead ruled D361–D362 during it and answered its Q19–Q25 the same day (D363–D369). On 2026-10-16 the org lead added console commands as event steps and rewards (§16, D382–D384), to be built before RaidableBases. **Every item left is built, tested and PR'd first; every walk — +D342–D346), planned and built the same day. On 2026-10-14 RaidableBases was deferred until the current paid copy arrives (D347), with both editions to be supported (D348, §11.4.2). The same day the org lead added Rust's own world events to the base module (§13, D349) and six more first-class optional plugins (§14, D350–D352), to be tested together in one phase before their integration plan. Their questions were answered the same day (D353–D359, the end walk now waiting for RaidableBases), and Steam sign-in stays on the website's plan, linked as §15 (D360). **The §14.2 test phase ran the same day on both rigs (§14.2.1)**: the org lead ruled D361–D362 during it and answered its Q19–Q25 the same day (D363–D369). On 2026-10-16 the org lead added console commands as event steps and rewards (§16, D382–D384), to be built before RaidableBases; **they were built the same day (§16.10)**, with D385. **Every item left is built, tested and PR'd first; every walk — theirs and the ones still owed by §3 and §5 — happens together at the end (D331, §9).** This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this @@ -935,7 +935,8 @@ pulls) is research, not a walk, so it still comes first: 2026-10-16 (D381).** **Stage 9, core's trigger starts, was built on 2026-10-16 (§14.3.21)**, which ends the build. 10. **Console commands as event steps and rewards** (§16, D382–D384), added on 2026-10-16: one stage across - Rust-Plugins, Rust-Link and Module-Rust (§16.8), built before RaidableBases. + Rust-Plugins, Rust-Link and Module-Rust (§16.8), built before RaidableBases. **Built on 2026-10-16 (§16.10)**, + with D385. **Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and §14's rows; the in-game rows @@ -1102,7 +1103,7 @@ The org lead answered §14.3's Q26–Q32 on 2026-10-14: | **D380** | **A guarded crate left standing is noted in the run log when it is placed, not when the run ends:** the *Place a guarded crate* step's success line says the crate is kept when the run ends and how it ends (org lead, 2026-10-15). Core gives a module no hook at a run's end for a step that is kept, so D373's note at the end would need new core work. | A run-end hook in core now (a MODULE_API minor, with the Module-uo proof), or filed for later. | 14.3.5, 14.3.19 | | **D381** | **Server Rewards is taken off the plan** (org lead, 2026-10-16: *"I do not want it"*). Stage 8 is not built. Gone with it: `integrations.serverRewards`, `points.pay` / `balances` / `adjust` and the sidecar's three routes, the tally's `points`, the step `rust.points.pay` and its budget, the `rust_player_points` table, staff add and take, the app's RP balance, and the *Hoarder* title and its column. It amends D350 (five plugins, not six), D369 (its wipe half now covers ZLevels only), D375 and D377 (no *Hoarder*). Nothing had been built. §14.2.1's test findings stay as a record. | Building §14.3.7 as planned. | 14, 14.3.7, 14.3.11 | -Console commands as event steps and rewards, added on 2026-10-16, were decided as D382–D384; they are tabled with their section, in §16.7. +Console commands as event steps and rewards, added on 2026-10-16, were decided as D382–D384, and D385 during the build; they are tabled with their section, in §16.7. ## 11. First-class optional plugins (D199–D202) @@ -3351,7 +3352,8 @@ A reward that runs the admin's commands once for each recipient. goes out once after the end walk (D332). This is how every item since §2 has added to it. - **Request:** `{ key, commands: [..], recipients?: [{ steamId, name }] }`. With no `recipients` the commands run once (§16.2). With `recipients` they run once per online recipient, after the placeholders are filled - (§16.3). + (§16.3). **D385 adds `teardown` to a run, which the bridge keeps with the key, and `{ key, teardownOf }`, + which runs the kept teardown** (§16.10). - **Main thread, spread over frames.** A full reward is at most 100 × 10 commands, so the bridge runs a bounded number per frame rather than all in one, and answers inside the step's 15 s budget (`BUDGET_MS`). - **At most once, by the key.** A command cannot be asked whether it already ran. So the bridge records the @@ -3403,6 +3405,7 @@ Answered by the org lead on 2026-10-16. | **D382** | **An event can run console commands an admin types**: a *Run console commands* step, with optional commands at teardown, and a *Reward by console command* reward run once per recipient. Both are module actions in Module-Rust within the event system as it stands. They are one item, planned here and built as one stage. | Writing a first-class integration for every plugin used in events; a game-agnostic command action in core (ADMIN_CONTROLS §8a admits only declared actions). | 16.2, 16.3 | | **D383** | **A command reward skips recipients who are offline** and names them in the step's detail. It does not hold the commands until they next join, *"for now"*. | A queue in the bridge that runs held commands when the player connects and survives a restart. | 16.3 | | **D384** | **No allowlist for these commands**: the admin types them into a free text box. What bounds them is core's: the step is off until an admin switches it on, only an admin may author it, the switchboard's caps, and the "I am sure" toggle. | An allowlist of command prefixes in the bridge's config, empty by default; an allowlist on the site. | 16.2, 16.4 | +| **D385** | **The bridge keeps a step's teardown with its key** (org lead, 2026-10-16, during the build). A run sends its `teardown` with its commands, and the bridge stores it without running it. A revert that has only the key asks every enabled server to run the teardown it kept under that key, at most once, and only where something may have run. That is the case of a step that failed after a first command ran, or whose answer was lost: core records no ledger row for either, so the plan's ledger copy never reaches them. The ledger row keeps its own copy for every step that succeeded. | Perform runs the teardown itself when a step fails part-way, and a lost answer leaves the row visible for an operator; or no teardown for a failed step, left to the operator. | 16.4, 16.10 | ### 16.8 Where the work lands, and in what order @@ -3432,4 +3435,62 @@ Economics' `deposit` meanwhile): see the online one rewarded, the offline one named as skipped, and a player whose name has spaces and a quote get it right; - retry a step after its answer is lost (stop the sidecar mid-step) and see nothing run twice; +- (D385) give a step a teardown, make its first command work and its second misspell, and see the run abort + and the teardown run once, from the bridge's copy; - **on Carbon**, see a plugin's command run, the confirmation §14.3.14 point 7 asked for. + +### 16.10 Built (2026-10-16) + +Built as §16.8's one stage the day it was planned. The branches are `feat/command-steps` in Rust-Plugins, +Rust-Link and Module-Rust, into `edge`, with this record on `docs/command-steps-built`. The specification is +[PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.11. The walk rows (§16.9) join the end walk (D331). + +**Spiked first on both rigs** (Rust 25902425, Oxide 2.0.7830, Carbon 2.0.262), from a timer, the context the +console probe measures: + +1. **`ConsoleSystem.Run` returns `null` both for a command the server does not know and for one that replies + nothing** (`rg.probe`), so the plan's "the game answers *Command '…' not found*" is not in the return value. + **The game logs that line synchronously inside the call**, as `Command '' not found`, the + same on both frameworks. The bridge listens to Unity's log while each command runs. That line is + `unknown-command`, and anything else heard is the reply when `Run` returns nothing (`oxide.version` replies + only by logging). +2. **`QuoteSafe()` (the game's own quoting) is available to a plugin on both**, and `"na\"me; status"` arrives + as one argument, `na"me; status`. **`;` does not separate console commands**: `x; status` is one command + with two arguments. So `{name}` is filled quoted, and a name cannot run anything else. +3. **Plugin commands run on Carbon** from that context (`consolePlugins=True` on the Carbon rig), so + §14.3.14 point 7's confirmation now waits only for the end walk's real path through the reader thread. + +**Then compiled on both rigs**, and driven through a throwaway plugin calling the bridge's handler (no sidecar). +On both frameworks a repeat key ran nothing, a misspelled second command stopped as `unknown-command` after the +first, offline recipients were skipped and named (a quote in a name intact), malformed requests were refused, +and D385's teardown ran once for a run that had run something, and not at all for one that had run nothing or +was unknown. Both rigs were put back on the protocol-13 bridge. + +**D385 was asked during the build.** Core records nothing from a failed step and reverts its placeholder with +only the key (EVENTS.md §D), so the ledger copy the plan relied on never reaches a step that failed part-way. +§16.7 has the decision. + +**Settled in code** (each can be overruled): + +1. **A retry while the first run is still going joins it.** The sidecar's reply timeout (10 s) is shorter than + the step's budget (15 s), so a full reward can be retried while it runs. The retry is answered when the run + ends, with the same answer. +2. **A run that a restart or a reload cut off is `interrupted`**: permanent, nothing run again, and its teardown + (D385) still runs. +3. **Per frame:** whole recipients, until 30 commands or 8 ms. A run without recipients (at most 10) is one + frame. +4. **Commands run not quiet**, so the server console shows each one and its answer as if typed. +5. **The answer keeps the first 20 replies** (each trimmed to 200 characters) and the first 50 skipped names + with a count. The key file keeps at most 500 keys, oldest forgotten first, besides the plan's seven days. +6. **A teardown takes no placeholders, and a reward takes no teardown** (refused `malformed`). +7. **The name filled in is the player's in-game name** as the server has it at that moment, not the site's copy. + The site's name is used only to name someone skipped. +8. **`{steamid}` and `{name}` ignore case.** Any other `{…}` is left as typed, since a command may need braces. +9. **Nobody qualifying sends nothing** (the step says *"nobody qualified, so no command ran"*). +10. **`multiple` drops duplicates** (MODULE_API 1.14.0), so a command listed twice runs once. A step that wants + `airdrop` twice uses two steps, or *Call a world event*. +11. **Permanent failures:** `malformed`, `unknown-command`, `interrupted`, `failed`, `events-disabled`. The + error says how many commands ran before it. A game that is down or a world still loading retries on core's + ordinary delay. +12. **A teardown's `unknown-command` fails the revert** and leaves the row visible: core's cleanup retries + replay the key's first answer, so nothing runs twice. diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index cd076e6..cbd4a1d 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -3400,3 +3400,103 @@ kit reward (§20.2), and it takes effect at once. **The website (Module-Rust).** Server settings' plugin line says whether ZLevels is loaded and, where it is, that an event may lend its XP boosts as permissions. Nothing else reads the field. + +### 20.11 Console commands — `command.run` (PLAN_REDESIGNS §16, D382–D385) + +An event can run **console commands an admin typed**: once, as a step, with teardown commands for when the run +ends, or once for each recipient of a reward who is online. This lets a plugin that has no support of its own +here be used from the scheduler, for example RaidableBases' `rbe` or `sr add {steamid} 100`. **There is no +allowlist (D384).** The site's switchboard is the switch, and here `EventsEnabled` is the server's, as for every +world verb (§15). The design is [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §16, and what the +build found is §16.10 there. + +**`command.run`**, in three shapes: + +```json +{"cmd":"command.run","reqId":"r-64","key":"<40>","commands":["rbe 1","say a raid base is up"],"teardown":["rbe stop"]} +{"cmd":"command.run","reqId":"r-65","key":"<40>","commands":["sr add {steamid} 100"], + "recipients":[{"steamId":"76561198000000001","name":"Alice"}]} +{"cmd":"command.run","reqId":"r-66","key":"<40>:teardown","teardownOf":"<40>"} +``` + +- **Without `recipients`** the commands run once, in order. **`teardown` is stored with the key and not run** + (D385). +- **With `recipients`** (at most 100) the commands run once per recipient who is **connected at that moment**. + The bridge fills **`{steamid}`** with the Steam id and **`{name}`** with the player's in-game name, **as one + quoted argument**, quoted the way the game's own `BuildCommand` quotes one: `"` becomes `\"`, trailing + backslashes are dropped, and control characters are removed first. A name with spaces, quotes or `;` stays + inside it (`;` does not separate console commands either). Both placeholders ignore case. Anyone offline is + **skipped, never held** (D383). A reward carries no `teardown`. +- **`teardownOf`** runs the teardown stored under that key, under its own `key`, **when anything there may have + run**: some of its commands ran, or a reload cut the run off. Otherwise it is answered with nothing run and + nothing recorded, which is what a server the step never reached says. +- At most **10** commands per field, each one line of at most **512** characters. Placeholders outside a + reward are refused. + +**At most once, by the key.** A command cannot be asked whether it already ran, so the key is written to +`data/RunicGateway/commands-run.json` **before anything runs** (at most 500 keys, kept seven days): + +- a repeat of a finished key is answered from its record, `repeat: true`, and runs nothing; +- a repeat **while the first is still running** waits for it and gets the same answer (the sidecar's 10 s + timeout is shorter than a step's 15 s budget, so a long reward can be retried while it runs); +- a key whose record a restart or a reload left running is answered `interrupted`: some of it may have run, + and none is run again. + +**Main thread, spread over frames.** A recipient's commands run together in one frame, and a frame takes no +more recipients past 30 commands or 8 ms. A run without recipients (at most 10 commands) is one frame. Each +command runs through `ConsoleSystem.Run` **not quiet**, so the server console shows it and its answer as if +typed. + +**The answer:** + +```json +{"kind":"command.ran","type":"reply","reqId":"r-65","key":"<40>","ran":1,"rewarded":1,"repeat":false, + "replies":[{"command":"sr add 76561198000000001 100","steamId":"76561198000000001","reply":"..."}], + "skipped":["Bob"],"skippedCount":1} +``` + +`ran` counts the commands run, and `rewarded` the recipients they ran for. **`replies` holds the first 20 +commands as run** (placeholders filled), each with what the console answered, trimmed to 200 characters: +`Run`'s return, or, when it returns nothing, what the game logged while the command ran. **`skipped` names +the first 50 skipped as offline**, and `skippedCount` counts them all. Refusals are +`{"kind":"command.error","reason":…,"message":…}`, carrying the same `key`, `ran`, `replies` and `skipped` +when anything ran first: + +| `reason` | When | +|---|---| +| `malformed` | No key or one over 128 characters; no commands; more than 10, one over 512 characters or with a line break; a recipient that is not a Steam id, or more than 100; `{steamid}`/`{name}` with nobody to fill them; a reward with a `teardown` | +| `unknown-command` | **The server does not know a command.** The run stops there, before the command runs for anyone else. The message adds that a plugin's command is not found on a framework where `console.plugins` is false (§20.5, D363) | +| `interrupted` | A restart or a reload cut this key's run off (above) | +| `busy` | A `teardownOf` for a run that is still going: ask again | +| `failed` | The run threw, in its words | + +`events-disabled` and the world's `not-ready` come first, as `world.error`, as for every world verb (§15). + +**How `unknown-command` is told.** `ConsoleSystem.Run` returns `null` both for a command the server does not +know and for one that replies nothing, so the return value cannot tell them apart. The game logs +*`Command '' not found`* **synchronously, inside the call**, on Oxide and on Carbon alike (spike, +2026-10-16, Oxide 2.0.7830, Carbon 2.0.262, Rust 25902425). So the bridge listens to Unity's log +(`Application.logMessageReceived`) while each command runs. That line, for this command, is +`unknown-command`; anything else it hears is the reply when `Run` returns nothing. + +**It knows only that a command ran and what the console said.** A plugin's reply is its own text, so "it ran" +is not "it worked". Nothing that follows reaches the feed, the map, a title or a trigger. + +`rg.world` prints `commandKeys=` (keys remembered) and `commandsRunning=`. + +**The sidecar** adds `POST /commands/run`, a thin forward that keeps nothing. + +**The website (Module-Rust).** Two steps, budget `rust.commands`, both `irreversible`, both with a required +*"I am sure"* toggle; the commands are core's `multiple` string field, a free text box: + +- **`rust.command.run`**, *Run console commands* (`reversible: 'ledger'`): `server`, `commands`, optional + `teardown`. It sends `{ key, commands, teardown }`. Its ledger row (`command-teardown`) carries **its own + copy** of the teardown, and core's revert sends it as `{ key: :teardown, commands }`. **A step that failed + or whose answer was lost has no row** (core records nothing then), so its revert has only the key. It sends + `{ key: :teardown, teardownOf: }` to every enabled server (D385). Priced one unit per command, + teardown included. +- **`rust.command.reward`**, *Reward by console command* (`reversible: 'none'`): the kit reward's recipient + modes, `linked` among them. It sends Steam ids and names, so no website account is needed, and never fills a + placeholder itself. Priced commands × people, the 100-recipient bound for a mode that is not a count. +- `unknown-command`, `malformed`, `interrupted`, `failed` and `events-disabled` are permanent. The error says + how many commands ran before it. A game that is down retries on core's ordinary delay. diff --git a/website/EVENTS.md b/website/EVENTS.md index 2849a17..d09bcf9 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1164,6 +1164,21 @@ exactly the reason the rest of this document is careful about. It is deleted. outcome against a participant and answer "who received what, from which step, in which run". One optional field on a shape that already exists — not a registry. +**An action can be text the module cannot read.** Module-Rust's *Run console commands* and *Reward by +console command* (`rust.command.run`, `rust.command.reward`; Rust PLAN_REDESIGNS §16, D382–D385) run console +commands an admin typed into a `multiple` string param. That is a free text box with no allowlist, and it +needs nothing new from core. The module cannot know what a command does, so **it rates both `irreversible`**. +Everything core already does for that class bounds them: they arrive off in the switchboard, only an admin +authors them, the per-run caps apply, and a failure aborts the run. A required `confirm` param is the author's +own "I am sure". The command step keeps **teardown commands on its ledger row**, and core's revert at the end of +the run runs them. Two details any module copying this needs: + +- **A step that failed, or whose answer was lost, has no ledger row.** Core records nothing from a failure, so + `revert()` gets only the idempotency key. A teardown that must run then, after a first command started + something and a second was misspelled, has to be findable by that key. Rust keeps it on the game side with + the key (D385). +- **`multiple` drops duplicates.** A command listed twice runs once. + ### Param option sources A `source` names a module-served option endpoint, so an authoring field is a dropdown of *real*