From 9938e71e07d25a44bbeae597fa2dcac2c32f9785 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 22 Oct 2026 08:09:22 -0500 Subject: [PATCH] feat(facts): phase 13 - per-game facts, Rust and RunicNPC checked, main green again 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//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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- PLAN.md | 58 ++++++- scripts/checkFacts.mjs | 159 +++++++++++++----- scripts/seedDemo.mjs | 2 +- src/components/Footer.astro | 5 +- src/components/StructuredData.astro | 4 +- src/components/architecture/ModuleSeam.astro | 21 ++- src/components/home/Capabilities.astro | 4 +- src/components/home/DataPath.astro | 27 +-- src/components/home/Hero.astro | 4 +- .../docs/administration/managing-modules.mdx | 4 + .../administration/the-shard-connection.mdx | 2 +- .../docs/architecture/protocol-versions.mdx | 10 +- .../getting-started/connect-a-game-server.mdx | 4 +- .../getting-started/install-a-game-module.mdx | 2 +- .../docs/getting-started/requirements.mdx | 2 +- src/content/docs/docs/index.mdx | 29 +++- .../docs/docs/modules/installing-modules.mdx | 7 + .../docs/docs/modules/module-lifecycle.mdx | 15 +- .../docs/docs/modules/the-integration-kit.mdx | 4 +- .../docs/docs/modules/the-module-api.mdx | 3 +- .../docs/docs/modules/the-module-manifest.mdx | 13 +- src/data/capabilities.mjs | 9 +- src/data/notBuilt.mjs | 24 +-- src/data/platform.json | 102 ++++++++--- src/data/reference.mjs | 2 +- src/pages/architecture.astro | 4 +- src/pages/community.astro | 7 +- src/pages/features.astro | 9 +- src/pages/integrations.astro | 2 +- 29 files changed, 384 insertions(+), 154 deletions(-) diff --git a/PLAN.md b/PLAN.md index d6b22db..18eaa9a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -237,7 +237,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r **Decisions after D13 are recorded where they were taken**, in the section describing the phase that raised them, rather than appended here — a decision is only re-litigated when its reasoning is -somewhere other than the thing it decided. The count of record is **sixty-five**: +somewhere other than the thing it decided. The count of record is **sixty-six**: | # | Where | What it settled | |---|---|---| @@ -255,6 +255,7 @@ somewhere other than the thing it decided. The count of record is **sixty-five** | D58 | §6, "How phase 12 delivered it" | `node_modules` ships in three layers because Cloudflare refuses a request body over 100 MB, and the workflow counts layers before it pushes | | D59 | §6, "How phase 12 delivered it" | The container publishes on every interface (amending D55), with `SITE_BIND_ADDR` to narrow it | | D60–D65 | §16, "The six decisions" | Per-game sidebar groups, RunicNPC inside Rust with its own page, the RunicNPC API carried in full with a drift check, game names allowed but game vocabulary confined, a twelfth check that enforces it (amending D53), Rust screenshots last and from the rigs | +| D66 | §16, "How phase 13 built it" | The hero and footer show the Module API only; a protocol and a bundle belong to a game | --- @@ -1609,11 +1610,12 @@ a mechanism rather than diligence: | Fact | Authority | |---|---| - | Protocol version | `link` `main:sidecar/src/main.rs` → `PROTOCOL_VERSION` | - | Overlay protocol | `servuo-plugins` `main:overlay.toml` → `protocol` | + | Protocol version, per game | the game's sidecar repo (`link`, `Rust-Link`) `main:sidecar/src/main.rs` → `PROTOCOL_VERSION` | + | Plugin protocol, per game | the game's plugin repo (`servuo-plugins`, `Rust-Plugins`) `main:overlay.toml` → `protocol` | | Module API | `website` `main:server/src/modules/version.js` → `MODULE_API_VERSION` | - | Module capability list | `Module-uo` `main:module.json` → `capabilities` (added in phase 3) | - | Bundle + component pins | `installer` branch `bundles`, **root** `current.json` | + | Module capabilities + `coreApi`, per game | `Module-uo` / `Module-Rust` `main:module.json` (capabilities added in phase 3, both games and `coreApi` in phase 13) | + | Bundle + component pins, per game | `installer` branch `bundles`, **`v2/servuo/current.json`** and **`v2/rust/current.json`** (phase 13; the root `current.json` is the frozen schema-1 manifest) | + | RunicNPC API, floors, required plugins | `runicnpc-rust` `main:plugin.toml`, and the Rust bundle's `npc` block (phase 13) | | Release versions | Gitea releases API per repo | Same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`: when the platform @@ -1974,6 +1976,52 @@ module collects, Steam ids first, before anything is claimed there. `checkTerms. **Phase 18 — screenshots** (D65), plus the emulator pass for `/app/`'s Rust screens. This is the only phase that needs the rigs. +### How phase 13 built it + +One decision, **D66**, taken 2026-10-22 when the restructure reached the homepage. The count of +record is now **sixty-six**. + +**D66 — the hero and the footer show the Module API only.** Both used to read "Protocol 8 · Bundle +2026.09.15", which was a description of the platform while there was one game. There are now two +protocols and two bundles, and neither belongs on the front door. The hero and footer show **Module +API**, the one version every deployment shares. The structured data's `softwareVersion` reads the +same value. Each game's protocol and bundle are on that game's pages, and the docs landing page +splits its table in two: shared versions, then one column per game. The alternative was per-game +chips on the hero ("Ultima Online: protocol 8, bundle … · Rust: protocol 14, bundle …"). It was +declined as busier and as putting game facts on the front door. + +**`platform.json` is per game.** `protocol`, `bundle` and `moduleUoCapabilities` are gone. In their +place is a `games` object (`uo` and `rust`, each with its protocol, its sidecar and plugin repos, its +module's capabilities and `coreApi`, and its bundle pins), plus a `runicnpc` block for the plugin's +API version, framework floors and required plugins. `releases` stays flat, so every repository's +release is read the same way. **41 facts** are now checked, up from 19. Checks 1, 2, 4 and 5 loop +over the games, and the bundle reads `v2//current.json` and refuses anything but schema 2. + +**What the re-read found** (§1 obliges a re-read of every page that quotes a changed value): + +- **Three hard-coded Module API versions in prose**, all saying 1.6.0, on *The module API* and *The + module manifest*. §12 has forbidden that since phase 1. Nothing caught it because `checkFacts` + guards `platform.json` and not the prose. All three now read `platform.json`. The manifest page's + worked example of a caret range now quotes the two modules' real `coreApi` ranges, which are + checked facts as of this phase. +- ***Module lifecycle* said modules load alphabetically.** That stopped being the whole truth on + 2026-09-23, when the installer began refusing a second module, and stopped being true at all on + 2026-10-22, when the loader began refusing one (W19). The page now says what the loader does. *Installing + modules* and *Managing modules* had never mentioned the one-module rule at all, and now do. +- **"There is one module" was false in five places.** Those were the `/architecture/` seam drawing and its + "what this does not yet prove", `/community/`, the `/features/` lede, the Game intelligence + summary, and the homepage's list of absences. Each now says two modules exist, both built inside + this project, and that the Integration Kit keeps its draft banner until someone outside does it. +- **The homepage diagram's sub-labels** said "ServUO today" and "uo-link". They became false the day + Rust shipped, so they changed here rather than waiting for phase 17. They now carry D63's wording. + The caption names both games. + +**One planned item moved to phase 17.** §16 put the Rust half of `capabilities.mjs`'s *page* coverage +assertion in this phase. But a coverage assertion needs a list of Rust capabilities on a page to +cover, and phase 17 is the phase that writes that list. Phase 13 does the half that has something to +check: `checkFacts` now holds Module-Rust's declared capabilities against `platform.json`, so a +capability the module adds turns this repo red today. The page-side assertion lands with the page. + --- [api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md diff --git a/scripts/checkFacts.mjs b/scripts/checkFacts.mjs index f6e3a33..f0cda8d 100644 --- a/scripts/checkFacts.mjs +++ b/scripts/checkFacts.mjs @@ -92,33 +92,42 @@ function extract(text, re, what, authority) { return m[1]; } +// Every game the site quotes facts for (PLAN.md §16). Checks 1, 2, 4 and 5 run once per +// game, and a row's label carries the game's key so a failure names which one moved. +const GAMES = Object.entries(platform.games); + // --------------------------------------------------------------------------- -// 1. The wire protocol, from the sidecar itself +// 1. Each game's wire protocol, from its sidecar // --------------------------------------------------------------------------- async function checkProtocol() { - const authority = 'link main:sidecar/src/main.rs'; - const src = await raw('link', 'sidecar/src/main.rs', 'main'); - const value = extract( - src, - /pub const PROTOCOL_VERSION:\s*u32\s*=\s*(\d+)\s*;/, - 'PROTOCOL_VERSION', - authority - ); - record('protocol (sidecar)', platform.protocol, Number(value), authority); + for (const [key, game] of GAMES) { + const authority = `${game.sidecarRepo} main:sidecar/src/main.rs`; + const src = await raw(game.sidecarRepo, 'sidecar/src/main.rs', 'main'); + const value = extract( + src, + /pub const PROTOCOL_VERSION:\s*u32\s*=\s*(\d+)\s*;/, + 'PROTOCOL_VERSION', + authority + ); + record(`${key} protocol (sidecar)`, game.protocol, Number(value), authority); + } } // --------------------------------------------------------------------------- -// 2. The overlay's declared protocol — the third declaration site +// 2. Each game plugin's declared protocol — the third declaration site // // CLAUDE.md: a protocol bump has to land in overlay.toml in the same PR as the emitters, -// or the installer refuses to pair the sidecar with the overlay. If these two ever -// disagree, the site must not print either number. +// or the installer refuses to pair the sidecar with the plugin. If these two ever +// disagree, the site must not print either number. Both games keep the file under the +// same name at the root of their plugin repository. // --------------------------------------------------------------------------- async function checkOverlayProtocol() { - const authority = 'servuo-plugins main:overlay.toml'; - const toml = await raw('servuo-plugins', 'overlay.toml', 'main'); - const value = extract(toml, /^\s*protocol\s*=\s*(\d+)\s*$/m, 'protocol', authority); - record('protocol (overlay)', platform.protocol, Number(value), authority); + for (const [key, game] of GAMES) { + const authority = `${game.pluginRepo} main:overlay.toml`; + const toml = await raw(game.pluginRepo, 'overlay.toml', 'main'); + const value = extract(toml, /^\s*protocol\s*=\s*(\d+)\s*$/m, 'protocol', authority); + record(`${key} protocol (plugin)`, game.protocol, Number(value), authority); + } } // --------------------------------------------------------------------------- @@ -137,7 +146,7 @@ async function checkModuleApi() { } // --------------------------------------------------------------------------- -// 4. The capabilities the installed module actually declares +// 4. The capabilities each game's module actually declares // // §12 names "module-uo's capability list" as one of the facts platform.json holds, and it // was the one fact nothing re-read. That mattered from phase 3 onwards, because the @@ -150,36 +159,109 @@ async function checkModuleApi() { // array is not a changed capability set. A slug appearing or disappearing is. // --------------------------------------------------------------------------- async function checkModuleCapabilities() { - const authority = 'Module-uo main:module.json'; - const manifest = JSON.parse(await raw('Module-uo', 'module.json', 'main')); - const declared = [...(manifest.capabilities || [])].sort(); - const expected = [...platform.moduleUoCapabilities].sort(); + for (const [key, game] of GAMES) { + const authority = `${game.module.repo} main:module.json`; + const manifest = JSON.parse(await raw(game.module.repo, 'module.json', 'main')); + const declared = [...(manifest.capabilities || [])].sort(); + const expected = [...game.module.capabilities].sort(); - record('moduleUoCapabilities', expected.join(' '), declared.join(' '), authority); + record(`${key} module capabilities`, expected.join(' '), declared.join(' '), authority); + // The range the module declares against core. The manifest docs use it as the worked + // example of a caret range resolving across minor bumps, so it is a quoted fact. + record(`${key} module coreApi`, game.module.coreApi, manifest.coreApi, authority); + } } // --------------------------------------------------------------------------- -// 5. The current bundle +// 5. Each game's current bundle // -// The manifests live at the ROOT of the `bundles` branch — `current.json`, -// `bundle-.json` — not under `bundles/`. Fetching the directory 404s. +// One manifest per game on the installer's `bundles` branch, at +// `v2//current.json` (schema 2). The ROOT `current.json` is the schema-1 manifest +// from before the installer learned a second game; it is frozen and no longer read here, +// because a check against a file that stopped moving passes forever. +// +// Each bundle is protocol-checked by CI when it is published, so its protocol is a third +// independent read of the same number rather than a duplicate of checks 1 and 2. // --------------------------------------------------------------------------- -async function checkBundle() { - const authority = 'installer bundles:current.json'; - const current = JSON.parse(await raw('installer', 'current.json', 'bundles')); +async function readBundle(game) { + const file = `v2/${game.bundleGame}/current.json`; + const authority = `installer bundles:${file}`; + const current = JSON.parse(await raw('installer', file, 'bundles')); + if (current.schema !== 2) { + throw new Error(`${authority} is schema ${current.schema}, and this check reads schema 2.`); + } + return { current, authority }; +} - record('bundle.tag', platform.bundle.tag, current.bundle, authority); - record('bundle.sidecar', platform.bundle.sidecar, current.link?.tag, authority); - record('bundle.overlay', platform.bundle.overlay, current.overlay?.tag, authority); +async function checkBundle() { + const { uo, rust } = platform.games; + + { + const { current, authority } = await readBundle(uo); + record('uo bundle.tag', uo.bundle.tag, current.bundle, authority); + record('uo bundle.sidecar', uo.bundle.sidecar, current.sidecar?.tag, authority); + record('uo bundle.overlay', uo.bundle.overlay, current.payload?.tag, authority); + record('uo bundle.servuoMin', uo.bundle.servuoMin, current.payload?.compat?.min_version, authority); + record('uo protocol (bundle)', uo.protocol, current.protocol, authority); + } + + { + const { current, authority } = await readBundle(rust); + const compat = current.payload?.compat ?? {}; + record('rust bundle.tag', rust.bundle.tag, current.bundle, authority); + record('rust bundle.sidecar', rust.bundle.sidecar, current.sidecar?.tag, authority); + record('rust bundle.plugin', rust.bundle.plugin, current.payload?.tag, authority); + record('rust bundle.oxideMin', rust.bundle.oxideMin, compat.frameworks?.oxide?.min_version, authority); + record('rust bundle.carbonMin', rust.bundle.carbonMin, compat.frameworks?.carbon?.min_version, authority); + record( + 'rust bundle.requiresPlugins', + [...rust.bundle.requiresPlugins].sort().join(' '), + [...(compat.requires_plugins ?? [])].sort().join(' '), + authority + ); + record('rust bundle.runicnpc', rust.bundle.runicnpc, current.npc?.tag, authority); + record('rust protocol (bundle)', rust.protocol, current.protocol, authority); + // The bundle carries RunicNPC's API too; check 5b reads it from the plugin's own repo. + record('runicnpc api (bundle)', platform.runicnpc.api, current.npc?.api, authority); + } +} + +// --------------------------------------------------------------------------- +// 5b. RunicNPC's own declarations +// +// RunicNPC stands alone as well as shipping in the Rust bundle (D61), so its facts are +// read from its own `plugin.toml` too — the file its release workflow folds into the +// manifest the installer and the egg read. The API version is the number a plugin author +// calling `RunicNpc_*` relies on, and the site quotes it beside the API reference (D62). +// --------------------------------------------------------------------------- +async function checkRunicNpc() { + const authority = 'runicnpc-rust main:plugin.toml'; + const toml = await raw('runicnpc-rust', 'plugin.toml', 'main'); + const npc = platform.runicnpc; + + record('runicnpc api', npc.api, Number(extract(toml, /^\s*api\s*=\s*(\d+)\s*$/m, 'api', authority)), authority); record( - 'bundle.servuoMin', - platform.bundle.servuoMin, - current.overlay?.servuo?.min_version, + 'runicnpc oxideMin', + npc.oxideMin, + extract(toml, /^\s*min_oxide_version\s*=\s*"([^"]+)"/m, 'min_oxide_version', authority), + authority + ); + record( + 'runicnpc carbonMin', + npc.carbonMin, + extract(toml, /^\s*min_carbon_version\s*=\s*"([^"]+)"/m, 'min_carbon_version', authority), + authority + ); + const required = extract(toml, /^\s*requires_plugins\s*=\s*\[([^\]]*)\]/m, 'requires_plugins', authority) + .split(',') + .map((name) => name.trim().replace(/^"|"$/g, '')) + .filter(Boolean); + record( + 'runicnpc requiresPlugins', + [...npc.requiresPlugins].sort().join(' '), + required.sort().join(' '), authority ); - // The bundle is protocol-checked by CI when it is published, so this is a third - // independent read of the same number rather than a duplicate of check 1. - record('protocol (bundle)', platform.protocol, current.protocol, authority); } // --------------------------------------------------------------------------- @@ -349,6 +431,7 @@ async function main() { checkModuleApi, checkModuleCapabilities, checkBundle, + checkRunicNpc, checkReleases, checkWebsiteHasNoReleases, checkAndroidApk, diff --git a/scripts/seedDemo.mjs b/scripts/seedDemo.mjs index b9161cc..4d4763d 100644 --- a/scripts/seedDemo.mjs +++ b/scripts/seedDemo.mjs @@ -69,7 +69,7 @@ const UOLINK_TOKEN = process.env.UOLINK_TOKEN || ''; // `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 UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.games.uo.protocol); const DRY = process.argv.includes('--dry-run'); diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 54a2b9e..c01d27b 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -62,8 +62,9 @@ const isExternal = (href: string) => href.startsWith('http');

diff --git a/src/components/StructuredData.astro b/src/components/StructuredData.astro index 00bde27..7eb90c6 100644 --- a/src/components/StructuredData.astro +++ b/src/components/StructuredData.astro @@ -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. diff --git a/src/components/architecture/ModuleSeam.astro b/src/components/architecture/ModuleSeam.astro index f8f90b7..67236a0 100644 --- a/src/components/architecture/ModuleSeam.astro +++ b/src/components/architecture/ModuleSeam.astro @@ -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'; Game module - one per deployment; UO today + one per deployment; UO or Rust today Routes @@ -130,11 +132,12 @@ import platform from '../../data/platform.json';

What this does not yet prove

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

diff --git a/src/components/home/Capabilities.astro b/src/components/home/Capabilities.astro index 23569c9..000b505 100644 --- a/src/components/home/Capabilities.astro +++ b/src/components/home/Capabilities.astro @@ -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); ---
@@ -52,7 +52,7 @@ assertCapabilityCoverage(platform.moduleUoCapabilities);

Some things people reasonably expect are deliberately not built — 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 features and{' '} integrations.

diff --git a/src/components/home/DataPath.astro b/src/components/home/DataPath.astro index 9461072..5da52cc 100644 --- a/src/components/home/DataPath.astro +++ b/src/components/home/DataPath.astro @@ -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'; Your game server - ServUO today · opens no inbound port + opens no inbound port Sidecar - uo-link · the only network-facing part + the only network-facing part Runic Gateway @@ -90,8 +90,9 @@ import platform from '../../data/platform.json';

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

@@ -109,8 +110,8 @@ import platform from '../../data/platform.json';

The sidecar

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.

diff --git a/src/components/home/Hero.astro b/src/components/home/Hero.astro index 265ef75..be5151c 100644 --- a/src/components/home/Hero.astro +++ b/src/components/home/Hero.astro @@ -55,9 +55,9 @@ import platform from '../../data/platform.json';
- Protocol {platform.protocol} + {/* 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. */} Module API {platform.moduleApi} - Bundle {platform.bundle.tag}
diff --git a/src/content/docs/docs/administration/managing-modules.mdx b/src/content/docs/docs/administration/managing-modules.mdx index c6da49d..30251f5 100644 --- a/src/content/docs/docs/administration/managing-modules.mdx +++ b/src/content/docs/docs/administration/managing-modules.mdx @@ -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. diff --git a/src/content/docs/docs/administration/the-shard-connection.mdx b/src/content/docs/docs/administration/the-shard-connection.mdx index ad7de3f..56957fa 100644 --- a/src/content/docs/docs/administration/the-shard-connection.mdx +++ b/src/content/docs/docs/administration/the-shard-connection.mdx @@ -26,7 +26,7 @@ on: | **Base URL (REST)** | `http://:8080` — point-in-time queries | | **WebSocket URL (feed)** | `ws://: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. diff --git a/src/content/docs/docs/architecture/protocol-versions.mdx b/src/content/docs/docs/architecture/protocol-versions.mdx index 7f67737..a53edc2 100644 --- a/src/content/docs/docs/architecture/protocol-versions.mdx +++ b/src/content/docs/docs/architecture/protocol-versions.mdx @@ -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 |