feat(facts): phase 13 - per-game facts, Rust and RunicNPC checked, main green again
All checks were successful
PR checks / checks (pull_request) Successful in 8m25s

platform.json drops the single protocol/bundle/capability list for a
`games` object (uo, rust) plus a `runicnpc` block. checkFacts.mjs loops
the protocol, plugin-protocol, capability and bundle checks over both
games, reads the schema-2 bundles at v2/<game>/current.json, checks each
module's coreApi, and reads RunicNPC's plugin.toml. 41 facts, up from 19;
clears the two that were red on main (Module API 1.17.0, installer
v0.8.0).

D66: the hero, footer and structured data show the Module API only; each
game's protocol and bundle go on that game's pages, and the docs landing
table splits into shared versions plus one column per game.

The re-read section 1 requires found, and this fixes:
- three hard-coded "1.6.0" Module API versions in the module docs
- module-lifecycle saying modules load alphabetically (the loader now
  refuses a second module, W19), and the install pages never mentioning
  the one-module rule
- "there is one module" in five places, now two modules both built
  inside this project; the Integration Kit keeps its draft banner
- the homepage diagram's ServUO/uo-link sub-labels (D63 wording now)

notBuilt drops `second-module` (resolved by Module-Rust's release) and
rewords `multi-module`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-22 08:09:22 -05:00
parent 2971b84d51
commit 9938e71e07
29 changed files with 384 additions and 154 deletions

View File

@@ -62,8 +62,9 @@ const isExternal = (href: string) => href.startsWith('http');
</span>
</p>
<p class="site-footer__meta">
<span class="chip chip--version">Protocol {platform.protocol}</span>
<span class="chip chip--version">Bundle {platform.bundle.tag}</span>
{/* D66, as on the hero: the shared version only. Each game's protocol and bundle
are on that game's pages. */}
<span class="chip chip--version">Module API {platform.moduleApi}</span>
</p>
</div>
</div>

View File

@@ -53,7 +53,9 @@ const application = {
// client. Not "Windows" — the installer runs there, the platform does not require it.
operatingSystem: 'Linux, Windows, Android',
license: 'https://www.gnu.org/licenses/gpl-3.0.html',
softwareVersion: platform.bundle.tag,
// D66: the Module API is the one version every deployment shares. Each game has its own
// bundle, so no single bundle tag describes the platform any more.
softwareVersion: platform.moduleApi,
publisher: { '@id': url('/#organization') },
// Self-hosted and free, and `offers` is the only way the vocabulary can say so. Omitting
// it reads as "price unknown"; stating zero is simply true.

View File

@@ -8,9 +8,11 @@ import platform from '../../data/platform.json';
* The other two draw runtime shapes. This one draws a code boundary, and it is here because
* it is the claim the whole project rests on: that a community platform can be built once
* and pointed at any game. An evaluator has every reason to read that as marketing, so the
* page draws the seam and then says plainly what does and does not prove it — one module
* exists, the second is a paper exercise, and the exit criterion for calling the contract
* proven is written down (§2, and the entries `/modules/` renders from `notBuilt.mjs`).
* page draws the seam and then says plainly what does and does not prove it — two modules
* exist, both built inside this project, and the exit criterion for calling the contract
* proven by an outsider is written down (§2, §16, and the entries `/modules/` renders from
* `notBuilt.mjs`). Until phase 13 this said one module and a paper exercise; Module-Rust's
* release made that false.
*
* The Module API version is read from `platform.json` like every other number on this site
* (§12). It is the one place a version genuinely belongs in this diagram: the seam is
@@ -73,7 +75,7 @@ import platform from '../../data/platform.json';
<!-- The module: everything that knows a game exists. -->
<rect class="host" x="8" y="306" width="364" height="186" rx="14" />
<text class="host-title" x="28" y="340">Game module</text>
<text class="host-sub" x="28" y="360">one per deployment; UO today</text>
<text class="host-sub" x="28" y="360">one per deployment; UO or Rust today</text>
<rect class="node" x="28" y="378" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="406">Routes</text>
@@ -130,11 +132,12 @@ import platform from '../../data/platform.json';
<section>
<h3>What this does not yet prove</h3>
<p>
One module exists and it is Ultima Online. A second, for a different game, is a
written dry-run that was deliberately never implemented — it exists to test whether
the contract generalises on paper. Until somebody builds the second one, the seam is
a well-argued design rather than a demonstrated one, and this site says so wherever
it comes up.
Two modules exist: Ultima Online, and Rust, which this project built second, from
the Integration Kit. Where the Rust module needed something the contract did not
yet offer, the gap was closed in core, for every module, rather than in a fork — so
the seam now has two games on its far side instead of one. What two modules built
inside this project cannot prove is that somebody outside it can do the same, and
that is why the Integration Kit still says draft.
</p>
</section>
</div>

View File

@@ -17,7 +17,7 @@ import { capabilityGroups, assertCapabilityCoverage } from '../../data/capabilit
* what exists while quietly omitting the well-known things that do not is the exact failure
* §1 is written to prevent.
*/
assertCapabilityCoverage(platform.moduleUoCapabilities);
assertCapabilityCoverage(platform.games.uo.module.capabilities);
---
<section class="page section caps">
@@ -52,7 +52,7 @@ assertCapabilityCoverage(platform.moduleUoCapabilities);
<p class="caps__foot prose">
Some things people reasonably expect are <strong>deliberately not built</strong> — a Matrix
integration, more than one game module active at once, a second game module. They are
integration, more than one game module active at once, a finished Integration Kit. They are
listed rather than left out, on <a href="/features/">features</a> and{' '}
<a href="/integrations/">integrations</a>.
</p>

View File

@@ -1,12 +1,10 @@
---
import platform from '../../data/platform.json';
/**
* The data path (PLAN.md §13 phase 3), drawn as inline SVG per §11's motif rule — hand-drawn
* geometry, used where it explains something, and no raster anywhere.
*
* ---------------------------------------------------------------------------------------
* THE LABELS ARE GENERIC, WITH UO AS THE CAPTION
* THE LABELS ARE GENERIC; THE CAPTION NAMES THE GAMES
* ---------------------------------------------------------------------------------------
* The org lead settled this before the diagram was drawn. The nodes say "your game server"
* and "sidecar", not "ServUO shard" and "uo-link", because §10's rule is that a reader
@@ -14,10 +12,12 @@ import platform from '../../data/platform.json';
* repositories in order to connect a game server — and because the tagline promises a
* platform, not a UO product.
*
* It does NOT hide what actually ships. The sub-labels and the caption name ServUO and
* uo-link outright, because §1 says the technical truth wins and today there is exactly one
* implementation of this shape. An operator running a shard has to see themselves in the
* picture on the first screen.
* It does NOT hide what actually ships: the caption names the games that ship on this shape,
* because §1 says the technical truth wins and an operator has to see their game in the
* picture on the first screen. Until phase 13 the sub-labels named ServUO and uo-link, which
* was true while there was exactly one implementation. With two, naming one would mislead,
* and D63 (PLAN.md §16) keeps each game's vocabulary on that game's own pages — so the
* caption names the games and the diagram names nothing else.
*
* ---------------------------------------------------------------------------------------
* WHY THE SVG IS aria-hidden
@@ -74,11 +74,11 @@ import platform from '../../data/platform.json';
<rect class="node" x="20" y="16" width="340" height="76" rx="12" />
<text class="node-title" x="42" y="50">Your game server</text>
<text class="node-sub" x="42" y="72">ServUO today · opens no inbound port</text>
<text class="node-sub" x="42" y="72">opens no inbound port</text>
<rect class="node" x="20" y="148" width="340" height="76" rx="12" />
<text class="node-title" x="42" y="182">Sidecar</text>
<text class="node-sub" x="42" y="204">uo-link · the only network-facing part</text>
<text class="node-sub" x="42" y="204">the only network-facing part</text>
<rect class="node node--self" x="20" y="280" width="340" height="76" rx="12" />
<text class="node-title" x="42" y="314">Runic Gateway</text>
@@ -90,8 +90,9 @@ import platform from '../../data/platform.json';
</svg>
<p class="datapath__caption">
Today that game server is a ServUO shard and that sidecar is uo-link. The shape is the
contract; the implementations are what plug into it.
Two games ship on this shape today, Ultima Online and Rust, each with its own plugin
and its own sidecar. The shape is the contract; the implementations are what plug
into it.
</p>
</div>
@@ -109,8 +110,8 @@ import platform from '../../data/platform.json';
<h3>The sidecar</h3>
<p>
A small service beside the game, and the only piece of the bridge anything else can
reach. It speaks a versioned wire protocol — protocol {platform.protocol} today — so
a mismatched pair is refused rather than misread, and it answers only your website's
reach. It speaks a versioned wire protocol — each game pins its own — so a
mismatched pair is refused rather than misread, and it answers only your website's
backend, over an authenticated WebSocket and REST.
</p>
</li>

View File

@@ -55,9 +55,9 @@ import platform from '../../data/platform.json';
</div>
<div class="chips">
<span class="chip chip--version">Protocol {platform.protocol}</span>
{/* D66: only the version every deployment shares. A protocol and a bundle belong
to a game, so they live on that game's pages rather than on the front door. */}
<span class="chip chip--version">Module API {platform.moduleApi}</span>
<span class="chip chip--version">Bundle {platform.bundle.tag}</span>
</div>
</div>

View File

@@ -41,6 +41,10 @@ Two consequences of how boots work:
Paste the new release's install-manifest URL and press Install. The bundle is verified
against its `sha256`, unpacked over the old one, and takes effect at the restart.
**A site runs one module.** Installing a module with a different id beside the one you have
is refused with `409`, and nothing is downloaded. To change game, remove the installed module
first.
An upgrade **deliberately leaves the state alone**: upgrading an enabled module must not
silently switch it off, and re-installing a disabled one must not silently switch it on.

View File

@@ -26,7 +26,7 @@ on:
| **Base URL (REST)** | `http://<shard host>:8080` — point-in-time queries |
| **WebSocket URL (feed)** | `ws://<shard host>:8080/ws` — the live event feed |
| **Auth token** | The sidecar's token |
| **Protocol** | {platform.protocol} today |
| **Protocol** | {platform.games.uo.protocol} today |
Saving restarts the ingest client, so a change takes effect immediately.

View File

@@ -10,7 +10,11 @@ The loopback wire protocol between the game plugin and the sidecar is a **versio
compatibility contract**, not a build dependency. Nothing compiles the three sides together,
so the number is what stops a mismatch from being discovered as corrupted data.
The current protocol is **{platform.protocol}**.
Each game has its own protocol, numbered independently: {platform.games.uo.name} is on
**{platform.games.uo.protocol}** and {platform.games.rust.name} on
**{platform.games.rust.protocol}**. The two numbers are not comparable — they version two
different wires. The examples below are the {platform.games.uo.name} bridge's; the
{platform.games.rust.name} bridge declares its number in the same three kinds of place.
## Three declaration sites
@@ -18,8 +22,8 @@ The same number is written down in three places, and they must move together.
| Where | What declares it |
|---|---|
| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION`, currently {platform.protocol} — what the sidecar speaks |
| `servuo-plugins/overlay.toml` | `protocol`, currently {platform.protocol} — what the plugin overlay speaks |
| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION`, currently {platform.games.uo.protocol} — what the sidecar speaks |
| `servuo-plugins/overlay.toml` | `protocol`, currently {platform.games.uo.protocol} — what the plugin overlay speaks |
| The bundle manifest | Copied from `overlay.toml` by CI, so a released pair carries its own claim |
<Aside type="caution" title="Bump the overlay in the same PR as the emitters">

View File

@@ -71,7 +71,7 @@ internet; only the sidecar is exposed, and only to your site.
<Aside type="note" title="It installs a bundle, not “latest of each”">
The three components version independently but must agree on one wire protocol, so what it
resolves is a **bundle**: an exact, protocol-checked pair of sidecar and overlay versions
({platform.bundle.tag} today — sidecar {platform.bundle.sidecar}, overlay {platform.bundle.overlay}).
({platform.games.uo.bundle.tag} today — sidecar {platform.games.uo.bundle.sidecar}, overlay {platform.games.uo.bundle.overlay}).
`--bundle <tag>` pins an exact past combination, so a reinstall in six months reproduces
today's install rather than tomorrow's.
</Aside>
@@ -83,7 +83,7 @@ features need edits to stock ServUO sources, and those are opt-in, off unless yo
and refused where the target lines are not stock. Skipping the tier costs you vendor-sale
events and in-game moderation audit forwarding; everything else works.
The tier is written and tested against stock ServUO {platform.bundle.servuoMin}. On any
The tier is written and tested against stock ServUO {platform.games.uo.bundle.servuoMin}. On any
other version it is unsupported and untested, and the prompt makes you answer past a
warning.

View File

@@ -60,7 +60,7 @@ Watch the log at the restart and you will see exactly what it mounted:
[modules] module "uo" started
```
Its capabilities are {platform.moduleUoCapabilities.join(', ')} — the shard console, the
Its capabilities are {platform.games.uo.module.capabilities.join(', ')} — the shard console, the
map atlas, the player-vendor marketplace, city governors, guilds, houses and IDOCs, champion
boards, and the cliloc strings that make item names readable.

View File

@@ -35,7 +35,7 @@ the [`uo` module](/docs/getting-started/install-a-game-module/) and the installe
| Requirement | Detail |
|---|---|
| **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. |
| **ServUO {platform.games.uo.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.games.uo.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. |

View File

@@ -15,19 +15,32 @@ public event stream.
## What the platform is on today
Two versions are shared by every deployment, whichever game it runs:
<table>
<tbody>
<tr><td>Wire protocol</td><td>{platform.protocol}</td></tr>
<tr><td>Module API</td><td>{platform.moduleApi}</td></tr>
<tr><td>Current bundle</td><td>{platform.bundle.tag}</td></tr>
<tr><td>uo-link sidecar</td><td>{platform.releases.link}</td></tr>
<tr><td>Plugin overlay</td><td>{platform.bundle.overlay}</td></tr>
<tr><td>Installer</td><td>{platform.releases.installer}</td></tr>
<tr><td>ServUO (minimum)</td><td>{platform.bundle.servuoMin}</td></tr>
</tbody>
</table>
Every value in that table is read from `src/data/platform.json` and re-checked against its
Everything else belongs to a game. Each game has its own wire protocol and its own bundle —
the exact, protocol-checked pair of sidecar and game plugin the installer deploys:
<table>
<thead>
<tr><th></th><th>{platform.games.uo.name}</th><th>{platform.games.rust.name}</th></tr>
</thead>
<tbody>
<tr><td>Game module</td><td>{platform.releases['Module-uo']}</td><td>{platform.releases['Module-Rust']}</td></tr>
<tr><td>Wire protocol</td><td>{platform.games.uo.protocol}</td><td>{platform.games.rust.protocol}</td></tr>
<tr><td>Current bundle</td><td>{platform.games.uo.bundle.tag}</td><td>{platform.games.rust.bundle.tag}</td></tr>
<tr><td>Sidecar</td><td>{platform.games.uo.bundle.sidecar}</td><td>{platform.games.rust.bundle.sidecar}</td></tr>
<tr><td>Game plugin</td><td>{platform.games.uo.bundle.overlay}</td><td>{platform.games.rust.bundle.plugin}</td></tr>
</tbody>
</table>
Every value in those tables is read from `src/data/platform.json` and re-checked against its
source of truth on each build. None of it is typed into prose — including here.
## An install is two installs
@@ -39,8 +52,8 @@ two independent deployments.
2. **The shard side** is the installer binary, run on the game server's host. It sets up
the plugin overlay and the sidecar, and it never contacts the website.
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.
They meet at the values the installer prints, pasted into the game module's connection
screen, and at the game's wire 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, moderation and the event

View File

@@ -29,6 +29,13 @@ Two artifacts:
The manifest URL is the thing an operator pastes. The bundle is downloaded, **verified
against the `sha256`**, and unpacked into `modules/<id>/` on the mounted volume.
<Aside type="caution" title="A site runs one module">
An install whose `id` differs from a module already on the volume is refused with `409`
before anything is downloaded. Upgrading the installed module is the only install a
populated site accepts; to change game, remove the module first. The loader enforces the
same rule at boot — see [Module lifecycle](/docs/modules/module-lifecycle/#the-scan).
</Aside>
Nothing is compiled at any point in that sequence.
## The two surfaces

View File

@@ -18,10 +18,17 @@ time**. The database is not consulted: what is on the volume determines what mou
missing modules directory is not an error** — "no modules installed" is the normal state of
bare core, and the loader must not make the mount mandatory to boot.
Modules load **alphabetically by `id`**, deterministically. There is no dependency
resolution between modules, and alphabetical order is the honest way of saying so: any
other order would imply a precedence nobody is computing. Do not build a module that needs
to load before or after another one.
**A site runs one module.** The scan is alphabetical by directory name, and the first
module directory it finds holds the site, whether or not that module then loads. Every
other module directory is recorded `startup_failed` at the manifest stage **before any of
its code is required**: no routes, navigation, tables or registrations. Its reason, in the
log and on **Admin → Modules**, names both modules and says to remove one. The installer
refuses the same thing earlier — an install whose id differs from the module already on the
volume answers `409` before anything is downloaded — so the loader's rule is what catches a
directory copied onto the volume by hand.
There is no dependency resolution between modules, because there is only ever one running.
Do not build a module that expects another to load beside it.
## Validation, in order

View File

@@ -49,7 +49,9 @@ your game server ──dials out──▶ your sidecar ──HTTP + WS──
Before any of them, the kit points at the [Rust dry
run](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust-dryrun.md)
— a complete module designed on paper for a second game, and the shortest honest picture of
the whole job.
the whole job. That module has since been built for real, as
[`Module-Rust`](https://gitea.whitlocktech.com/RunicGateway/Module-Rust), so the dry run now
reads as the plan beside a finished result.
## The template is built, not just quoted

View File

@@ -4,6 +4,7 @@ description: The two arguments core hands your module — what you can reach, wh
---
import { Aside } from '@astrojs/starlight/components';
import platform from '../../../../data/platform.json';
Your server entry point exports one function:
@@ -14,7 +15,7 @@ module.exports = function register(ctx, api) { /* … */ }
`ctx` is what core lends you. `api` is what you register with it. Everything crossing the
module boundary goes through one of the two.
The contract version is **`MODULE_API_VERSION`**, currently **1.6.0**, and your manifest's
The contract version is **`MODULE_API_VERSION`**, currently **{platform.moduleApi}**, and your manifest's
[`coreApi` range](/docs/modules/the-module-manifest/#coreapi-and-what-a-range-means) is
checked against it before your code is required.

View File

@@ -4,6 +4,7 @@ description: Every key in module.json, what the loader does with each, and why a
---
import { Aside } from '@astrojs/starlight/components';
import platform from '../../../../data/platform.json';
`module.json` sits at the root of your bundle. The loader reads it synchronously, before
anything else about your module runs.
@@ -78,12 +79,14 @@ repository is branching on them.
## `coreApi` and what a range means
Core exports a single semver string, currently **1.6.0**. Your range is checked at boot,
before your code is required.
Core exports a single semver string, currently **{platform.moduleApi}**. Your range is
checked at boot, before your code is required.
A **minor** bump adds members without removing any or changing a signature, so `^1.3.0`
keeps resolving against 1.6.0 — which is exactly why `module-uo` still declares `^1.3.0`
and runs fine.
A **minor** bump adds members without removing any or changing a signature, so an older
caret range keeps resolving against a newer core. The {platform.games.uo.name} module
declares <code>{platform.games.uo.module.coreApi}</code> and runs on {platform.moduleApi};
the {platform.games.rust.name} module declares
<code>{platform.games.rust.module.coreApi}</code>.
Use a caret range against the oldest core you actually support and test against. Pinning
exactly buys nothing and strands you on the next additive release.

View File

@@ -23,7 +23,7 @@
* capability slugs the module actually declares in its `module.json`, and
* `assertCapabilityCoverage()` fails the build if the two lists drift apart.
*
* That closes a real gap. `platform.json` holds `moduleUoCapabilities` and
* That closes a real gap. `platform.json` holds `games.uo.module.capabilities` and
* `scripts/checkFacts.mjs` re-reads it from the module's manifest on every build — so the
* day `module-uo` gains a capability, the JSON goes red and someone updates it. Before this
* function, updating the JSON was the end of it and the page kept the old list. Now the
@@ -188,8 +188,9 @@ const gameIntelligence = {
title: 'Game intelligence',
moduleSupplied: true,
summary:
'Supplied by the installed game module, not by the core site. Today that module is ' +
'module-uo, and this is what it publishes from a live shard.',
'Supplied by the installed game module, not by the core site. Two modules exist, for ' +
'Ultima Online and for Rust; this is what the Ultima Online module publishes from a ' +
'live game server, and the Rust module brings its own set in its place.',
items: [
{
label: 'Live server status',
@@ -490,7 +491,7 @@ export function assertCapabilityCoverage(declared) {
}
throw new Error(
`src/data/capabilities.mjs disagrees with platform.json's moduleUoCapabilities:\n` +
`src/data/capabilities.mjs disagrees with platform.json's games.uo.module.capabilities:\n` +
lines.map((line) => ` - ${line}`).join('\n') +
`\n\nUpdate the "Game intelligence" items, or the JSON if the module itself changed.\n`
);

View File

@@ -78,26 +78,18 @@ export const notBuilt = [
scope: ['features', 'integrations', 'modules'],
title: 'More than one game module at a time',
body:
'One active module per deployment. The database columns that would scope data to a ' +
'module exist and are populated, so the door is not nailed shut, but nothing ' +
'exercises them and no interface offers it. A community running two games runs two ' +
'deployments.',
'One module per deployment, and the site enforces it: it loads one module at boot ' +
'and refuses every other, with a message saying so, rather than letting two games ' +
'share one site. The database columns that would scope data to a module exist and ' +
'are populated, so the door is not nailed shut, but no interface offers it. A ' +
'community running two games runs two deployments.',
resolvedBy:
'Someone needing it. The schema was shaped to keep it possible, which is a different ' +
'thing from planning it.',
},
{
id: 'second-module',
scope: ['integrations', 'modules'],
title: 'A second game module',
body:
'There is exactly one, and it is Ultima Online. A paper dry-run for a Rust module ' +
'exists and is deliberately unimplemented — it was written to test whether the ' +
'module contract generalises, not to ship. Until a second one exists, "any game" is ' +
'an argument about a shape rather than a demonstration.',
resolvedBy: 'The first module built for a game that is not Ultima Online.',
link: { href: `${GITEA}/docs/src/branch/main/modules/rust-dryrun.md`, label: 'The dry-run' },
},
// `second-module` was here until phase 13 (PLAN.md §16): "a second game module", resolved
// by exactly the event its own `resolvedBy` named — the Rust module's release. Removed
// rather than reworded, because an absence that is no longer absent is not an entry.
{
id: 'integration-kit-draft',
scope: ['integrations', 'modules'],

View File

@@ -12,24 +12,28 @@
"wrong protocol number in the first place."
],
"verifiedOn": "2026-09-15",
"verifiedOn": "2026-10-22",
"protocol": 8,
"moduleApi": "1.10.0",
"bundle": {
"tag": "2026.09.15",
"sidecar": "v2.3.0",
"overlay": "v1.3.0",
"servuoMin": "57.4"
},
"$comment_moduleApi": [
"The one version every deployment shares, whichever game it runs. D66: it is the only",
"version the homepage hero and the footer show — a protocol and a bundle belong to a game,",
"so they appear on that game's own pages."
],
"moduleApi": "1.17.0",
"$comment_releases": [
"Every repository whose release the site quotes, flat, so checkFacts.mjs reads them all",
"the same way. Which game a repository belongs to is said once, under `games` below."
],
"releases": {
"installer": "v0.8.0",
"Android-app": "v0.5.0",
"link": "v2.3.0",
"installer": "v0.2.0",
"Module-uo": "v1.3.0",
"Android-app": "v0.5.0"
"Rust-Link": "v0.3.0",
"Rust-Plugins": "v0.3.0",
"Module-Rust": "v0.5.0",
"runicnpc-rust": "v1.0.0"
},
"$comment_website": [
@@ -40,16 +44,70 @@
],
"websiteHasReleases": false,
"moduleUoCapabilities": [
"shard",
"atlas",
"market",
"governors",
"guilds",
"houses",
"champs",
"cliloc"
"$comment_games": [
"PLAN.md §16. One entry per game the platform supports. Each game has its own wire",
"protocol (declared by its sidecar AND its game-side plugin, and re-read from both) and",
"its own bundle on the installer's `bundles` branch, at `v2/<bundleGame>/current.json`.",
"The root `current.json` is the schema-1 manifest and is no longer read: it stopped",
"moving when the installer went to one bundle per game.",
"",
"`module.capabilities` is re-read from the module's own module.json, unsorted order being",
"the module's business. `name` is the game's own name — D63 lets the site at large say it."
],
"games": {
"uo": {
"name": "Ultima Online",
"bundleGame": "servuo",
"protocol": 8,
"module": {
"repo": "Module-uo",
"coreApi": "^1.10.0",
"capabilities": ["shard", "atlas", "market", "governors", "guilds", "houses", "champs", "cliloc"]
},
"sidecarRepo": "link",
"pluginRepo": "servuo-plugins",
"bundle": {
"tag": "2026.09.15",
"sidecar": "v2.3.0",
"overlay": "v1.3.0",
"servuoMin": "57.4"
}
},
"rust": {
"name": "Rust",
"bundleGame": "rust",
"protocol": 14,
"module": {
"repo": "Module-Rust",
"coreApi": "^1.17.0",
"capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"]
},
"sidecarRepo": "Rust-Link",
"pluginRepo": "Rust-Plugins",
"bundle": {
"tag": "2026.10.22",
"sidecar": "v0.3.0",
"plugin": "v0.3.0",
"oxideMin": "2.0.7585",
"carbonMin": "2.0.259",
"requiresPlugins": ["Kits", "ZoneManager"],
"runicnpc": "v1.0.0"
}
}
},
"$comment_runicnpc": [
"RunicNPC — the Rust NPC plugin (PLAN.md §16, D61). It ships in the Rust bundle and also",
"stands alone, so it has facts of its own: `api` is the version a calling plugin relies",
"on, declared in runicnpc-rust's plugin.toml and re-read from there and from the bundle.",
"Its framework floors are its own and differ from the bridge's."
],
"runicnpc": {
"api": 6,
"oxideMin": "2.0.7726",
"carbonMin": "2.0.259",
"requiresPlugins": ["Kits"]
},
"androidApplicationId": "com.runicgateway.app",

View File

@@ -256,7 +256,7 @@ export const canonicalDocs = {
'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",
'modules/rust-dryrun.md': 'A second module designed on paper, to test that the contract generalises',
'modules/rust-dryrun.md': 'The Rust module as first designed on paper, to test that the contract generalises — since built',
'modules/uo/API.md': "module-uo's own API, including its audience rules",
'android/PLAN.md': 'The Android app',
};

View File

@@ -83,8 +83,8 @@ const docs = `${platform.gitea.base}/${platform.gitea.org}/docs/src/branch/main`
</a>
<span
>What the game and the sidecar say to each other, and what the sidecar publishes.
Protocol {platform.protocol} today, and versioned so a mismatched pair is refused
rather than misread.</span
Each game pins its own protocol version, so a mismatched pair is refused rather
than misread.</span
>
</li>
<li>

View File

@@ -135,10 +135,9 @@ const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
<li>
<h3>Build a module for another game</h3>
<p>
There is one module and it is Ultima Online, so the claim that this platform is
game-agnostic is currently an argument rather than a demonstration. The
<a href="/modules/">Integration Kit</a> exists to be followed by somebody outside
this project — and it stays marked draft until it has been.
There are two modules, Ultima Online and Rust, and both were built inside this
project. The <a href="/modules/">Integration Kit</a> exists to be followed by
somebody outside it — and it stays marked draft until it has been.
</p>
</li>
<li>

View File

@@ -53,7 +53,7 @@ import {
* matches bytes, and a stock build hides every one of these links, so a mistake here is
* invisible until the day somebody configures a demo.
*/
assertCapabilityCoverage(platform.moduleUoCapabilities);
assertCapabilityCoverage(platform.games.uo.module.capabilities);
assertDetailCoverage();
/**
@@ -91,9 +91,10 @@ const description =
>.
</p>
<p>
Today there is one module and it covers Ultima Online, so the second group below is
what a UO deployment gets. On a deployment with no module, that group is simply absent
and the other four are unchanged.
Two modules exist today, for Ultima Online and for Rust, and a deployment runs one of
them. The second group below is what the Ultima Online module adds; the Rust module
brings its own set in its place. On a deployment with no module, that group is simply
absent and the other four are unchanged.
</p>
</PageHeader>

View File

@@ -201,7 +201,7 @@ const integrations = [
server, and an installed module merges its own routes into it — so whatever you build
against is documented by the thing that is actually running, at
{' '}Module API {platform.moduleApi}. The bridge to a game server is a documented wire
protocol on the same principle, currently protocol {platform.protocol}.
protocol on the same principle, versioned per game.
</p>
<div class="integ__actions">
<a class="btn btn--primary" href="/modules/">How modules work</a>