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: 'Users and roles', slug: 'docs/administration/users-and-roles' },
|
||||||
{ label: 'Authentication', slug: 'docs/administration/authentication' },
|
{ label: 'Authentication', slug: 'docs/administration/authentication' },
|
||||||
{ label: 'Teams', slug: 'docs/administration/teams' },
|
{ label: 'Teams', slug: 'docs/administration/teams' },
|
||||||
|
{ label: 'Scheduled events', slug: 'docs/administration/events' },
|
||||||
{ label: 'Moderation', slug: 'docs/administration/moderation' },
|
{ label: 'Moderation', slug: 'docs/administration/moderation' },
|
||||||
{ label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' },
|
{ label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' },
|
||||||
{ label: 'Engagement rules', slug: 'docs/administration/engagement-rules' },
|
{ label: 'Engagement rules', slug: 'docs/administration/engagement-rules' },
|
||||||
@@ -66,6 +67,7 @@ export const docsSidebar = [
|
|||||||
{ label: 'The bridge', slug: 'docs/architecture/the-bridge' },
|
{ label: 'The bridge', slug: 'docs/architecture/the-bridge' },
|
||||||
{ label: 'Authentication architecture', slug: 'docs/architecture/authentication-architecture' },
|
{ label: 'Authentication architecture', slug: 'docs/architecture/authentication-architecture' },
|
||||||
{ label: 'Teams architecture', slug: 'docs/architecture/teams-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' },
|
{ label: 'Protocol versions', slug: 'docs/architecture/protocol-versions' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -77,7 +79,7 @@ export const docsSidebar = [
|
|||||||
{ label: 'sidecar.toml', slug: 'docs/reference/sidecar-toml' },
|
{ label: 'sidecar.toml', slug: 'docs/reference/sidecar-toml' },
|
||||||
{ label: 'Bridge.cfg', slug: 'docs/reference/bridge-cfg' },
|
{ label: 'Bridge.cfg', slug: 'docs/reference/bridge-cfg' },
|
||||||
{ label: 'HTTP API', slug: 'docs/reference/http-api' },
|
{ 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' },
|
{ label: 'Canonical documents', slug: 'docs/reference/canonical-documents' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -113,6 +115,7 @@ export const plannedSidebar = {
|
|||||||
'Users and roles',
|
'Users and roles',
|
||||||
'Authentication',
|
'Authentication',
|
||||||
'Teams',
|
'Teams',
|
||||||
|
'Scheduled events',
|
||||||
'Moderation',
|
'Moderation',
|
||||||
'Notifications and email',
|
'Notifications and email',
|
||||||
'Engagement rules',
|
'Engagement rules',
|
||||||
@@ -137,6 +140,7 @@ export const plannedSidebar = {
|
|||||||
'The bridge',
|
'The bridge',
|
||||||
'Authentication architecture',
|
'Authentication architecture',
|
||||||
'Teams architecture',
|
'Teams architecture',
|
||||||
|
'Events architecture',
|
||||||
'Protocol versions',
|
'Protocol versions',
|
||||||
],
|
],
|
||||||
Reference: [
|
Reference: [
|
||||||
@@ -145,7 +149,7 @@ export const plannedSidebar = {
|
|||||||
'sidecar.toml',
|
'sidecar.toml',
|
||||||
'Bridge.cfg',
|
'Bridge.cfg',
|
||||||
'HTTP API',
|
'HTTP API',
|
||||||
'Event catalog',
|
'Shard event catalog',
|
||||||
'Canonical documents',
|
'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
|
## What a bump obliges
|
||||||
|
|
||||||
Changing a message shape means editing every side plus the specification. The most recent
|
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 |
|
| Repository | What had to change |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `servuo-plugins` | The emitters, the config keys, and `overlay.toml` |
|
| `servuo-plugins` | The handlers, the caps and switches in `Bridge.cfg`, and `overlay.toml` |
|
||||||
| `link` | `PROTOCOL_VERSION`, and the projections |
|
| `link` | `PROTOCOL_VERSION`, and the endpoints that carry the new commands |
|
||||||
| `module-uo` | The tables, the ingest, and the kind-to-feature map |
|
| `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 |
|
| `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
|
**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
|
forwards. That is not automatic, and it has happened once: version 4 added a column to a
|
||||||
version 5 only widened frames the store already keeps whole.
|
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
|
## This is not the module API version
|
||||||
|
|
||||||
@@ -93,9 +102,9 @@ What is worth inheriting is the **shape**:
|
|||||||
|
|
||||||
## Canonical documents
|
## 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
|
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;
|
the one before it;
|
||||||
[`link/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md)
|
[`link/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md)
|
||||||
§7 is the wire protocol, and
|
§7 is the wire protocol, and
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ A representative line looks like:
|
|||||||
"price":75000,"commission":3750}
|
"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
|
## 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.
|
{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
|
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,
|
community website — news, wiki, pages, Teams, forums, accounts, moderation and the event
|
||||||
and none of them knows a game exists. The second install is what fills the game screens.
|
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
|
## 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
|
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,
|
Then **Administration** covers running it: configuration, branding, content, users,
|
||||||
authentication, Teams, moderation, notifications, modules, the shard connection, upgrades,
|
authentication, Teams, scheduled events, moderation, notifications, modules, the shard
|
||||||
and what to do when something is wrong.
|
connection, upgrades, and what to do when something is wrong.
|
||||||
|
|
||||||
## Where the truth lives
|
## 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`.
|
- **"Why is the module system like this?"** → `MODULE_SYSTEM.md`.
|
||||||
- **"What does this API return?"** → your own deployment's `/api/docs`, then
|
- **"What does this API return?"** → your own deployment's `/api/docs`, then
|
||||||
`BACKEND_DESIGN.md` §4.
|
`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,
|
- **"Who may see this?"** → `SHARD_VISIBILITY.md` for the administrator's view,
|
||||||
`modules/uo/API.md` §4 for the specification.
|
`modules/uo/API.md` §4 for the specification.
|
||||||
- **"How do I set a shard up?"** → `installer/INSTALL.md`.
|
- **"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.
|
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.
|
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
|
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
|
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.
|
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 ' +
|
'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.',
|
'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',
|
label: 'Wiki',
|
||||||
demoPath: '/wiki',
|
demoPath: '/wiki',
|
||||||
@@ -306,6 +316,17 @@ const administration = {
|
|||||||
'view with an emergency unban, deliberately — it is not somewhere to tune a ' +
|
'view with an emergency unban, deliberately — it is not somewhere to tune a ' +
|
||||||
'threshold at three in the morning.',
|
'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',
|
label: 'Module management',
|
||||||
detail:
|
detail:
|
||||||
|
|||||||
@@ -471,6 +471,35 @@ export const collected = [
|
|||||||
'website server/db/schema.sql — engagement_sends, engagement_suppressions, ' +
|
'website server/db/schema.sql — engagement_sends, engagement_suppressions, ' +
|
||||||
'notification_channel_prefs; server/src/utils/engagementRetentionPrune.js',
|
'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',
|
id: 'deploy-game-data',
|
||||||
scope: 'deployment',
|
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 ' +
|
'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.',
|
'admin panel — the bridge itself forwards, and the site decides.',
|
||||||
retention: { summary: 'Operator-configured' },
|
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."
|
"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": {
|
"bundle": {
|
||||||
"tag": "2026.09.01",
|
"tag": "2026.09.10",
|
||||||
"sidecar": "v2.1.0",
|
"sidecar": "v2.2.0",
|
||||||
"overlay": "v1.1.0",
|
"overlay": "v1.2.0",
|
||||||
"servuoMin": "57.4"
|
"servuoMin": "57.4"
|
||||||
},
|
},
|
||||||
|
|
||||||
"releases": {
|
"releases": {
|
||||||
"link": "v2.1.0",
|
"link": "v2.2.0",
|
||||||
"installer": "v0.1.1",
|
"installer": "v0.1.1",
|
||||||
"Module-uo": "v1.1.0",
|
"Module-uo": "v1.2.2",
|
||||||
"Android-app": "v0.5.0"
|
"Android-app": "v0.5.0"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,49 @@ export const bridgeCfg = {
|
|||||||
AdminReasonMaxLength: 'Reason field cap',
|
AdminReasonMaxLength: 'Reason field cap',
|
||||||
AdminBanMaxDurationSec: 'Longest ban the site may set',
|
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: {
|
Accounts: {
|
||||||
SignupMode: 'How game accounts may be created',
|
SignupMode: 'How game accounts may be created',
|
||||||
AccountCreateEnabled: 'Allow creation at all',
|
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_SYSTEM.md': 'Why the module system is shaped this way',
|
||||||
'website/MODULE_API.md': 'Everything a module may do — the contract',
|
'website/MODULE_API.md': 'Everything a module may do — the contract',
|
||||||
'website/TEAMS.md': 'Teams as a platform primitive',
|
'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/SHARD_VISIBILITY.md': 'The audience ladder, for administrators',
|
||||||
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
|
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
|
||||||
'website/TRUSTED_DEVICES_MFA.md': 'Trusted devices and the second factor',
|
'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/PLAN.md': 'The sidecar design of record, the data catalog and the wire protocol',
|
||||||
'link/INTEGRATION.md': 'Integrating with the sidecar',
|
'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',
|
'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/INSTALL.md': 'The operator guide for setting a shard up',
|
||||||
'installer/PLAN.md': "The installer's design of record",
|
'installer/PLAN.md': "The installer's design of record",
|
||||||
|
|||||||
Reference in New Issue
Block a user