diff --git a/src/config/sidebar.mjs b/src/config/sidebar.mjs
index e91818e..b7baadd 100644
--- a/src/config/sidebar.mjs
+++ b/src/config/sidebar.mjs
@@ -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',
],
};
diff --git a/src/content/docs/docs/administration/events.mdx b/src/content/docs/docs/administration/events.mdx
new file mode 100644
index 0000000..fc1aef3
--- /dev/null
+++ b/src/content/docs/docs/administration/events.mdx
@@ -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.
+
+
+
+### 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.
+
+
+
+## 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.
+
+
+
+## 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/).
diff --git a/src/content/docs/docs/architecture/events-architecture.mdx b/src/content/docs/docs/architecture/events-architecture.mdx
new file mode 100644
index 0000000..6362ee8
--- /dev/null
+++ b/src/content/docs/docs/architecture/events-architecture.mdx
@@ -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.
+
+
+
+## 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.
+
+
+
+## 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.
+
+
+
+## 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.
diff --git a/src/content/docs/docs/architecture/protocol-versions.mdx b/src/content/docs/docs/architecture/protocol-versions.mdx
index dbaf1ae..893b863 100644
--- a/src/content/docs/docs/architecture/protocol-versions.mdx
+++ b/src/content/docs/docs/architecture/protocol-versions.mdx
@@ -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
diff --git a/src/content/docs/docs/architecture/the-bridge.mdx b/src/content/docs/docs/architecture/the-bridge.mdx
index 4d8773d..dcafcf3 100644
--- a/src/content/docs/docs/architecture/the-bridge.mdx
+++ b/src/content/docs/docs/architecture/the-bridge.mdx
@@ -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
diff --git a/src/content/docs/docs/index.mdx b/src/content/docs/docs/index.mdx
index f44712d..29b1ffb 100644
--- a/src/content/docs/docs/index.mdx
+++ b/src/content/docs/docs/index.mdx
@@ -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
diff --git a/src/content/docs/docs/reference/canonical-documents.mdx b/src/content/docs/docs/reference/canonical-documents.mdx
index 9d34fa8..14be822 100644
--- a/src/content/docs/docs/reference/canonical-documents.mdx
+++ b/src/content/docs/docs/reference/canonical-documents.mdx
@@ -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`.
diff --git a/src/content/docs/docs/reference/event-catalog.mdx b/src/content/docs/docs/reference/event-catalog.mdx
index 8ae00db..097f785 100644
--- a/src/content/docs/docs/reference/event-catalog.mdx
+++ b/src/content/docs/docs/reference/event-catalog.mdx
@@ -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.
diff --git a/src/data/capabilities.mjs b/src/data/capabilities.mjs
index 2df9c24..c0c83c2 100644
--- a/src/data/capabilities.mjs
+++ b/src/data/capabilities.mjs
@@ -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:
diff --git a/src/data/collection.mjs b/src/data/collection.mjs
index 2af9fb3..ee69496 100644
--- a/src/data/collection.mjs
+++ b/src/data/collection.mjs
@@ -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',
},
];
diff --git a/src/data/platform.json b/src/data/platform.json
index 062facc..ea52ca2 100644
--- a/src/data/platform.json
+++ b/src/data/platform.json
@@ -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"
},
diff --git a/src/data/reference.mjs b/src/data/reference.mjs
index 4164ff5..69f3bcb 100644
--- a/src/data/reference.mjs
+++ b/src/data/reference.mjs
@@ -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",