4 Commits

Author SHA1 Message Date
c3c347d237 Merge pull request 'docs(site): the Asset Bridge — protocol 8, a screen for client files, and pictures in the screenshots (Phase 9c)' (#31) from docs/asset-bridge-p9c into main
Some checks failed
PR checks / checks (push) Successful in 9m28s
Build and publish the image / build (push) Failing after 14m2s
Build and publish the image / deploy (push) Has been cancelled
Reviewed-on: #31
2026-09-15 12:11:16 +00:00
4e1ce2316a docs(site): the Asset Bridge — protocol 8, a screen for client files, and pictures in the screenshots (Phase 9c)
All checks were successful
PR checks / checks (pull_request) Successful in 1m41s
The last leg of the Asset Bridge (docs/link/v8.md §16, phase 9c). The cutover put
protocol 8 on `main` in every declaration site, which turned this repo red by
construction: `checkFacts.mjs` reads the protocol from `link`'s `main`.

Two checks were failing, and only one of them was expected.

**checkFacts (10)** — protocol 7 → 8 in all three reads (sidecar, overlay, bundle),
the bundle triple to 2026.09.15 / sidecar v2.3.0 / overlay v1.3.0, and the releases
to link v2.3.0, installer v0.2.0, Module-uo v1.3.0. `moduleApi` and the capability
list did not move: core's whole share of eight phases was a deletion.

**checkReference (9)** — nobody had planned for this one. `Bridge.cfg` grew nine keys
across phases 5, 6 and 7 and the reference page had gone on not mentioning them,
which is the failure mode that check exists for. They are listed as two new groups,
`Client assets` and `Spawn files`, because they are two separate consents.

## The prose the bridge changed

- **New page, Administration → Client files.** `Admin → Client files` is a screen an
  operator has to press a button on and the site had no page for it. It carries the
  rule nothing else states: **nothing here happens on a restart**, so a patched client
  keeps serving the old artwork until somebody says so.
- **Protocol versions** said the most recent bump touched five repositories and named
  `website`. Protocol 8 touched four and did not reach core — its absence is the
  interesting half, and the page now says why. The store-migration paragraph gains 8,
  which changed no line of the sidecar's store despite moving megabytes.
- **The bridge** said "two ways in". There are three: bulk reads go over the
  request/reply path in pages, one in flight at a time, and the reason they must not
  ride the event stream is structural rather than about speed.
- **Requirements** gains `libgdiplus` for Linux shard hosts, and the note that no game
  client has to be installed for any of this — a ServUO shard cannot boot without one.
- **Troubleshooting** gains the failure mode that did not exist before: a bestiary with
  no pictures, or items reading as numbers.
- **Maintenance** gains the button to press after patching a client, and **Verify the
  whole stack** the `doctor` check that landed with it.
- The spawn atlas and the marketplace both carry artwork now, so both capability
  descriptions say so, and `canonicalDocs` points at `link/v8.md`.

## Screenshots, retaken against the real thing

The atlas and marketplace shots predate the asset plane, so both were retaken on the
demo deployment against a real shard and a real sidecar, plus the new admin screen:
1,095 portraits imported in 2.1 s, 67,496 names in 1.4 s, 313 item pictures in 0.8 s.
Nothing here is a mock-up. The atlas shot moved 36px down its page because portraits
made the rows taller.

## One defect found on the way

`seedDemo.mjs` pinned the demo deployment at **protocol 4** with a comment explaining
a `module-uo` debt that has since been paid. Its default now comes from
`platform.json`, so the rig cannot drift two protocols behind the platform again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 05:45:38 -05:00
73f664c38e Merge pull request 'docs(site): the Event System — the platform's facts, and two pages for it' (#30) from docs/events-system into main
All checks were successful
Build and publish the image / build (push) Successful in 4m14s
Build and publish the image / deploy (push) Successful in 42s
PR checks / checks (push) Successful in 8m22s
Reviewed-on: #30
2026-09-10 04:27:50 +00:00
e91e76bfa9 docs(site): the Event System — the platform's facts, and two pages for it
All checks were successful
PR checks / checks (pull_request) Successful in 9m34s
Phase 16c of the events plan: `runicgateway.com`'s half of the workstream, now
that `main` carries the engine, the module, the app and the bundle.

The checks were already red and named their own answers:

* `checkFacts` — nine values had moved. Protocol 5 → 7 in all three declaration
  sites, `moduleApi` 1.9.0 → 1.10.0, the bundle to 2026.09.10 with sidecar
  v2.2.0 and overlay v1.2.0, `link` v2.2.0, `Module-uo` v1.2.2.
* `checkReference` — twenty-seven `Bridge.cfg` keys the site listed nowhere: the
  events switch and its sweep, the ten caps, the oracle NPC, the two lease keys
  and the seven participation keys. They are five new groups rather than an
  appendix to an existing one, because `EventsEnabled` is a second consent
  switch and belongs beside its own ceilings.

Two pages, matching the treatment Teams has:

* **Scheduled events** (Administration) — where it is and who sees it, authoring
  and immutable versions, the switchboard that arrives off, caps as a condition
  on an `UPDATE` rather than a role check, the dry run, the run console, what an
  event owns versus what it borrows, generated cleanup, the shard's own switches,
  and what a player sees.
* **Events architecture** — the two sentences it turns on, what is a table and
  what deliberately is not, budgets in SQL, the ledger's two rules, at-most-once
  on a wire that can lose an answer, the three layers, and the four omissions.

And the rest of the surface:

* `/privacy` gains **`deploy-events`** — the participation ledger is personal
  data and no row named it. Scores and ranks against a module-opaque member key,
  linked to an account where one is linked; the diagnostic log swept after 90
  days on terminal runs only; the run, its steps and its participants not swept
  at all, because they are the record of what was done to a shared world.
  `deploy-game-data`'s citation moves from `link/v4.md` to `v7.md`.
* **Protocol versions** — the most recent bump touched *five* repositories, and
  the `website` row is the interesting one: core is normally out of a protocol
  bump's reach and this one reached it, because what changed was not a game noun
  but the shape of a thing core owns the ledger for. The store-migration
  paragraph now says four bumps' worth rather than two.
* Two capability entries, so `/`, `/features/` and `/modules/` stop omitting the
  subsystem — an Administration item, and an **Event calendar** under Community
  with `/site/events` as its deep link. Deliberately *not* `needsModule`: a bare
  core can author and run an event, and only the world verbs need a module.
* **`reference/event-catalog` is retitled "Shard event catalog"** and says what
  it is not. Two things in the docs were called an event catalog; the route is
  unchanged, so nothing outside this repository breaks.
* `canonicalDocs` gains `website/EVENTS.md` and moves `link/v4.md` → `v7.md`.

No screenshots. Capturing the events surfaces means standing the whole rig back
up — game server, sidecar, core, module, a published event with a live run — for
two or three images that no check requires, and the engagement workstream's own
site leg added none either.

`npm run verify` green end to end, including `checkReference` against the
protocol spec that only reached `docs` `main` in RunicGateway/docs#232 — the
seventh cutover step, which 16b had left on `edge`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-09 22:04:27 -05:00
23 changed files with 744 additions and 49 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -14,7 +14,7 @@
* UOLINK_BASE http://127.0.0.1:8080 sidecar REST, written to Admin → Shard
* UOLINK_WS ws://127.0.0.1:8080/ws sidecar WebSocket
* UOLINK_TOKEN (unset) sidecar auth token; skipped when absent
* UOLINK_PROTOCOL 4 wire protocol to pin — see the note below
* UOLINK_PROTOCOL (platform.json) wire protocol to pin — see the note below
*
* ---------------------------------------------------------------------------------------
* WHY THE SEED DRIVES THE API AND NEVER THE DATABASE
@@ -45,6 +45,8 @@
import { readFileSync } from 'node:fs';
import platform from '../src/data/platform.json' with { type: 'json' };
const BASE = (process.env.RG_BASE || 'http://localhost:3000').replace(/\/+$/, '');
const API = `${BASE}/api/v1`;
const ADMIN_USER = process.env.RG_ADMIN_USER || 'demoadmin';
@@ -53,16 +55,21 @@ const DEMO_PASS = process.env.RG_DEMO_PASS || 'DemoReview!2026';
const UOLINK_BASE = process.env.UOLINK_BASE || 'http://127.0.0.1:8080';
const UOLINK_WS = process.env.UOLINK_WS || 'ws://127.0.0.1:8080/ws';
const UOLINK_TOKEN = process.env.UOLINK_TOKEN || '';
// The pinned wire protocol has to be STATED, not left to the module's default.
// The pinned wire protocol, read from `platform.json` rather than written down here.
//
// `module-uo`'s schema fragment still carries `protocol INT NOT NULL DEFAULT 3`, from the
// protocol-3 cutover; the sidecar on `link` `main` speaks 4. The module handles protocol 4's
// frames — `guild.roster` and `guild.leave` ingest landed with the Teams cutover — but a
// FRESH install pins 3, and the sidecar answers a 3 with `409 protocol version mismatch` on
// every REST call. So a new deployment reads nothing from its shard until somebody edits the
// number in Admin → Shard. Raised with the org lead rather than patched from here: the fix
// belongs in `module-uo`, not in this repo's screenshot rig (PLAN.md §13 phase 9).
const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || 4);
// It was a literal `4` until the Asset Bridge cutover, with a note explaining that
// `module-uo` pinned 3 on a fresh install while the sidecar spoke 4, so a new deployment
// read nothing from its shard until somebody edited the number in Admin → Shard. That debt
// has since been paid: the module's schema fragment defaults the column to the protocol its
// build speaks and carries a one-shot migration per bump, so both a fresh install and an
// upgraded one land on the right number by themselves.
//
// What remains is the rig's own reason to state it: this seed points a demo deployment at a
// sidecar, and if it pins the wrong number every REST call comes back `409`. Reading it from
// `platform.json` means the number is the one `checkFacts.mjs` verified against `link`'s
// `main` — so the rig cannot quietly drift two protocols behind the platform again, which is
// exactly what the literal did.
const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.protocol);
const DRY = process.argv.includes('--dry-run');

View File

@@ -36,12 +36,14 @@ 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' },
{ label: 'Message templates', slug: 'docs/administration/message-templates' },
{ label: 'Managing modules', slug: 'docs/administration/managing-modules' },
{ label: 'The shard connection', slug: 'docs/administration/the-shard-connection' },
{ label: 'Client files', slug: 'docs/administration/client-files' },
{ label: 'Maintenance and upgrades', slug: 'docs/administration/maintenance-and-upgrades' },
{ label: 'Troubleshooting', slug: 'docs/administration/troubleshooting' },
],
@@ -66,6 +68,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 +80,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,12 +116,14 @@ export const plannedSidebar = {
'Users and roles',
'Authentication',
'Teams',
'Scheduled events',
'Moderation',
'Notifications and email',
'Engagement rules',
'Message templates',
'Managing modules',
'The shard connection',
'Client files',
'Maintenance and upgrades',
'Troubleshooting',
],
@@ -137,6 +142,7 @@ export const plannedSidebar = {
'The bridge',
'Authentication architecture',
'Teams architecture',
'Events architecture',
'Protocol versions',
],
Reference: [
@@ -145,7 +151,7 @@ export const plannedSidebar = {
'sidecar.toml',
'Bridge.cfg',
'HTTP API',
'Event catalog',
'Shard event catalog',
'Canonical documents',
],
};

View File

@@ -0,0 +1,103 @@
---
title: Client files
description: Creature portraits, item pictures and the game's own name table — where they come from, the one button that imports them, and why nothing here happens on a restart.
---
import Screenshot from '../../../../components/Screenshot.astro';
import { Aside } from '@astrojs/starlight/components';
Most of what a game shows you is not text. Ultima Online keeps its creature artwork, its
item graphics and even its item *names* inside the client files, and a site that cannot read
them shows a bestiary of words and a marketplace of numbers.
With the `uo` module installed, **Client files** appears in the admin sidebar at
`/admin/uo/files`. It is where those three things arrive.
<Screenshot id="admin-client-files" />
## Where they come from
A ServUO shard cannot boot without a UO client — it resolves one at startup to read the
world's own data. So the files were already on the shard host, and the shard reads and
decodes them there, handing the results over the bridge like everything else.
**Nothing is converted on a desktop and nothing is uploaded.** Earlier versions of this
platform asked an operator to install a third-party tool, build a converter against it and
copy the output onto the web host. That path is gone.
## Three things, one page
| Section | Fills | How it arrives |
|---|---|---|
| **Creature portraits** | The bestiary and the spawn atlas | One picture per creature body, imported as a **set** |
| **Item and land pictures** | Marketplace listings and character sheets | **One at a time**, shortly after a page asks for one |
| **Item and title names (clilocs)** | Anywhere an item is named | The whole table at once — tens of thousands of names |
They are one page because they are one job: they come out of one client install, and they
all change at the same moment — when you patch it.
<Aside type="caution" title="Nothing here happens on a restart">
Boot deliberately never asks the shard for client files. A client patch is an event **you**
know about and the website does not, and a site that re-read hundreds of megabytes on every
restart to discover nothing had changed would pay for the rare case forever.
So after you patch your client, the site keeps serving the old pictures and the old names
until somebody presses a button on this page. That is the whole reason the page exists.
</Aside>
## Update, or re-import everything
Every section offers the same pair, and the difference is worth knowing:
- **Update** asks the shard what changed first and transfers only that. When nothing has, it
costs one small round trip and answers *"unchanged"*.
- **Re-import everything** fetches the lot. It is for the case the first cannot see — you
restored a backup, or lost the uploads volume, and the database still remembers pictures
that are no longer on disk.
Item and land pictures work differently, because there are tens of thousands of item
graphics times every dye colour and importing them as a set would be absurd. They arrive
lazily instead. The two buttons there — *Fetch waiting pictures* and *Refresh the ones I
have* — exist for the two moments waiting is the wrong answer: you have just linked a shard,
or you have just patched a client.
## When the page says something is wrong
Every one of these is a reported state with a reason, not an error. The site keeps serving
whatever is already imported in all of them.
| What you see | What it means |
|---|---|
| **The shard is busy with another client-file request** | Not a fault. The shard serves one of these at a time, and an import — or the item-picture pass refilling itself — is holding it. It frees itself. |
| **The shard is not answering for client files** | The ordinary bridge problem: see [The shard connection](/docs/administration/the-shard-connection/). |
| **…set `AssetsEnabled` on the shard** | The asset plane is switched off in [`Bridge.cfg`](/docs/reference/bridge-cfg/). It is a separate switch on purpose — turning it on is consenting to the website reading this host's client files. |
| **The shard host cannot render images** | A Linux host with no `libgdiplus`. Names are unaffected, because they have no pixels in them. |
| **Waiting for you: *n* pictures … no longer offered** | The shard stopped offering artwork this site holds. A deletion is never silent here; it waits for you to approve or dismiss it. |
<Aside type="note" title="Linux shard hosts need one package">
ServUO runs under Mono on Linux, and the library it decodes sprites with is a thin layer
over **`libgdiplus`** — in the *decode* path, not merely the encode. Without it the shard
cannot read a single sprite.
`sudo apt-get install libgdiplus`, or `dnf install libgdiplus`. `runicgateway doctor` checks
for it, and Windows shard hosts need nothing. See
[Requirements](/docs/getting-started/requirements/).
</Aside>
## What it will not do
- **It never writes to the game.** Everything on this plane is a read.
- **It never overwrites your own artwork.** A portrait you drew and named yourself always
wins over an imported one.
- **Creatures with no artwork stay as text.** That is normal rather than a failure — a stock
client has no animation for most ghost and gargoyle bodies, and the shard reports nothing
rather than guessing. A wrong picture is worse than no picture.
## Canonical documents
[`link/v8.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.md)
is the asset plane's design of record;
[`link/SHARD_PREREQS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/SHARD_PREREQS.md)
covers what a shard host needs first, and
[`website/UPGRADE_NOTES.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/UPGRADE_NOTES.md)
is what to do on a site that was running before this existed.

View 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/).

View File

@@ -116,6 +116,13 @@ when ServUO needs restarting — it never restarts your shard itself. Because it
**bundle**, the sidecar and the plugin move together and cannot end up disagreeing about the
protocol.
<Aside type="caution" title="After you patch the UO client, press one more button">
Creature portraits, item pictures and the name table are read from that client, and the
site deliberately never re-reads them on its own — a restart does not, and neither does
`update`. They keep serving the old artwork until somebody presses *Update* on
**Admin → Client files**. It is one round trip when nothing has changed.
</Aside>
<Aside type="note" title="Update the two sides in either order, but verify after each">
They are independent deployments joined by a version-checked contract: a mismatch is
rejected with a `409` rather than mis-parsed. So the worst case is a bridge that refuses to

View File

@@ -81,6 +81,13 @@ lines, and a duration in seconds. Re-posting the same id **replaces** that messa
The id is the useful part — give a recurring announcement a stable one and you can update or
withdraw it without waiting for it to expire.
## Client files
The other half of what the bridge carries has its own screen: creature portraits, item
pictures and the game's own name table, read from the UO client on the shard host. It is
**Client files**, at `/admin/uo/files`, and it is where an operator goes after patching that
client — nothing imports on a restart. See [Client files](/docs/administration/client-files/).
## What reaches the public
Events from the shard fan out over two separate streams, and the split is a security

View File

@@ -88,6 +88,23 @@ the tree and never compiles — and ServUO ignores the script build's exit code,
looks clean. `doctor` catches it by comparing file hashes against the install record.
</Aside>
## The bestiary has no pictures, or items show numbers
Those come out of the UO client on the shard host, and **nothing imports them on a
restart** — a button on **Admin → Client files** is the only thing that does. Check that
page first: it reports why rather than failing.
| What it says | What to do |
|---|---|
| Counts are zero and no import is recorded | Press *Update*. On a shard that was linked before this existed, nobody ever has. |
| *…set `AssetsEnabled` on the shard* | The asset plane is off in `Bridge.cfg`. It is a separate switch on purpose. |
| *The shard host cannot render images* | A Linux host with no `libgdiplus`. Install it and press *Update* again. Names are unaffected either way. |
| *The shard is busy with another client-file request* | Not a fault. Something ordinary holds the slot; it frees itself. |
| Pictures were fine and went blank | Check the uploads volume before anything else — the database still remembers pictures that are no longer on disk, and *Re-import everything* is the button for exactly that. |
Items reading as numbers rather than names is the same page, different section: it means the
cliloc table has not been imported. See [Client files](/docs/administration/client-files/).
## Teams are missing
Check the sync panel on **Admin → Teams** before anything else: *last success: never* with

View 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.

View File

@@ -46,18 +46,34 @@ 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 — **8**, which taught the bridge to carry a game's own client files — touched four
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 extractors and the decoders they call, the switches and caps in `Bridge.cfg`, and `overlay.toml` |
| `link` | `PROTOCOL_VERSION`, a cap on how large a line the shard may send, and the endpoints that carry the new commands |
| `module-uo` | The importers, the admin screen, and the pages that render a picture |
| `docs` | The protocol document and the integration guide |
**`website` is not on that list, and its absence is the interesting part.** Core holds no
game connection and names no game noun, so most protocol bumps do not reach it at all. The
one before this did, because what changed then was not a game *noun* but the shape of a
thing core owns the ledger for. This one did not reach core because everything it needed —
somewhere to put a picture — core already offered every module. Its entire share of eight
phases of work was a **deletion**: a developer tool it no longer needed.
**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, 7 and 8 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.
Version 8 puts it more sharply still. It is the largest bump this protocol has had, and it
moves megabytes of artwork rather than events — and it changed **no line** of the sidecar's
store, because the things it carries are answers to requests rather than events to keep. A
forwarder that holds no opinion about what it forwards has nothing to migrate.
## This is not the module API version
@@ -93,9 +109,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/v8.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.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/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v7.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

View File

@@ -68,7 +68,7 @@ responsibility. The sidecar is a **dumb forwarder** — it makes no access-contr
and holds no policy. Access control and the admin-toggleable visibility scope live on the
**website**, where an administrator can see and change them.
## Two ways in
## Three ways in
**Live events** arrive over an outbound **WebSocket** and are routed by the module's ingest
dispatcher. Kinds are handled differently by nature: state-changing kinds update tables,
@@ -77,6 +77,23 @@ than accumulating history.
**Point-in-time reads and commands** go over **REST**, through a client that never throws.
**Bulk reads** — a game's own client artwork, its string table, its spawn files — are the
newest and the least obvious. They go over the request/reply path in **pages**, with **one
request in flight at a time** and a hard cap on how large a single line may be.
<Aside type="note" title="Why bulk data must not ride the event stream">
It is the tempting shortcut, and it is wrong for a structural reason rather than a
performance one: the sidecar **persists every event and broadcasts it to every connected
client**. That is exactly what you want for "a house went IDOC" and exactly what you do not
want for hundreds of megabytes of artwork, which is an *answer to a question somebody
asked* rather than news.
Sending it as replies instead is what let the same bump move megabytes without the sidecar's
store changing by a line. The single slot is the other half: it is what keeps the queue
between the game and the writer thread shallow, so rule 2 above still holds while a
transfer is running.
</Aside>
Every call carries `Authorization: Bearer <token>` and an `X-UOLink-Version` header. **A
protocol mismatch fails fast with `409`** rather than being mis-parsed — see [Protocol
versions](/docs/architecture/protocol-versions/).
@@ -95,7 +112,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

View File

@@ -36,11 +36,20 @@ the [`uo` module](/docs/getting-started/install-a-game-module/) and the installe
|---|---|
| **A working ServUO install** | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. |
| **ServUO {platform.bundle.servuoMin}** *(patch tier only)* | The base install works on any reasonably current ServUO. The optional patch tier is written and tested against stock {platform.bundle.servuoMin}; on any other version it is unsupported, and skipping it still leaves you with a working bridge. |
| **`libgdiplus`** *(Linux shard hosts only)* | Only needed for **artwork**. ServUO runs under Mono on Linux, and the library it decodes sprites with is a thin layer over this one — in the decode path, not merely the encode. Without it creature portraits and item pictures stay empty, and names and the spawn atlas are unaffected because neither touches a pixel. `sudo apt-get install libgdiplus`; `doctor` checks for it. **Windows hosts need nothing.** |
| **The shard stopped** | `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit. The installer refuses to deploy under a running shard. |
| **Administrator / root** | It writes into system directories and registers a service. |
| **Outbound HTTPS** | To fetch the bundle and its two artifacts. No Gitea account and no git client are needed. |
| **The sidecar on the same host as the shard** | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — that loopback socket *is* the trust boundary for inbound commands. |
<Aside type="note" title="You do not need to install a game client for this">
You already have one. A ServUO shard cannot boot without a UO client — it resolves one at
startup to read the world's own data — so the artwork, the animations and the name table
the site shows are already sitting on that host. The shard reads them there and hands the
results over the bridge; nothing is converted on a desktop and nothing is uploaded. See
[Client files](/docs/administration/client-files/).
</Aside>
<Aside type="caution" title="Back up before the shard install">
The overlay overwrites `Scripts/Scripts.csproj`, a stock file, and the optional patch tier
edits stock sources. A copy of `Scripts/` and `Config/` costs nothing and is the difference

View File

@@ -86,11 +86,18 @@ So verify each link in the chain, in order. Each check tells you which one to fi
<Aside type="note" title="`runicgateway doctor` answers most of this in one command">
Run on the shard host, it checks the install record, the ServUO tree, every overlay file
hash, the patch tier, the sidecar, its service, `/health`, and that the sidecar and overlay
agree on a protocol. Its output is the first thing anyone helping you will ask for. It
hash, the patch tier, the sidecar, its service, `/health`, that the sidecar and overlay
agree on a protocol, and — on Linux — that the host can decode an image at all. Its output is the first thing anyone helping you will ask for. It
exits non-zero when a check failed, so a monitoring system can run it too.
</Aside>
<Aside type="note" title="A bridge can be green and still show no artwork">
Creature portraits and item pictures are a separate switch and a separate import, so a
perfectly healthy bridge shows a bestiary of text until somebody presses *Update* on
**Admin → Client files**. That is the expected first-run state, not a fault — see [Client
files](/docs/administration/client-files/).
</Aside>
## What "working" looks like a week later
- The public shard page shows live status, and the admin dashboard shows events arriving.

View File

@@ -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

View File

@@ -45,7 +45,11 @@ 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 `v8.md` for the current
protocol — which is also where the asset plane is specified.
- **"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`.

View File

@@ -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.

View File

@@ -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',
@@ -222,7 +232,8 @@ const gameIntelligence = {
detail:
'Every player vendor on the server and what is on it, searchable without logging ' +
'in to the game. Item names arrive from the world as numeric ids and are resolved ' +
"against the game's own string table, so they read as names rather than numbers.",
"against the game's own string table, so they read as names rather than numbers — " +
"beside the item's own picture, in the colour it was dyed.",
},
{
label: 'Houses and IDOC decay',
@@ -239,7 +250,8 @@ const gameIntelligence = {
detail:
"A bestiary and spawn map built by reading your shard's own spawn tables, so it " +
"describes your server rather than someone else's idea of the game. Regions, " +
'landmarks and champion altars come with it.',
'landmarks and champion altars come with it, and each creature is shown as the ' +
'artwork your own client draws it with.',
},
{
label: 'Champion boards',
@@ -306,6 +318,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 deployments 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:

View File

@@ -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 operators 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',
},
];

View File

@@ -12,23 +12,23 @@
"wrong protocol number in the first place."
],
"verifiedOn": "2026-09-01",
"verifiedOn": "2026-09-15",
"protocol": 5,
"protocol": 8,
"moduleApi": "1.9.0",
"moduleApi": "1.10.0",
"bundle": {
"tag": "2026.09.01",
"sidecar": "v2.1.0",
"overlay": "v1.1.0",
"tag": "2026.09.15",
"sidecar": "v2.3.0",
"overlay": "v1.3.0",
"servuoMin": "57.4"
},
"releases": {
"link": "v2.1.0",
"installer": "v0.1.1",
"Module-uo": "v1.1.0",
"link": "v2.3.0",
"installer": "v0.2.0",
"Module-uo": "v1.3.0",
"Android-app": "v0.5.0"
},

View File

@@ -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',
@@ -158,6 +201,33 @@ export const bridgeCfg = {
AccountNameMaxLength: 'Account name cap',
AccountPasswordMaxLength: 'Account password cap',
},
'Client assets': {
AssetsEnabled:
'Whether the website may read the UO client files on this host \u2014 art, animations, ' +
'the string table \u2014 over the link at all. Its own switch, because it is its own consent',
AssetBatchBytes:
'Byte budget for one reply page, inside the 1 MiB line cap the sidecar accepts',
AssetBodyBatch:
'Bodies one catalogue request may name. Counted in items rather than bytes, because ' +
'what it bounds is building and deleting that many real mobiles on the core thread. ' +
'A larger request is refused, never truncated',
AssetFetchKeys:
'How many keys one fetch may name. The byte budget above still decides where a page is cut',
AssetScanMs: 'How long a catalogue scan may run before the page it has is returned',
AssetPlayerDirection:
'Which of the five directions a player body renders as. 0 is head-on, facing the viewer',
AssetCreatureDirection:
'The same for everything else. 1 is the front three-quarter \u2014 a wolf seen head-on ' +
'is a dark blob',
},
'Spawn files': {
TreeEnabled:
'Whether the shard configuration itself \u2014 spawn files, regions, locations, champion ' +
'spawns, decoration \u2014 may cross the bridge. A third switch for a third consent: ' +
'this is the work of the operator rather than the game client. Off means the spawn ' +
'atlas needs a shared filesystem again',
TreeChunkBytes: 'How large a slice of one file may be before it is compressed and sent',
},
};
/**
@@ -175,12 +245,14 @@ 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/v8.md': 'The current protocol, and its cross-repository obligations',
'link/SHARD_PREREQS.md': 'What a shard host needs before any of this works',
'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",

View File

@@ -34,7 +34,8 @@
* ---------------------------------------------------------------------------------------
* A demo deployment of this platform, wired to a real ServUO shard over a real sidecar
* (D42): the marketplace rows are player vendors the game actually holds, the atlas is
* parsed from the shard's own spawn files, the guild rosters came over the bridge. The
* parsed from the shard's own spawn files, the guild rosters came over the bridge, and
* since protocol 8 the artwork in both came off that host's own UO client. The
* deployment is branded "Runic Gateway Demo" rather than a real community's name (D43) —
* the screenshots show the platform, not somebody's private shard.
*
@@ -81,19 +82,19 @@ export const screens = [
route: '/uo/market',
admin: false,
scrollY: 470,
alt: 'The marketplace page, listing items for sale by player vendors with their prices, shop names and locations, above a search box and price filters.',
alt: 'The marketplace page, listing items for sale by player vendors — each row showing the item picture, its name, the shop, the seller, the location and the price — above a search box and price filters.',
caption:
'Player vendors, searchable from the website — the same index the in-game vendor search reads, honouring the same per-vendor opt-out.',
'Player vendors, searchable from the website — the same index the in-game vendor search reads, honouring the same per-vendor opt-out. The names and the pictures both come out of the client on the shard host.',
family: 'web',
},
{
id: 'spawn-atlas',
route: '/uo/atlas',
admin: false,
scrollY: 430,
alt: 'The spawn atlas, listing creatures with how many of them spawn and on which facets, above a search box and facet filters.',
scrollY: 466,
alt: 'The spawn atlas, listing creatures — sea serpent, water elemental, orc, ettin, horse, goat, sheep — each with the artwork from the game client beside how many of them spawn and on which facets.',
caption:
"The spawn atlas is parsed from the shard's own spawn files, so it stays accurate whether or not the server is up.",
"The spawn atlas is parsed from the shard's own spawn files, so it stays accurate whether or not the server is up. Every portrait was decoded on the shard host and came over the same bridge.",
family: 'web',
},
{
@@ -151,6 +152,15 @@ export const screens = [
'The shard connection, showing a live sidecar. The token is write-only: it is never sent back to any client, including this screen.',
family: 'web',
},
{
id: 'admin-client-files',
route: '/admin/uo/files',
admin: true,
alt: 'The client files screen, showing the creature portraits section: 1,095 pictures held of 1,095 catalogued, 746 of 800 creatures matched, the last import and the extractor version, above Update and Re-import everything buttons.',
caption:
'Client files, imported from the UO client on the shard host over the same bridge. Nothing here happens on a restart — these buttons are the only thing that imports.',
family: 'web',
},
{
id: 'admin-modules',
route: '/admin/modules',