feat(rust): the rewards — tally, kit reward, chat and the news leg (phase 13b, protocol 10)

Four event verbs and the announce leg, per PLAN.md §29:

- rust.participation.open / .collect: the plugin counts who takes part
  (seconds, kills or both, in a zone this run opened or the whole server)
  and collect files them as the run's participants, keyed by Steam id.
- rust.kit.entitle: the five recipient modes (D101), rows in the new
  rust_perm_run_grants (D84) unioned into the permission push, one extra
  use of the kit per reward as site-held credits on perm.sync (D103),
  and the rust.kit.entitled notice deferred from phase 10 (D64).
- rust.announce: one server or every server (D105).
- rust.chat announce leg, speaking only on servers whose new news switch
  is on (D104) - a card on Admin -> Rust visibility (D106).

Budgets rust.grants and rust.announcements; the kit source and four
fixed-choice sources (core has no enum param type). rust_perm_run_grants
carries core's idempotency key so a revert of a lost answer can find its
rows. Protocol 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 07:00:44 -05:00
parent 9753dda4af
commit cc185db26b
27 changed files with 2096 additions and 45 deletions

View File

@@ -63,7 +63,9 @@ const TIMEOUT_MS = 12000
* a value on a server and give it back (PLAN.md §27); **9** adds the world
* verbs — `/world/monuments`, `/world/owned`, `/world/zone`, `/world/place` and
* `/world/revert` — what an event places in the world and gives back (PLAN.md
* §28). The bump lands here in the same change as the emitters,
* §28); **10** adds the rewards — `/tally/open`, `/tally/snapshot`,
* `/tally/close`, `/kits` and `/chat`, and a `credits` field on the permission
* sync (PLAN.md §29). The bump lands here in the same change as the emitters,
* because the sidecar refuses a client declaring a different version with a
* `409`: a module left on 2 would stop being able to read the server board it
* has been reading all along. A constant that lags the deployment is not a safe
@@ -73,7 +75,7 @@ const TIMEOUT_MS = 12000
* deployment into a `409` naming both numbers instead of a parse failure three
* layers further in.
*/
const PROTOCOL_VERSION = 9
const PROTOCOL_VERSION = 10
/** What a caller gets back. Shaped once so every call site reads the same. */
function reply(ok, status, data = null) {
@@ -362,6 +364,28 @@ const worldPlace = (server, body) => request(server, '/world/place', { method: '
*/
const worldRevert = (server, body) => request(server, '/world/revert', { method: 'POST', body })
/**
* Start counting who takes part in a run (protocol 10). `data.kind` is
* `tally.ok` or `tally.error`; a repeated key is answered `repeat: true`.
*/
const tallyOpen = (server, body) => request(server, '/tally/open', { method: 'POST', body })
/** Who has taken part in a run so far, scored by the plugin. `tally.error` `no-tally` when it holds none. */
const tallySnapshot = (server, runId) =>
request(server, `/tally/snapshot?runId=${encodeURIComponent(String(runId))}`)
/** Stop counting and forget. `closed: false` means it was already gone, which is a success. */
const tallyClose = (server, body) => request(server, '/tally/close', { method: 'POST', body })
/** The kits this server's Kits plugin has, and the plugin's reward bounds. `kits.error` when Kits is not loaded. */
const kits = (server) => request(server, '/kits')
/**
* Say one line in the server's chat. `chat.ok` carries `said: false` when the
* key was said in the last ten minutes — a retry, answered and not repeated.
*/
const chat = (server, body) => request(server, '/chat', { method: 'POST', body })
module.exports = {
TIMEOUT_MS,
LEASE_TIMEOUT_MS,
@@ -388,5 +412,10 @@ module.exports = {
worldZone,
worldPlace,
worldRevert,
tallyOpen,
tallySnapshot,
tallyClose,
kits,
chat,
joinUrl,
}