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
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:
58
PLAN.md
58
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/<game>/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
|
||||
|
||||
@@ -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-<tag>.json` — not under `bundles/`. Fetching the directory 404s.
|
||||
// One manifest per game on the installer's `bundles` branch, at
|
||||
// `v2/<game>/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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user