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

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