docs(modules): the event catalogue and the engagement catalogue

Phases 12-13 described the event MECHANISM and never the CATALOGUE - one budget
and one lease as a proof of life, which is a skeleton rather than a product.
Same omission for engagement: R7 settled that the set ships and nothing said
what goes in it. Two new sections fix both.

EVENTS.md section H is a Rust/Oxide compatibility section that already sketched
the event half, and it should have been read before the phase list was written.
Its thesis is the one to design around: the lease is the primitive that travels,
not the spawn. Double gather rate for the weekend is the canonical Rust
community event and it is exactly lease-with-expiry. It also rates Rust the
EASIER case than UO, because Oxide's convars are live by default where ServUO's
are mostly cached at boot - an argument for expecting them to work, never a
substitute for verifying each key live.

Section 9 declares four budget dimensions, five option sources, seven leases and
four actions, taking section H's ids rather than inventing a parallel set. Two
things in it are load-bearing.

Caps are PER RUN, and R8 makes that matter: run.scope is part of a run's unique
key, so one definition fanning out to six servers is six separate budgets rather
than one shared pool. An operator setting a cap of 30 is setting it per server.

And rust.group.membership is the lease section H names that R16 did not - the
pair is the whole reward design. A permanent earned entitlement is an ACTION
with reversible: ledger (R16). A time-limited group is genuinely core.lease,
held with a deadline the game enforces on its own. Same permission mirror,
two shapes, and choosing wrong is the mistake: a weekend VIP implemented as a
grant is a VIP for ever if the website goes away.

Section 10 is the engagement catalogue - eleven triggers with their ceilings,
three audiences, and the seed grouping. The ceiling lattice is containment and
not size, so every ceiling is chosen against that rather than against a ladder.

rust.base.destroyed is the one that matters most and is most likely to be got
wrong. The offline raid alert is the single most-wanted notification in Rust and
its ceiling is OWNER - the player whose base it was. Ceilinged staff it is
useless to the person who needs it; ceilinged everyone it broadcasts base
locations to the server. Exactly the case the lattice exists for.

Three hooks carry data that must never widen: CanUserLogin and OnUserApproved
carry IP addresses, OnPlayerReported carries player reports. README.md section 5
already flags these as admin-channel-only on the live feed and the same
judgement binds their triggers.

One design note flagged rather than decided: PopupNotifications gives the module
an IN-GAME alert surface, which is not one of core's channels. An in-game popup
is the module publishing to its own surface off its own trigger, not a fourth
channel core learns about. A raid alert that reaches a phone and pops on screen
next login is two mechanisms and only one of them is core's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-15 12:17:39 -05:00
parent 43bf73bae5
commit 601ee4e05f

View File

@@ -479,8 +479,9 @@ honest reading is that the first list was a game-bridge plan with a website modu
the kit treats the module as the bulk of the work.
04 produce a working read-only multi-server Rust site that an operator can actually install. 67 are
the permissions product. 9 is Teams. 10 is the notifications set. 1213 are events. 14 is the map. 18
is how any of it reaches somebody who is not us. The Android legs (5, 8, 11, 15) each trail the
the permissions product. 9 is Teams. 10 is the notifications set, whose catalogue is **§10**. 1213
are events, whose catalogue is **§9**. 14 is the map. 18 is how any of it reaches somebody who is
not us. The Android legs (5, 8, 11, 15) each trail the
website surface they consume by one phase, per R10.
Each phase ends with its findings written down, as every workstream here does.
@@ -497,10 +498,10 @@ Each phase ends with its findings written down, as every workstream here does.
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
| 9 | **Teams from first-party clans** (R5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook | Module-Rust + docs | An operator turns on a rule, edits a body, and a wipe announcement reaches the right people and nobody else |
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers | Android-app | A Rust notification arrives on a phone and can be switched off there |
| 12 | **Events: one budget, one verified lease.** [kit][kit] ch. 5's own ordering — the lease before the action | Module-Rust + 2 | The leased value is observed changing in the running game and restored, per key |
| 13 | **Events: the Kits reward action** (R3, R16). Grants the kit's `RequiredPermission`, not the items; `reversible: 'ledger'`; the option source flags kits with no permission gate | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it |
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it |
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
@@ -658,6 +659,149 @@ recorded as R7R14 in §2 rather than repeated here:
| Extension slots? | `admin.users.detail` **and** `site.footer.status` | R13 |
| Which mount prefixes? | `/rust` on all three tiers | R14 |
The two in §3 are deferred by choice rather than unanswered.
§3 is empty; both were closed on the same day.
## 9. The event catalogue
Added 2026-09-15. **Phases 1213 described the event *mechanism* and never the *catalogue*** — one
budget and one lease as a proof of life, which is a skeleton rather than a product. This section is
what the module actually declares.
[`EVENTS.md`](../../website/EVENTS.md) **§H is a Rust/Oxide compatibility section that already
sketched this**, and it should have been read before §5 was written. What follows takes its ids and
its reasoning as the starting point rather than inventing a parallel set.
> **§H's thesis, and it is the one to design around:** *"The lease is the primitive that travels, not
> the spawn. Double gather rate for the weekend is the canonical Rust community event, and it is
> exactly lease-with-expiry. Spawning creatures at a landmark is UO-shaped; holding a value for four
> hours is every game."* It also rates Rust the **easier** case than UO, because Oxide's convars are
> live by default where ServUO's are mostly cached at boot.
### Budgets — what core counts and bounds
| Dimension | Counts |
|---|---|
| `rust.prefabs` | objects placed into the world by a run |
| `rust.zone.minutes` | zone/dome time held |
| `rust.grants` | entitlements granted (R16) |
| `rust.announcements` | in-game broadcasts |
**Caps are per run, and R8 makes that load-bearing.** §H: `run.scope` is part of a run's unique key,
so one definition fanning out to six servers is **six separate budgets, not one shared pool**. An
operator setting a cap of 30 prefabs is setting it per server. Say so on the field.
### Option sources — what fills a dropdown
| Source | Filled from |
|---|---|
| `rust.options.kits` | Kits `GetKitNames` / `GetAllKits`, **flagged by whether `RequiredPermission` is set** (R16) |
| `rust.options.groups` | Oxide permission groups (§H names this one) |
| `rust.options.permissions` | registered permissions |
| `rust.options.prefabs` | a plugin-declared constructible allowlist — the analogue of UO's spawn atlas |
| `rust.options.monuments` | monument names, shared with the map work (R9) |
Every one resolves from live data and returns `[]` on failure rather than defending with a hardcoded
list that will be wrong. A source that refuses degrades its field to free text with a warning and
never blocks the form.
### Leases — values borrowed with a deadline
The heart of it, and the thing to build first.
| Lease | Value |
|---|---|
| `rust.rate.gather` | gather rate multiplier |
| `rust.rate.craft` | craft speed |
| `rust.rate.smelt` | smelting speed |
| `rust.rate.decay` | decay scale |
| `rust.time.night` | night length |
| `rust.population.<kind>` | spawn population multipliers |
| `rust.group.membership` | **a time-limited permission group — weekend VIP** |
**`rust.group.membership` is the one §H names that R16 did not, and the pair is the whole design.**
R16 settled that a *permanent* earned entitlement is an **action** with `reversible: 'ledger'` — grant
the kit's permission, revert revokes it. §H settles that a *time-limited* group is genuinely
**`core.lease`** — held with a deadline the game enforces on its own, restored when it expires
without core having to come back. Same underlying permission mirror (R2), two different shapes,
and choosing the wrong one is the mistake: a weekend VIP implemented as a grant is a VIP who stays
one for ever if the website goes away.
**Every key gets verified live before it is advertised** — apply, observe in the running game,
restore, per key. §H's claim that Rust convars are live by default is an argument for *expecting*
them to work, never a substitute for checking. A value the server reads once at boot applies
cleanly, reads back cleanly, and does nothing at all, and neither core nor review can catch it.
### Actions — verbs a run performs
| Action | `risk` | `reversible` | Notes |
|---|---|---|---|
| `rust.kit.entitle` | `change` | `ledger` | R16 — grants the kit's `RequiredPermission`; revert revokes |
| `rust.prefab.place` | `change` | `ledger` | §H's verb; revert kills the entity, and needs the persisted ownership registry ch. 4 describes |
| `rust.announce` | `notify` | `none` | via PopupNotifications (R6) — global or targeted |
| `rust.zone.open` | `change` | `ledger` | §H's other verb; needs a zone plugin, so it belongs in the optional tier (R15) rather than the base |
**Rewards are not a contract member.** `EVENTS.md` deleted a `registerEventRewards` registry because
it carried four Ultima Online nouns inside a core signature. A reward here is an ordinary action —
which is exactly why R16 could change what it grants without touching anything of core's.
## 10. The engagement catalogue — what Rust can expose
Added 2026-09-15, answering "check the default alerts Rust can expose". R7 settled that the set
ships; this is what goes in it.
**The ceiling lattice is containment, not size**`self`, `owner`, `subscribers`, `staff`,
`members`, `authenticated`, `everyone`, and the flat reading is the trap. `staff` is **not** a
superset of `owner`: for a cheat-detection event, "one person" is *the player it was detected on*.
Every ceiling below is chosen against that, not against a ladder.
| Trigger | Source | `ceiling` | `subjectKey` |
|---|---|---|---|
| `rust.wipe.started` | `OnNewSave` | `everyone` | server |
| `rust.server.online` / `.offline` | link state transition | `everyone` | server |
| `rust.leaderboard.topped` | our own rollup (R12) | `everyone` | server |
| `rust.base.destroyed` | `OnEntityDeath` on owned building blocks | **`owner`** | player |
| `rust.kit.entitled` | R16's own grant | **`self`** | user |
| `rust.player.linked` | R1's link flow | **`self`** | user |
| `rust.clan.member.added` / `.left` / `.kicked` | first-party clan hooks (R5) | `members` | clan |
| `rust.clan.disbanded` | `OnClanDisbanded` | `members` | clan |
| `rust.player.reported` | `OnPlayerReported` | **`staff`** | player |
| `rust.login.denied` | `CanUserLogin` | **`staff`** | player |
| `rust.player.banned` / `.unbanned` | `OnUserBanned` / `OnUserUnbanned` | **`staff`** | player |
**`rust.base.destroyed` is the one that matters most and the one most likely to be got wrong.** The
offline raid alert is the single most-wanted notification in Rust, and its ceiling is `owner` — the
player whose base it was. Ceilinged `staff` it would be useless to the person who needs it, and
ceilinged `everyone` it would broadcast base locations to the server. This is exactly the case the
lattice exists for.
**Three hooks carry data that must never widen.** `CanUserLogin` and `OnUserApproved` carry **IP
addresses**; `OnPlayerReported` carries player reports. [`README.md`](README.md) §5 already flags
these as admin-channel-only on the live feed, and the same judgement binds their triggers.
### Audiences
| Audience | Resolves to | `ceiling` |
|---|---|---|
| `rust.clan.members` | a clan's linked members | `members` |
| `rust.server.players` | linked accounts seen on a server this wipe | `authenticated` |
| `rust.wipe.participants` | everyone who played the current wipe | `authenticated` |
A resolver returns **user ids and nothing else** — never a template, a channel or an address — and
one that fails resolves to **nobody**, never to everybody and never to its last good answer. Its
params are constant, filled in when an operator saves the rule, so "the clan this event was about"
is not expressible; an event that needs that carries its own recipients.
### Seeds, and one thing to decide when building them
Bodies re-ensure every boot under a seed version; **rule groups are offered once per group key**, so
a rule appended to an existing group reaches fresh installs only. Wipe announcements, raid alerts and
clan transitions each take their own group key for that reason.
**One design note, flagged rather than decided.** `PopupNotifications` gives the module an *in-game*
alert surface, which is not one of core's channels — core resolves ids to email, in-app and push. So
an in-game popup is the module publishing to its own surface off its own trigger, not a fourth
channel core learns about. Worth settling deliberately at phase 10: a raid alert that reaches a
player's phone *and* pops on their screen next login is two mechanisms, and only one of them is
core's.
[kit]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit