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

@@ -58,6 +58,7 @@ module.exports = function register(ctx, api) {
const seeds = require('./engagement/seeds')
const eventLeases = require('./eventLeases')
const eventWorld = require('./eventWorld')
const eventRewards = require('./eventRewards')
const boot = require('./boot')
/* eslint-enable global-require */
@@ -129,8 +130,9 @@ module.exports = function register(ctx, api) {
// refresh. Registration is a claim, not a call: nothing here touches the
// database, and the seeds are written by core after the schema is up.
//
// **Not registered, and that is D62:** no announce leg and no post hook. Both
// need something in game to deliver to, and phase 10 reaches no game.
// The announce leg arrived with phase 13b (D62, D104), when there was a chat
// verb to deliver through; it is registered below with the rewards. There is
// still no post hook: nothing in game mirrors a post as state.
api.registerEventTriggers(TRIGGERS)
api.registerNotificationStreams(STREAMS)
api.registerAudiences(AUDIENCES)
@@ -160,19 +162,32 @@ module.exports = function register(ctx, api) {
// The world verbs (PLAN.md §28, protocol 9): what an event MAKES and gives
// back — a zone, crates, NPCs — and the budgets that price them, each declared
// beside the verb that spends it (D79, D89). A lease spends none of them.
api.registerEventBudgets(eventWorld.BUDGETS)
api.registerEventActions(eventWorld.ACTIONS)
//
// The rewards (PLAN.md §29, protocol 10) join them: the tally, the kit reward
// and the chat line, with the two budgets they spend. Registered in the same
// calls' neighbours, not merged into eventWorld's arrays, so each file keeps
// its own statement of what it declares.
api.registerEventBudgets([...eventWorld.BUDGETS, ...eventRewards.BUDGETS])
api.registerEventActions([...eventWorld.ACTIONS, ...eventRewards.ACTIONS])
// News in game chat (D104). Core enqueues every registered leg for every
// published post, so the leg itself sends only to the servers whose switch an
// operator turned on — off by default, and a server that is down is skipped.
api.registerAnnounceLeg(eventRewards.LEG)
// ONE call for every option source: core takes a batch once, as this module's
// complete statement, and refuses a second.
api.registerEventOptionSources([...eventLeases.OPTION_SOURCES, ...eventWorld.OPTION_SOURCES])
api.registerEventOptionSources([
...eventLeases.OPTION_SOURCES,
...eventWorld.OPTION_SOURCES,
...eventRewards.OPTION_SOURCES,
])
// Everything else this module will register — the rewards and the announce
// leg (13b), the slash commands — is deliberately absent. Each arrives
// with the phase that has something real to put in it. A registration
// with nothing behind it is worse than a missing one: a declared trigger
// nothing emits and a declared slot nothing fills are both surfaces an operator
// can configure and then wait on.
// Everything else this module will register — the slash commands — is
// deliberately absent, and arrives with the phase that has something real to
// put in it. A registration with nothing behind it is worse than a missing
// one: a declared trigger nothing emits and a declared slot nothing fills are
// both surfaces an operator can configure and then wait on.
log.info('registered', {
version: require('../module.json').version,
@@ -183,8 +198,9 @@ module.exports = function register(ctx, api) {
streams: STREAMS.length,
audiences: AUDIENCES.length,
leases: eventLeases.LEASES.length,
actions: eventWorld.ACTIONS.length,
budgets: eventWorld.BUDGETS.length,
optionSources: eventLeases.OPTION_SOURCES.length + eventWorld.OPTION_SOURCES.length,
actions: eventWorld.ACTIONS.length + eventRewards.ACTIONS.length,
budgets: eventWorld.BUDGETS.length + eventRewards.BUDGETS.length,
announceLeg: eventRewards.LEG.leg,
optionSources: eventLeases.OPTION_SOURCES.length + eventWorld.OPTION_SOURCES.length + eventRewards.OPTION_SOURCES.length,
})
}