docs(rust): console commands built — command.run (PROTOCOL §20.11, PLAN_REDESIGNS §16.10, D385)

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-16 02:43:27 -05:00
parent 7eac16bfb2
commit d914a50523
3 changed files with 180 additions and 4 deletions

View File

@@ -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 '<line>' 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: <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: <key>:teardown, teardownOf: <key> }` 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.