docs(site): the Event System — the platform's facts, and two pages for it #30
@@ -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