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:
@@ -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.
|
||||
|
||||
0–4 produce a working read-only multi-server Rust site that an operator can actually install. 6–7 are
|
||||
the permissions product. 9 is Teams. 10 is the notifications set. 12–13 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**. 12–13
|
||||
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 R7–R14 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 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. 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
|
||||
|
||||
Reference in New Issue
Block a user