Merge pull request 'docs(rust): protocol 14 — Economics, as built (PROTOCOL §20.3, PLAN_REDESIGNS §11.2.4)' (#339) from docs/rust-protocol-14-economics into main

Reviewed-on: #339
This commit is contained in:
2026-10-12 12:40:03 +00:00
3 changed files with 130 additions and 3 deletions

View File

@@ -1455,7 +1455,7 @@ push, and Play (M6–M8) follow the designed app.
| Screen | Route | Reads |
| --- | --- | --- |
| **My Rust account** | `player/rust` | `GET /player/rust/links`, `POST /player/rust/link`, `DELETE …/links/{steamId}`, `GET /player/rust/permissions` |
| **My Rust account** | `player/rust` | `GET /player/rust/links`, `POST /player/rust/link`, `DELETE …/links/{steamId}`, `GET /player/rust/permissions`, `GET /player/rust/economics` (the Coins section, 2026-10-12, [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §11.2: one card per server, "last known" when a server did not answer, the reason where it has no Economics, and left out on a 404 from an older module) |
#### The gate is `rust`, and `PLAYER` means staff too

View File

@@ -10,7 +10,7 @@ rows still to come (§3.4); §4, built 2026-09-29 (§4.1). **§6 was spiked on 2
chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned in
[`../../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). **Every item left is built, tested and PR'd first; every walk —
raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). **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
@@ -1173,6 +1173,27 @@ On both rigs, with Economics installed:
- run `ecowipe`, and see the column clear;
- read a balance on the account page and in the app.
#### 11.2.4 Built (2026-10-12)
Built to §11.2.2 on protocol 14, and specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.3. The
bridge compiled on both rigs (Rust 25877464; Oxide 2.0.7826, Carbon 2.0.262), which were then given back their
protocol 13 bridge. Six points the design left open were settled in the code, and are named here so they can
be overruled:
- **`economics.wiped` carries `newSave`.** After `ecowipe` the site clears the wipe's balances; at a new save it
clears nothing, because the new map's balances arrive as players connect, and the last wipe's stay as the
record of how it ended.
- **A balance frame with no `name` is not play.** The plugin names only a connected player, and the site writes
such a frame's balance without moving anybody's last-seen.
- **The Coins column is per wipe.** A balance is a value, so there is no all-time one: the column and
`sort=balance` exist only when one wipe of a server that reports Economics is shown.
- **A payment's `failed` reasons** are `at-limit`, `refused`, `unmeasured` (deposited, but not measured — remembered
with the key so a retry pays nothing more) and `bad-id`.
- **Payments and adjustments keep their keys apart**, so a step's key can never be answered with a staff
adjustment's reply. The admin form sends a fresh key with each submission.
- **The step spends a new `rust.coins` budget**, priced like the kit reward: the amount times the count, or
times the recipient bound for a mode that is not a count.
### 11.3 Backpacks (D201)
- **Read, never write (R2).** A new verb, `backpacks.read {steamId}`, returns

View File

@@ -2641,7 +2641,7 @@ loaded.
---
## 20. Protocol 14 — the step editor
## 20. Protocol 14 — the step editor and the optional plugins
Opened by [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §2 after protocol 13 shipped with RunicNPC
v1.0.0 (D332). It is built on `edge` in Rust-Plugins, Rust-Link and Module-Rust and released once, after the
@@ -2701,3 +2701,109 @@ optional `message` and `delivery`; the site writes them on each grant row, and e
- **Absent is not empty**, as for `credits`: a sync with no `notices` field leaves the plugin's set alone. The
site sends the field on every sync from protocol 14, empty when there is nothing to say. The notices are part
of the sync's digest, so a new one is pushed at once.
### 20.3 Economics — coins, balances, staff adjust (§11.2, D199, D335, D336)
Wulf's **Economics** (3.9.2, uMod), optional like BetterChat and PopupNotifications (D199): every verb below
answers `economics-missing` without it, and nothing else in the bridge needs it. `requires_plugins` keeps Kits
and ZoneManager only. The design is [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §11.2.2; what the
spike found, which shapes every call, is §11.2.1 there.
**How the plugin calls it.** Economics' API is private methods called by name, each with a `string` and an
`object` overload. The bridge sends **only ids it has parsed as Steam ids** (17 digits, at or above the
individual base), because Economics makes an account of any string it is given, and **boxes every amount as
a `double`**: an `int` matches no overload, and `Call` answers null having done nothing. What a deposit paid is
**the balance after less the balance before**: `Deposit` answers `true` while rounding to two places and
clamping to Economics' balance limit.
**Hello and status.** `integrations` gains `economics`:
```json
"economics": { "loaded": true, "version": "3.9.2", "startingBalance": 1000, "wipesWithMap": false }
```
`startingBalance` and `wipesWithMap` come from `Economics.json` in the framework's config directory
(`"Starting account balance (0 or higher)"`, `"Wipe balances on new save file"`), read again only when the file
changes. **Each is absent when the file cannot say**; the site then assumes Economics' default of 1000 and the
servers page says so. Hello is sent again when Economics loads or unloads.
**`economics.pay` — coins from an event step, kept once paid (D335).**
```json
{"cmd":"economics.pay","reqId":"r-31","key":"7:4","amount":500,"steamIds":["76561190000000001"],
"message":"You earned {amount} coins","delivery":"popup"}
{"kind":"economics.paid","type":"reply","reqId":"r-31","key":"7:4","repeat":false,"amount":500,
"paid":[{"steamId":"76561190000000001","before":1000,"after":1500}],"failed":[]}
```
- **Main thread, one call for the list.** For each id: read the balance, deposit, read it again. `paid` holds
each player's before and after. `failed` lists the rest by reason: `at-limit` (the deposit moved nothing —
the balance is at Economics' limit), `refused` (`Deposit` answered false), `unmeasured` (deposited, but a
balance read failed — not told an amount, and still remembered with the key), `bad-id` (not a Steam id).
- **The key is remembered with its reply**, in `data/RunicGateway/payments.json`, for seven days (the tally
ceiling, D108). A key already paid is answered from the first reply with `repeat: true`, and pays nobody. A
refusal (`economics.error`) is not remembered: nothing moved.
- **The message** (optional; blank tells nobody) is said once to each player paid, with `{amount}` replaced by
**what that player received**: at once if they are connected and awake, otherwise on `OnPlayerSleepEnded`,
the next time they wake on this server — §20.2's rule. Who has been told is kept in `payments.json` beside
the key. A popup where `delivery` is `popup` and PopupNotifications is loaded, chat otherwise; cut at the
chat bound.
- **Bounds:** at most `EventsMaxRecipients` ids; an amount from 0.01 to 1,000,000. Behind `EventsEnabled`, as
every event verb is.
- Refusals are `economics.error`: `economics-missing`, `events-disabled`, `malformed` (no key), `bad-amount`,
`too-large`.
**`economics.balances` — what some players hold.**
```json
{"cmd":"economics.balances","reqId":"r-32","steamIds":["76561190000000001"]}
{"kind":"economics.balances","type":"reply","reqId":"r-32","balances":[{"steamId":"76561190000000001","balance":1500}],"startingBalance":1000}
```
Bounded by the sync's own bound (`PermMaxOps`), like `kits.uses`; ids that are not Steam ids are left out.
**A balance at the starting value may belong to an account Economics has never seen**: it answers the
starting balance for one and stores nothing (§11.2.1). `startingBalance` is absent where the config cannot be
read. Refusals: `economics-missing`, `too-large`.
**`economics.adjust` — staff deposit, withdraw or set one balance.**
```json
{"cmd":"economics.adjust","reqId":"r-33","key":"adj-3f2a9c1e-…","steamId":"76561190000000001","op":"withdraw","amount":500}
{"kind":"economics.adjusted","type":"reply","reqId":"r-33","key":"adj-3f2a9c1e-…","repeat":false,"op":"withdraw","amount":500,
"steamId":"76561190000000001","before":1500,"after":1000}
```
- `op` is `deposit`, `withdraw` or `set`; an amount from 0.01 to 1,000,000, and from 0 for `set`. Not behind
`EventsEnabled`: it is staff, not an event.
- **A withdrawal above the balance is refused** with `insufficient` and the `balance`, because Economics takes
all or nothing. A deposit that moved nothing is `at-limit`, with the balance.
- The key is remembered seven days like a payment's, in its own namespace, so a resent form answers `repeat`
and moves nothing. Other refusals: `economics-missing`, `malformed`, `bad-amount`, `refused`.
**The tally.** `player.tally` gains **`balance`: the current balance, a value and not a change**, like the
`best*` fields (§19.10). It is set when a player connects (so a balance carried over from last wipe is known at
once, D336), when Economics loads with players on, and from `OnEconomicsBalanceUpdated` for any id that parses
as a Steam id. **The player need not be online**: a payment or an adjustment reaches an offline player, and
that frame **carries no `name`** — the plugin names only a connected player, and the site reads a nameless
balance frame as not play: it moves nobody's last-seen.
**`economics.wiped`**, an event from `OnEconomicsDataWiped` in both of its shapes (one method taking an
`IPlayer`, null for the no-argument call):
```json
{"kind":"economics.wiped","type":"event","newSave":false}
```
`newSave` is true for the no-argument call — a new save, with Economics set to wipe balances with the map — and
false after `ecowipe`. Balances not yet flushed are dropped. **The site clears the wipe's balances after
`ecowipe`**; at a new save it does nothing, because the new map's balances arrive as players connect and the
last wipe's stay as the record of how it ended.
**The sidecar** adds `POST /economics/pay`, `POST /economics/balances` and `POST /economics/adjust`, thin
forwards like `/kits/uses`. It keeps nothing.
**The website (Module-Rust)** stores the tally's balance in `rust_player_wipe_stats.balance`, written over and
never summed. It serves `GET /player/rust/economics` (live, falling back to the last known) and
`GET`/`POST /admin/users/:id/rust/economics`, pays through the `rust.coins.pay` step (`risk: 'irreversible'`,
`reversible: 'none'`), and ranks the richest — a leaderboard column for one wipe, and the `richest` title
category, *Magnate* — **only above the server's starting balance** (D336).