docs(site): the Event System — the platform's facts, and two pages for it
All checks were successful
PR checks / checks (pull_request) Successful in 9m34s
All checks were successful
PR checks / checks (pull_request) Successful in 9m34s
Phase 16c of the events plan: `runicgateway.com`'s half of the workstream, now that `main` carries the engine, the module, the app and the bundle. The checks were already red and named their own answers: * `checkFacts` — nine values had moved. Protocol 5 → 7 in all three declaration sites, `moduleApi` 1.9.0 → 1.10.0, the bundle to 2026.09.10 with sidecar v2.2.0 and overlay v1.2.0, `link` v2.2.0, `Module-uo` v1.2.2. * `checkReference` — twenty-seven `Bridge.cfg` keys the site listed nowhere: the events switch and its sweep, the ten caps, the oracle NPC, the two lease keys and the seven participation keys. They are five new groups rather than an appendix to an existing one, because `EventsEnabled` is a second consent switch and belongs beside its own ceilings. Two pages, matching the treatment Teams has: * **Scheduled events** (Administration) — where it is and who sees it, authoring and immutable versions, the switchboard that arrives off, caps as a condition on an `UPDATE` rather than a role check, the dry run, the run console, what an event owns versus what it borrows, generated cleanup, the shard's own switches, and what a player sees. * **Events architecture** — the two sentences it turns on, what is a table and what deliberately is not, budgets in SQL, the ledger's two rules, at-most-once on a wire that can lose an answer, the three layers, and the four omissions. And the rest of the surface: * `/privacy` gains **`deploy-events`** — the participation ledger is personal data and no row named it. Scores and ranks against a module-opaque member key, linked to an account where one is linked; the diagnostic log swept after 90 days on terminal runs only; the run, its steps and its participants not swept at all, because they are the record of what was done to a shared world. `deploy-game-data`'s citation moves from `link/v4.md` to `v7.md`. * **Protocol versions** — the most recent bump touched *five* repositories, and the `website` row is the interesting one: core is normally out of a protocol bump's reach and this one reached it, because what changed was not a game noun but the shape of a thing core owns the ledger for. The store-migration paragraph now says four bumps' worth rather than two. * Two capability entries, so `/`, `/features/` and `/modules/` stop omitting the subsystem — an Administration item, and an **Event calendar** under Community with `/site/events` as its deep link. Deliberately *not* `needsModule`: a bare core can author and run an event, and only the world verbs need a module. * **`reference/event-catalog` is retitled "Shard event catalog"** and says what it is not. Two things in the docs were called an event catalog; the route is unchanged, so nothing outside this repository breaks. * `canonicalDocs` gains `website/EVENTS.md` and moves `link/v4.md` → `v7.md`. No screenshots. Capturing the events surfaces means standing the whole rig back up — game server, sidecar, core, module, a published event with a live run — for two or three images that no check requires, and the engagement workstream's own site leg added none either. `npm run verify` green end to end, including `checkReference` against the protocol spec that only reached `docs` `main` in RunicGateway/docs#232 — the seventh cutover step, which 16b had left on `edge`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -36,6 +36,7 @@ export const docsSidebar = [
|
||||
{ label: 'Users and roles', slug: 'docs/administration/users-and-roles' },
|
||||
{ label: 'Authentication', slug: 'docs/administration/authentication' },
|
||||
{ label: 'Teams', slug: 'docs/administration/teams' },
|
||||
{ label: 'Scheduled events', slug: 'docs/administration/events' },
|
||||
{ label: 'Moderation', slug: 'docs/administration/moderation' },
|
||||
{ label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' },
|
||||
{ label: 'Engagement rules', slug: 'docs/administration/engagement-rules' },
|
||||
@@ -66,6 +67,7 @@ export const docsSidebar = [
|
||||
{ label: 'The bridge', slug: 'docs/architecture/the-bridge' },
|
||||
{ label: 'Authentication architecture', slug: 'docs/architecture/authentication-architecture' },
|
||||
{ label: 'Teams architecture', slug: 'docs/architecture/teams-architecture' },
|
||||
{ label: 'Events architecture', slug: 'docs/architecture/events-architecture' },
|
||||
{ label: 'Protocol versions', slug: 'docs/architecture/protocol-versions' },
|
||||
],
|
||||
},
|
||||
@@ -77,7 +79,7 @@ export const docsSidebar = [
|
||||
{ label: 'sidecar.toml', slug: 'docs/reference/sidecar-toml' },
|
||||
{ label: 'Bridge.cfg', slug: 'docs/reference/bridge-cfg' },
|
||||
{ label: 'HTTP API', slug: 'docs/reference/http-api' },
|
||||
{ label: 'Event catalog', slug: 'docs/reference/event-catalog' },
|
||||
{ label: 'Shard event catalog', slug: 'docs/reference/event-catalog' },
|
||||
{ label: 'Canonical documents', slug: 'docs/reference/canonical-documents' },
|
||||
],
|
||||
},
|
||||
@@ -113,6 +115,7 @@ export const plannedSidebar = {
|
||||
'Users and roles',
|
||||
'Authentication',
|
||||
'Teams',
|
||||
'Scheduled events',
|
||||
'Moderation',
|
||||
'Notifications and email',
|
||||
'Engagement rules',
|
||||
@@ -137,6 +140,7 @@ export const plannedSidebar = {
|
||||
'The bridge',
|
||||
'Authentication architecture',
|
||||
'Teams architecture',
|
||||
'Events architecture',
|
||||
'Protocol versions',
|
||||
],
|
||||
Reference: [
|
||||
@@ -145,7 +149,7 @@ export const plannedSidebar = {
|
||||
'sidecar.toml',
|
||||
'Bridge.cfg',
|
||||
'HTTP API',
|
||||
'Event catalog',
|
||||
'Shard event catalog',
|
||||
'Canonical documents',
|
||||
],
|
||||
};
|
||||
|
||||
191
src/content/docs/docs/administration/events.mdx
Normal file
191
src/content/docs/docs/administration/events.mdx
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Scheduled events
|
||||
description: Author an event as phases and steps, price it against this deployment's caps before it runs, and let it change a live game world unattended — with a ledger that makes the undo automatic.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
An **event** is a scheduled, bounded, audited change to a live game world. You write it once
|
||||
as a sequence of phases, publish a version of it, put it on the calendar, and it runs — at
|
||||
four in the morning if that is when you scheduled it, with nobody watching.
|
||||
|
||||
That last clause is the whole reason this feature is shaped the way it is. Everything below
|
||||
that looks like extra ceremony — the switchboard, the caps, the dry run, the ledger — is
|
||||
there because the thing being automated is somebody's game world, and the person who
|
||||
authored the change is asleep when it happens.
|
||||
|
||||
**Core owns the engine; the installed module owns the meaning.** Core decides whether an
|
||||
action is permitted, when it runs, in what order, how many times, within what budget, what
|
||||
it created and who is told. The module says which verbs exist and performs them. Core never
|
||||
learns a game word: every label you see in the step editor came from the module that
|
||||
registered it.
|
||||
|
||||
## Where it is
|
||||
|
||||
**Admin → Events**, its own group in the sidebar:
|
||||
|
||||
| Row | Who sees it |
|
||||
|---|---|
|
||||
| **Events** — the definitions, and their runs | Admin, editor, moderator |
|
||||
| **Calendar** — month and list view, with series | Admin, editor, moderator |
|
||||
| **Actions** — what this deployment permits, and the caps | **Admin only** |
|
||||
| **My participation** — your own attendance | Everyone |
|
||||
|
||||
Reading is staff-wide on purpose. A moderator's power over this feature is the **run
|
||||
console** — the screen you open when an event is doing something wrong at two in the
|
||||
morning — and hiding it from the one role that exists for incident response would be a
|
||||
strange way to build an incident tool. The narrower gates are on the actions, not the rows:
|
||||
authoring is admin and editor, publishing a version and starting a run are admin only, and
|
||||
all of it is enforced on the server rather than by hiding a button.
|
||||
|
||||
## Authoring
|
||||
|
||||
A **definition** is the thing that gets listed, searched, scheduled and audited: a title, a
|
||||
slug, a storyline, a schedule, and an ordered list of **phases**. Each phase holds **steps**,
|
||||
and a step is one action with its parameters.
|
||||
|
||||
A phase advances on a condition — after a duration, or when something happens in the game a
|
||||
given number of times. The vocabulary of "something that happens" is the trigger catalog the
|
||||
installed module already ships, so a module gains phase conditions by declaring one more
|
||||
entry in a list it already had.
|
||||
|
||||
<Aside type="note" title="A timeline, not a node graph">
|
||||
The phase editor is a vertical list, deliberately. The condition grammar has no branching —
|
||||
it is `and` / `or` / `not` over comparisons and nothing else — and a canvas would advertise
|
||||
power the engine does not have. Phases in order, each with its steps, its advance condition,
|
||||
its budget draw and its failure policy, is exactly what it can do.
|
||||
</Aside>
|
||||
|
||||
### Versions are immutable, and a run pins one
|
||||
|
||||
Publishing takes a snapshot. The run that starts on Saturday holds the version that was
|
||||
published, not the one you edited on Friday — which is what makes a run reproducible and an
|
||||
audit answerable after a change. **A running event cannot be edited**; you edit the
|
||||
definition, publish a new version, and the next run picks it up.
|
||||
|
||||
## Nothing is enabled until you enable it
|
||||
|
||||
**Admin → Events → Actions** lists every action the installed modules registered, and
|
||||
**everything above a notification arrives switched off.** Installing a module must never
|
||||
start doing things to your world.
|
||||
|
||||
Each row has two controls: whether the action is permitted on this deployment at all, and its
|
||||
**per-run caps** — how much of a budget dimension one run may consume. Dimensions are
|
||||
declared by the module (`uo.creatures`, `uo.bosses`, `uo.rewards` and so on), and consumption
|
||||
is counted in the database with a conditional update, not checked in application code.
|
||||
|
||||
That distinction matters more than it sounds. A stolen admin session has already passed every
|
||||
role check there is; it still cannot exceed the cap, because the cap is a condition on the
|
||||
`UPDATE` that spends the budget.
|
||||
|
||||
<Aside type="caution" title="A cap breach is a refusal, not a failure">
|
||||
A step that would exceed a cap does not run, does not retry, and is recorded `refused` with
|
||||
the dimension and both numbers — *"asks for 12 of `uo.creatures`; 0 of 5 is already spent this
|
||||
run"*. That is an authoring mistake being reported to the author, not an outage.
|
||||
</Aside>
|
||||
|
||||
## Dry run before anything unattended
|
||||
|
||||
**Verify** materialises the whole plan without touching the world: every step is dispatched
|
||||
with a verify flag, and you get back what *would* happen and what it *would* cost against the
|
||||
caps, in the module's own words. Refusals show up here, before the calendar entry exists.
|
||||
|
||||
A definition that has never been verified is exactly the one worth not scheduling. Verifying
|
||||
is cheap, and it is the last point a human sees the plan.
|
||||
|
||||
## Running one
|
||||
|
||||
Runs start on the schedule, or by hand. A **series** groups definitions into an arc, so a
|
||||
three-part story reads as one thing on the calendar rather than three unrelated entries.
|
||||
|
||||
The **run console** shows live status, the steps and their attempts, the budget consumed
|
||||
against each cap, any failures, and the cleanup. Its controls are:
|
||||
|
||||
- **Pause** and **resume** — resume carries a run past any settled step, including one that
|
||||
failed or was refused.
|
||||
- **Skip**, **retry** and **confirm** a single step. *Confirm* is how a human-cue step
|
||||
advances: the run posts the instruction, waits, and moves on when somebody says they did it.
|
||||
- **Advance** a phase by hand.
|
||||
- **Cancel**, with or without cleanup.
|
||||
|
||||
Every one of those is logged with the person who did it.
|
||||
|
||||
## What an event does to a world, and how it is undone
|
||||
|
||||
Two different things, and the difference is the whole safety story.
|
||||
|
||||
**What it owns.** Creatures, bosses, oracle NPCs, decoration, a temporary gate — things the
|
||||
run created. Each one is written to a **resource ledger** as it is made, with the run and
|
||||
step that made it.
|
||||
|
||||
**What it borrows.** A spawner's respawn timer, a starting skill cap, a seasonal flag — values
|
||||
that already existed and are being changed for the duration. Those are **leases**: the game
|
||||
keeps the original, the site records both halves, and the lease carries its own deadline.
|
||||
|
||||
<Aside type="tip" title="Cleanup is generated, never authored">
|
||||
There is no undo phase for you to write, and that is on purpose: an operator cannot be relied
|
||||
on to write the undo, and an aborted run never reaches the phase they wrote it in. Teardown
|
||||
steps are derived from the ledger and run on **every** terminal path — completion,
|
||||
cancellation and abort alike.
|
||||
|
||||
A lease is safer still. The game restores the baseline when the deadline passes whether or not
|
||||
it ever hears from the site again, and a lease is never written to disk — so a game-server
|
||||
restart puts every borrowed value back too.
|
||||
</Aside>
|
||||
|
||||
## The game server has its own switches
|
||||
|
||||
They live on the shard host, outside the site's reach, and the site cannot turn them on.
|
||||
|
||||
**`EventsEnabled` is off by default, and it is a different switch from `AdminWriteEnabled`.**
|
||||
Turning the admin plane on is consenting to staff moderation driven from a screen somebody is
|
||||
looking at. Turning this on is consenting to the site changing and watching your world
|
||||
unattended. One switch could not honestly express both.
|
||||
|
||||
Beside it sit the game's own ceilings — how many creatures one call may spawn, how long a gate
|
||||
may stand, how much one run may own in total, how often the world may be saved. **They refuse
|
||||
rather than clamp**, for the same reason the caps do: a quietly shortened request leaves the
|
||||
two halves disagreeing about what actually happened. See
|
||||
[Bridge.cfg](/docs/reference/bridge-cfg/) for every key.
|
||||
|
||||
## What players see
|
||||
|
||||
The public calendar at `/site/events` carries what is scheduled, what is happening now, what
|
||||
finished recently, and published results. A run that was cancelled says so — *"Did not
|
||||
happen"* — rather than quietly disappearing.
|
||||
|
||||
**Listing is separate from publishing.** A definition has its own *listed* switch, because
|
||||
publishing is what makes an event runnable and a surprise invasion should not have to be
|
||||
advertised a fortnight in advance in order to be allowed to happen. Unlisting hides the
|
||||
definition, its runs and its results from the public pages and from a participant's own
|
||||
history; it hides nothing from staff.
|
||||
|
||||
Where a module can tell who took part, a run can keep a **participation ledger** — scores and
|
||||
ranks, published as a results table when the run finishes. Ranks are computed at publication
|
||||
and stored, so somebody added afterwards does not silently renumber a table people have
|
||||
already read. A signed-in person sees their own attendance under their account, and staff see
|
||||
theirs on the same screen.
|
||||
|
||||
## When something goes wrong
|
||||
|
||||
- **`degraded` is not `failed`.** If the game server disappears mid-run, the run degrades,
|
||||
world-changing steps park unattempted, and it recovers when the connection does. The public
|
||||
page does not say so — that is operator information.
|
||||
- **`refused` means a bound said no**, and it is reported with the numbers.
|
||||
- **The run log answers "why did phase 3 not start?"** as a query, not by reading a wall of
|
||||
text. It is kept for 90 days after a run reaches a terminal state — and a run still in
|
||||
flight keeps every line it has, however old, because the question it answers is still open.
|
||||
- **Cleanup can be re-run** from the run console if a teardown was interrupted.
|
||||
|
||||
## Where the record is
|
||||
|
||||
[`website/EVENTS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/EVENTS.md)
|
||||
is the design of record — the model, the data, the security argument and what was deliberately
|
||||
left out.
|
||||
[`website/MODULE_API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md)
|
||||
is the contract a module registers its verbs against, and
|
||||
[`link/ADMIN_CONTROLS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/ADMIN_CONTROLS.md)
|
||||
is what the site may ask a game to do at all.
|
||||
|
||||
For how the engine is put together, see
|
||||
[Events architecture](/docs/architecture/events-architecture/).
|
||||
163
src/content/docs/docs/architecture/events-architecture.mdx
Normal file
163
src/content/docs/docs/architecture/events-architecture.mdx
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: Events architecture
|
||||
description: An event does not edit the world — it holds a lease. How a game-agnostic engine schedules changes to a live game world it cannot name.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The Event System is a game-agnostic engine for **scheduled, bounded, audited** changes to a
|
||||
live game world. Core runs it and cannot name a single thing in your game.
|
||||
|
||||
The administrator's view is [Scheduled events](/docs/administration/events/).
|
||||
|
||||
## The two sentences the design turns on
|
||||
|
||||
**An event does not edit the world. It holds a lease.**
|
||||
|
||||
Anything an event changes that already existed is borrowed, not set: the game keeps the
|
||||
baseline, the site records both halves, and the lease carries its own deadline. When the
|
||||
deadline passes the game restores the value — whether or not it ever hears from the site
|
||||
again. A lease is never written to the game's save file either, so a server restart also
|
||||
puts every borrowed value back. That is the difference between automating a change and
|
||||
handing an unattended process a `[set` command.
|
||||
|
||||
**The module declares; core dispatches.** A module says a verb exists, what it costs and what
|
||||
it needs; core decides whether it is permitted, when it runs, in what order, how many times,
|
||||
within what budget, what it created and who is told. Nothing crosses that line as a string
|
||||
core interprets — the browser posts an action *id* and a params object, both validated
|
||||
against the registry before anything is dispatched. There is no passthrough field and no
|
||||
place a request body can name a game command.
|
||||
|
||||
## What is a table, and what deliberately is not
|
||||
|
||||
Eleven core tables, no ORM, and no migration system — which makes every table a permanent
|
||||
commitment. The rule applied was: **a table is for what must be queried, claimed or joined.**
|
||||
|
||||
| Kind | Where it lives |
|
||||
|---|---|
|
||||
| Definitions, series, versions, runs, steps, budget, resources, participants, gates, settings, log | Tables |
|
||||
| Phases | Configuration inside an immutable version snapshot. A phase has no identity a query needs; a step does |
|
||||
| Actions, budget dimensions, conditions | Registry entries a module declares at load. A stored one would outlive the module that can perform it |
|
||||
| A reward catalog | Neither. A reward is an ordinary action, so a granted reward is an ordinary ledger row |
|
||||
|
||||
**The step is the unit of execution, and it is a row** — one action invocation with a due
|
||||
time, a status, an attempt count and a claim. Retries, timeouts, duplicate execution and
|
||||
resumption after a crash are then all properties of that row rather than of a process's
|
||||
memory, which is what lets the runner be killed mid-run and pick up where it stopped.
|
||||
|
||||
**One run per occurrence, guaranteed by a unique index** on the definition, the scope and the
|
||||
scheduled instant — not by the claim. Two application instances cannot both start the same
|
||||
occurrence, because the second insert fails.
|
||||
|
||||
## Budgets are enforced in SQL
|
||||
|
||||
Consumption is spent with a conditional update:
|
||||
|
||||
```sql
|
||||
UPDATE event_run_budget
|
||||
SET consumed = consumed + ?
|
||||
WHERE run_id = ? AND dimension = ? AND consumed + ? <= cap
|
||||
```
|
||||
|
||||
No transaction, no read-then-write, and no way for two concurrent steps to both squeeze past
|
||||
the same ceiling.
|
||||
|
||||
<Aside type="tip" title="Why that is the strongest control here">
|
||||
A compromised admin session has already passed every role check the application has. It has
|
||||
not passed this one, because this one is not a check — it is a condition on the write. That is
|
||||
the reason per-run quotas were kept after the delegation model was dropped.
|
||||
</Aside>
|
||||
|
||||
## The ledger, and why cleanup is generated
|
||||
|
||||
Every world write appends to a resource ledger before it is confirmed: the run, the step, the
|
||||
owning module, an opaque kind and reference, and — for a borrowed value — the baseline
|
||||
alongside what was applied.
|
||||
|
||||
Teardown is then **derived from the ledger**, never authored, and runs on every terminal path:
|
||||
completion, cancellation and abort alike. An operator cannot be relied on to write the undo,
|
||||
and an aborted run never reaches the phase they wrote it in.
|
||||
|
||||
Two rules make that hold up:
|
||||
|
||||
- **A unique index across non-reverted rows** stops two events leasing the same target. The
|
||||
second one is refused rather than layered on top of the first.
|
||||
- **A restore is a compare-and-set.** If the current value is not what the lease applied,
|
||||
somebody else changed it since; the row is marked `drifted` rather than stamped over. The
|
||||
ledger would rather say "I do not know what happened here" than lie about having undone it.
|
||||
|
||||
## At-most-once, on a wire that can lose an answer
|
||||
|
||||
Every command the site sends the game carries an **idempotency key**, and the game executes a
|
||||
given key at most once — a repeat is answered with the original reply rather than re-run.
|
||||
|
||||
Without it, a lost acknowledgement is indistinguishable from a command that never applied, so
|
||||
every world write has to be declared un-retryable and one has to be *lost* rather than risk
|
||||
*doubling* it. The key is what makes a world-changing step an ordinary retried row like any
|
||||
other.
|
||||
|
||||
<Aside type="caution" title="The rule that pays for it">
|
||||
**Do not answer an error after changing the world.** The store treats a handler that ran and
|
||||
deliberately refused as a transient outcome and releases the key, so the answer is not frozen
|
||||
for ever — the acceptance walk found a refusal ("the last save was 227 seconds ago") replayed
|
||||
identically six times, with a number that could never age. A handler that has already changed
|
||||
something must not take that path.
|
||||
</Aside>
|
||||
|
||||
## Three layers, and the role check is only one of them
|
||||
|
||||
1. **Declaration** — a module says a verb exists. That is code the operator installed; it is
|
||||
not a permission.
|
||||
2. **Enablement** — an admin turns an action on for this deployment and sets its caps.
|
||||
Nothing above a notification is on by default.
|
||||
3. **Invocation** — the role check, then the cap, then the game's own switches. Admin routes
|
||||
are re-validated against the database on every request, so a demotion takes effect on the
|
||||
next click.
|
||||
|
||||
The game's switches are the layer the site cannot reach: `EventsEnabled` and
|
||||
`AdminWriteEnabled` live in a file on the shard host and are off out of the box, and the
|
||||
game's own ceilings **refuse rather than clamp** — because a silently shortened request leaves
|
||||
the two halves disagreeing about what happened.
|
||||
|
||||
<Aside type="note" title="Stated plainly">
|
||||
The module boundary is **not** a security boundary — a module runs in the same process with
|
||||
full access, and the module system's own documentation says so. None of the above defends
|
||||
against a hostile module. It defends against a compromised session and an operator mistake,
|
||||
both of which are made larger by *scheduling*: a change that happens while nobody is watching.
|
||||
That is why the caps and the leases matter more here than the role check does.
|
||||
</Aside>
|
||||
|
||||
## Where it meets everything else
|
||||
|
||||
- **[Engagement](/docs/administration/engagement-rules/)** — core registers `event.` triggers
|
||||
and owns none of the delivery. Who is told about a run is an ordinary rule.
|
||||
- **[The bridge](/docs/architecture/the-bridge/)** — every world verb becomes a command on the
|
||||
same versioned wire the game already speaks, through the same sidecar. Core still holds no
|
||||
game connection.
|
||||
- **[Teams](/docs/architecture/teams-architecture/)** — "this Team's members" is already a
|
||||
registered audience, so a guild-scoped event needs no event-side feature at all.
|
||||
- **News** — an event does not write posts. A core action links an *existing* post to a run and
|
||||
enqueues it through the announcement pipeline, so the in-game town crier and Discord arrive
|
||||
as legs that already exist.
|
||||
|
||||
## What it deliberately does not do
|
||||
|
||||
- **No branching.** The condition grammar is `and` / `or` / `not` over comparisons, and the
|
||||
phase editor is a timeline rather than a canvas, because a canvas would promise power the
|
||||
engine has not got.
|
||||
- **No delegation, grants or proposal queue.** Permissions gate on the admin roles that
|
||||
already exist. The whole authorisation decision lives behind one function, which is what
|
||||
keeps a coordinator model a later option rather than a redesign.
|
||||
- **No event invoking another event.** It already works by composition — a second event's
|
||||
condition can be the first one completing.
|
||||
- **No mutation of game-owned content without a baseline.** If it cannot be restored, it
|
||||
cannot be leased, and it is out.
|
||||
|
||||
## Canonical documents
|
||||
|
||||
[`website/EVENTS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/EVENTS.md)
|
||||
is the design of record;
|
||||
[`website/MODULE_API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md)
|
||||
is the contract a module registers against; and
|
||||
[`link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v7.md)
|
||||
is the wire protocol the world verbs travel on.
|
||||
@@ -46,18 +46,27 @@ allowed to be chosen independently.
|
||||
## What a bump obliges
|
||||
|
||||
Changing a message shape means editing every side plus the specification. The most recent
|
||||
bump touched:
|
||||
bump touched five repositories:
|
||||
|
||||
| Repository | What had to change |
|
||||
|---|---|
|
||||
| `servuo-plugins` | The emitters, the config keys, and `overlay.toml` |
|
||||
| `link` | `PROTOCOL_VERSION`, and the projections |
|
||||
| `module-uo` | The tables, the ingest, and the kind-to-feature map |
|
||||
| `servuo-plugins` | The handlers, the caps and switches in `Bridge.cfg`, and `overlay.toml` |
|
||||
| `link` | `PROTOCOL_VERSION`, and the endpoints that carry the new commands |
|
||||
| `module-uo` | The verbs it declares, their option sources, and the ingest |
|
||||
| `website` | Core learned a shape it had not had — a lease aimed at one named target |
|
||||
| `docs` | The protocol document and the integration guide |
|
||||
|
||||
The `website` row is the one worth noticing. Core holds no game connection and names no
|
||||
game noun, so most protocol bumps do not reach it at all — the two before this one did not.
|
||||
This one did, because what changed was not a game *noun* but the shape of a thing core owns
|
||||
the ledger for.
|
||||
|
||||
**A protocol bump can also require a store migration**, because the sidecar persists what it
|
||||
forwards. That is not automatic — version 4 needed one and version 5 did not, because
|
||||
version 5 only widened frames the store already keeps whole.
|
||||
forwards. That is not automatic, and it has happened once: version 4 added a column to a
|
||||
table that already existed. Versions 5, 6 and 7 needed none, because every frame is
|
||||
persisted whole — a bump that only widens a frame, or adds a kind, or adds a guarantee about
|
||||
how a command is executed, asks nothing of a store that defines no schema for a frame's
|
||||
contents. That is the dumb-forwarder property paying for itself.
|
||||
|
||||
## This is not the module API version
|
||||
|
||||
@@ -93,9 +102,9 @@ What is worth inheriting is the **shape**:
|
||||
|
||||
## Canonical documents
|
||||
|
||||
[`link/v5.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v5.md)
|
||||
[`link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v7.md)
|
||||
is the current protocol's record, including its cross-repository obligations, and
|
||||
[`link/v4.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v4.md)
|
||||
[`link/v6.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v6.md)
|
||||
the one before it;
|
||||
[`link/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md)
|
||||
§7 is the wire protocol, and
|
||||
|
||||
@@ -95,7 +95,7 @@ A representative line looks like:
|
||||
"price":75000,"commission":3750}
|
||||
```
|
||||
|
||||
The full catalog is [Event catalog](/docs/reference/event-catalog/).
|
||||
The full catalog is the [Shard event catalog](/docs/reference/event-catalog/).
|
||||
|
||||
## Two design details worth stealing
|
||||
|
||||
|
||||
@@ -43,8 +43,9 @@ They meet at four values pasted into the module's shard screen, and at protocol
|
||||
{platform.protocol}, which both sides check before they will pair.
|
||||
|
||||
You can stop after the first one. A site with no game server attached is a complete
|
||||
community website — news, wiki, pages, Teams, forums, accounts and moderation are all core,
|
||||
and none of them knows a game exists. The second install is what fills the game screens.
|
||||
community website — news, wiki, pages, Teams, forums, accounts, moderation and the event
|
||||
calendar are all core, and none of them knows a game exists. The second install is what
|
||||
fills the game screens, and what lets an event reach into a world.
|
||||
|
||||
## Start here
|
||||
|
||||
@@ -59,8 +60,8 @@ you should expect to see before you move on:
|
||||
6. [Verify the whole stack](/docs/getting-started/verify-the-whole-stack/) — proving it works, rather than assuming
|
||||
|
||||
Then **Administration** covers running it: configuration, branding, content, users,
|
||||
authentication, Teams, moderation, notifications, modules, the shard connection, upgrades,
|
||||
and what to do when something is wrong.
|
||||
authentication, Teams, scheduled events, moderation, notifications, modules, the shard
|
||||
connection, upgrades, and what to do when something is wrong.
|
||||
|
||||
## Where the truth lives
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ moved turns this page red rather than leaving a dead link.
|
||||
- **"Why is the module system like this?"** → `MODULE_SYSTEM.md`.
|
||||
- **"What does this API return?"** → your own deployment's `/api/docs`, then
|
||||
`BACKEND_DESIGN.md` §4.
|
||||
- **"What can the shard send?"** → `link/PLAN.md` §5, and `v4.md` for the current protocol.
|
||||
- **"What can the shard send?"** → `link/PLAN.md` §5, and `v7.md` for the current protocol.
|
||||
- **"What may a scheduled event do to the world?"** → `website/EVENTS.md` for the model,
|
||||
`MODULE_API.md` for the verbs a module may declare, and `link/ADMIN_CONTROLS.md` for what
|
||||
the site may ask a game to do at all.
|
||||
- **"Who may see this?"** → `SHARD_VISIBILITY.md` for the administrator's view,
|
||||
`modules/uo/API.md` §4 for the specification.
|
||||
- **"How do I set a shard up?"** → `installer/INSTALL.md`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Event catalog
|
||||
title: Shard event catalog
|
||||
description: What a game server can tell the website, how those events are grouped, and the five-rung ladder that decides who may see each one.
|
||||
---
|
||||
|
||||
@@ -8,6 +8,12 @@ import { visibilityLadder } from '../../../../data/reference.mjs';
|
||||
|
||||
The events a shard emits, and the mechanism that decides who may see them.
|
||||
|
||||
**Not to be confused with a scheduled event.** This page is about what the game tells the
|
||||
site, unprompted, as things happen in the world. An *event* in the sense of a thing you put
|
||||
on the calendar and run — phases, steps, a boss at eight o'clock — is
|
||||
[Scheduled events](/docs/administration/events/). The two do meet: a kind listed below is
|
||||
exactly what a scheduled event's phase can wait for.
|
||||
|
||||
The exact wire shapes are in the protocol specification and are **not** restated here — a
|
||||
copy of a wire format is a copy that will be wrong after the next bump. This page is the map
|
||||
and the security model.
|
||||
|
||||
@@ -143,6 +143,16 @@ const community = {
|
||||
'than per person by you. The on-site inbox arrives by default and can be switched ' +
|
||||
'off; push and email only ever arrive if they were asked for.',
|
||||
},
|
||||
{
|
||||
label: 'Event calendar',
|
||||
demoPath: '/site/events',
|
||||
detail:
|
||||
'What is scheduled, what is happening now, what finished recently, and the ' +
|
||||
'results afterwards — with arcs, so a three-part story reads as one thing rather ' +
|
||||
'than three unrelated entries. A run that was cancelled says so instead of ' +
|
||||
'quietly vanishing. Core owns the whole calendar and can run an event on its ' +
|
||||
'own; what an event may do inside a game world comes from the installed module.',
|
||||
},
|
||||
{
|
||||
label: 'Wiki',
|
||||
demoPath: '/wiki',
|
||||
@@ -306,6 +316,17 @@ const administration = {
|
||||
'view with an emergency unban, deliberately — it is not somewhere to tune a ' +
|
||||
'threshold at three in the morning.',
|
||||
},
|
||||
{
|
||||
label: 'Scheduled world events',
|
||||
detail:
|
||||
'Author an event as phases and steps, publish a version, put it on the calendar ' +
|
||||
'and let it run unattended — with a dry run first that prices the whole plan ' +
|
||||
'against this deployment’s caps. Every action arrives switched off, every run ' +
|
||||
'has a per-run budget enforced in the database rather than in a role check, and ' +
|
||||
'everything an event creates or borrows is written to a ledger so the undo is ' +
|
||||
'generated rather than authored. Pause, resume, skip a step or cancel with ' +
|
||||
'cleanup, all logged with the person who did it.',
|
||||
},
|
||||
{
|
||||
label: 'Module management',
|
||||
detail:
|
||||
|
||||
@@ -471,6 +471,35 @@ export const collected = [
|
||||
'website server/db/schema.sql — engagement_sends, engagement_suppressions, ' +
|
||||
'notification_channel_prefs; server/src/utils/engagementRetentionPrune.js',
|
||||
},
|
||||
{
|
||||
id: 'deploy-events',
|
||||
scope: 'deployment',
|
||||
title: 'Who took part in a scheduled event',
|
||||
body:
|
||||
'Where the operator runs scheduled events, a run can count who took part — kept as ' +
|
||||
'the name the game module knows a participant by, a score, and a rank, linked to a ' +
|
||||
'site account where one is linked and left unlinked where it is not. That is what ' +
|
||||
'the published results table renders, and what a signed-in person sees as their own ' +
|
||||
'event history. Beside it the site records what each run did: which step ran, what ' +
|
||||
'it created or borrowed in the game world, whether the undo succeeded, and which ' +
|
||||
'staff account started, paused or cancelled it.',
|
||||
retention: {
|
||||
summary:
|
||||
"A run's diagnostic log is swept after 90 days; the run itself and its participants " +
|
||||
'are kept until the operator removes them',
|
||||
detail:
|
||||
'The log that answers "why did this run stall" is deleted 90 days after a run ' +
|
||||
'reaches a terminal state, and only then — a run still in flight keeps every line ' +
|
||||
'it has, however old, because the question it answers is still open. The run, its ' +
|
||||
'steps, what it created and its participant list are not swept: they are the ' +
|
||||
'record of what was done to a shared world, and deleting one silently would ' +
|
||||
'unmake an audit. Deleting an account detaches its participation rows rather than ' +
|
||||
'removing them — the result table keeps the score and stops naming a person.',
|
||||
},
|
||||
source:
|
||||
'website server/db/schema.sql — event_runs, event_run_participants, ' +
|
||||
'event_run_resources, event_run_log; server/src/utils/eventRunner.js',
|
||||
},
|
||||
{
|
||||
id: 'deploy-game-data',
|
||||
scope: 'deployment',
|
||||
@@ -481,7 +510,7 @@ export const collected = [
|
||||
'Which of it is visible to the public is the operator’s decision, made in the ' +
|
||||
'admin panel — the bridge itself forwards, and the site decides.',
|
||||
retention: { summary: 'Operator-configured' },
|
||||
source: 'docs/link/v4.md — the visibility framework',
|
||||
source: 'docs/link/v7.md — the visibility framework',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -12,23 +12,23 @@
|
||||
"wrong protocol number in the first place."
|
||||
],
|
||||
|
||||
"verifiedOn": "2026-09-01",
|
||||
"verifiedOn": "2026-09-09",
|
||||
|
||||
"protocol": 5,
|
||||
"protocol": 7,
|
||||
|
||||
"moduleApi": "1.9.0",
|
||||
"moduleApi": "1.10.0",
|
||||
|
||||
"bundle": {
|
||||
"tag": "2026.09.01",
|
||||
"sidecar": "v2.1.0",
|
||||
"overlay": "v1.1.0",
|
||||
"tag": "2026.09.10",
|
||||
"sidecar": "v2.2.0",
|
||||
"overlay": "v1.2.0",
|
||||
"servuoMin": "57.4"
|
||||
},
|
||||
|
||||
"releases": {
|
||||
"link": "v2.1.0",
|
||||
"link": "v2.2.0",
|
||||
"installer": "v0.1.1",
|
||||
"Module-uo": "v1.1.0",
|
||||
"Module-uo": "v1.2.2",
|
||||
"Android-app": "v0.5.0"
|
||||
},
|
||||
|
||||
|
||||
@@ -151,6 +151,49 @@ export const bridgeCfg = {
|
||||
AdminReasonMaxLength: 'Reason field cap',
|
||||
AdminBanMaxDurationSec: 'Longest ban the site may set',
|
||||
},
|
||||
'Scheduled events': {
|
||||
EventsEnabled:
|
||||
'Whether the website may run scheduled events against this world at all. Off by ' +
|
||||
'default, and deliberately a separate switch from AdminWriteEnabled',
|
||||
EventsSweepSeconds: 'How often expired gates are collected and lost objects pruned',
|
||||
EventsMinSaveIntervalSec:
|
||||
'Shortest gap between world saves, counted from the last save by anyone. A save ' +
|
||||
'asked for too soon is refused rather than queued',
|
||||
},
|
||||
'Event caps': {
|
||||
EventsMaxCreatures: 'Creatures one call may spawn',
|
||||
EventsMaxBosses: 'Enhanced "boss" variants one call may spawn',
|
||||
EventsMaxNpcs: 'Oracle NPCs one call may place',
|
||||
EventsMaxDecor: 'Decoration items one call may place',
|
||||
EventsMaxGateMinutes: 'Longest a temporary gate may stand',
|
||||
EventsMaxOwnedPerRun: 'Objects one run may own across every verb — the runaway bound',
|
||||
EventsMaxSpread: 'How far from the chosen spot things may be scattered',
|
||||
EventsMaxBossMultiplier: 'How much harder than normal a boss may be made',
|
||||
EventsMaxGrantPerRun: 'How many characters one item grant may reach',
|
||||
EventsMaxGrantStack: 'How large one granted stack may be',
|
||||
},
|
||||
'The oracle NPC': {
|
||||
EventsOracleMaxLines: 'Keyword lines it will answer to',
|
||||
EventsOracleGreetRange: 'How close a player must be to be greeted',
|
||||
EventsOracleSpeechRange: 'How close a player must be to be heard',
|
||||
EventsOracleGreetCooldownSec: 'How often it greets the same player',
|
||||
EventsOracleAnswerCooldownSec: 'How often it answers the same player',
|
||||
},
|
||||
Leases: {
|
||||
LeaseMaxDurationSec:
|
||||
'Longest lease this shard will hold, whatever the site asks for. A longer request ' +
|
||||
'is refused rather than shortened',
|
||||
LeaseGraceSec: 'How long a restored lease stays listed, so a late teardown still gets a verdict',
|
||||
},
|
||||
Participation: {
|
||||
ParticipationSweepSeconds: "How often everyone standing in a run's area is credited",
|
||||
ParticipationKillWeight: 'What one kill inside the area is worth against one minute in it',
|
||||
ParticipationMaxRuns: 'Runs counted at once',
|
||||
ParticipationMaxMembers: 'Members counted per run',
|
||||
ParticipationMaxRadius: 'Widest area an event may declare',
|
||||
ParticipationGraceSec: "How long a closed run's tally stays readable",
|
||||
ParticipationSnapshotChunk: "Members resolved per yield of the game's core thread",
|
||||
},
|
||||
Accounts: {
|
||||
SignupMode: 'How game accounts may be created',
|
||||
AccountCreateEnabled: 'Allow creation at all',
|
||||
@@ -175,12 +218,13 @@ export const canonicalDocs = {
|
||||
'website/MODULE_SYSTEM.md': 'Why the module system is shaped this way',
|
||||
'website/MODULE_API.md': 'Everything a module may do — the contract',
|
||||
'website/TEAMS.md': 'Teams as a platform primitive',
|
||||
'website/EVENTS.md': 'The Event System — leases, the ledger, and the module seam',
|
||||
'website/SHARD_VISIBILITY.md': 'The audience ladder, for administrators',
|
||||
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
|
||||
'website/TRUSTED_DEVICES_MFA.md': 'Trusted devices and the second factor',
|
||||
'link/PLAN.md': 'The sidecar design of record, the data catalog and the wire protocol',
|
||||
'link/INTEGRATION.md': 'Integrating with the sidecar',
|
||||
'link/v4.md': 'Protocol 4, and its cross-repository obligations',
|
||||
'link/v7.md': 'The current protocol, and its cross-repository obligations',
|
||||
'link/ADMIN_CONTROLS.md': 'What the site may command the game to do',
|
||||
'installer/INSTALL.md': 'The operator guide for setting a shard up',
|
||||
'installer/PLAN.md': "The installer's design of record",
|
||||
|
||||
Reference in New Issue
Block a user