A cold agent was given this repo and the documents it links to, and nothing else — no core source, no module-uo — and asked to build a module for a second game. It did, in one pass. The record is docs/modules/kit-acceptance.md; this is the repair list, plus the two things it recommended that were not defects. The one it could not find, because it had no core to render against: a module page built exactly as this kit teaches renders OUTSIDE the site. PublicLayout is the chrome, not the body. Core grew an opt-in `shell` prop for it (MODULE_API_VERSION 1.5.0, website#148); the template passes shell="narrow" and chapter 2 explains why you name a width and never a class. Fixed: - **F1, and the worst of them, because it lands in the first twenty minutes.** `npm run check:swagger` failed on a PRISTINE template on Windows: the check compared the committed fragment byte-for-byte and a default Windows clone is CRLF while the generator writes LF. The message blamed "the routes or their annotations". Now `template/.gitattributes` pins `eol=lf` and the comparison normalises line endings anyway — a check may only fail for the reason it names, and this one names a diagnosis. - **F3** — `.gitea/workflows/release.yml` carries `gitea.example.com` and `your-org/your-module` under a literal `# CHANGE THESE`, was not in the rename checklist, and `checkRenameSites.js` could not match it, so CI was silent by construction. Row added, pattern widened. (The agent reported both workflow flavours; only the Gitea one is affected — GitHub supplies its own variables. Corrected in the record.) The near-miss is kept in the check's comments and its suite: the obvious widening is `example\.com`, which fires on a fixture URL in checkImports.test.js. Every alternative has to be a string that cannot occur by accident, which is the same rule that made the id `examplegame`. - **F4** — the release bundle's include list was hardcoded, so adding `server/utils/` would have silently dropped it from every release while the bundle check stayed green. Inverted to an exclusion list, in both flavours, and run by hand because a release workflow never executes in CI. - **F5** — the annotation-quoting warning was wrong in both directions, and the correction is measured rather than reasoned. A backtick is harmless (the template's own description has two spans and they survive). A `"` is not, and it does not throw: `'A "quoted" status'` is silently TRUNCATED to `A "` while swagger-autogen prints Success and the error capture sees nothing. The only signal is check:swagger blaming your routes. - **F6** — `template/.gitignore`, so a copied template that is `git init`ed inherits ignore rules instead of nothing. - **F7** — the UI kit is eight exports across five rows, not seven. The contract said seven and this kit had faithfully carried the miscount out of it. Adopted, not defects: - Chapter 1 now says to run every check on the untouched copy first. That is what found F1; without a baseline the first failure is ambiguous forever. - The template ships the §2.7 self-check the agent wrote for itself. The rule has no CI in general — an outbound socket is not statically detectable — but a module can make a decidable claim about its own tree. Ported from its code with a header explaining how to NARROW it when a sidecar client arrives, since talking to your sidecar is the expected shape and is not what §2.7 forbids. The pin moves to website edge 4ad8b2b, the 1.5.0 bump, and template/module.json declares ^1.5.0 — so checkCoreApi's equality assertion still holds and the template uses a member that exists only at that ref and later. 32 server + 18 client template tests, 21 kit-script tests, all four checks green. Co-Authored-By: Claude <noreply@anthropic.com>
Runic Gateway — Integration Kit
How to put a game on a Runic Gateway site.
Runic Gateway is a website platform for game communities. Core knows nothing about
any particular game: everything game-specific — routes, tables, pages, navigation,
notifications — arrives as an installable module, and an operator installs one
from an admin panel without building anything. module-uo is the
first module and serves an Ultima Online shard. This kit is how you write the
second one.
🚧 This is a draft
The kit is finished when someone outside this project builds a working module for a new game by following it alone, without reading core's source. That has not happened yet, so treat every chapter as untested on you. If you are that person: the places you get stuck are the most valuable thing this repo can receive — tell us, and please say where you left the kit and what you did next.
What you are building
Three things, and the kit is one book rather than a page in three repos because the reasons live in the joins between them:
| # | Part | What it is |
|---|---|---|
| 1 | The website module | A bundle core loads at boot: server routes, a schema fragment, a prebuilt client chunk, navigation. The bulk of the work, and the only part every module needs. |
| 2 | The sidecar | A small service that owns the connection to your game server, and owns the durable copy of what the game said. Not optional — see below. |
| 3 | The game-side plugin | Whatever runs inside your game and feeds the sidecar, without ever letting the sidecar stall the game. |
your game server ──dials out──▶ your sidecar ──HTTP + WS──▶ website core
(plugin: bounded (owns the socket, (loads your module,
queue, writer thread) persists to its own serves the pages)
store, then forwards)
The website process never opens a connection to a game server. That is a rule
in the contract (MODULE_API.md §2.7, MODULE_API_VERSION 1.4.0), not a
style preference, and chapter 3 is mostly about why. The short version: the
website is the internet-facing process and your game is not; the sidecar persists
before it forwards, so a website that is down or mid-deploy loses nothing; and a
game must never block on a web request. A game that already exposes a
remote-control surface — Rust's RCON over WebSocket, say — needs a thin sidecar,
not none.
Start here
- The dry run — a complete module designed on paper for a second game, Rust, chosen for how little it shares with Ultima Online. Read it first. It is the shortest honest picture of the whole job, and it names the one thing the contract cannot do yet.
template/— a module that builds and loads, doing almost nothing. Copy it, rename it, and you have a running module before you have read a chapter.- The book —
book/, four chapters, in the order the work happens.
The one rule this kit follows
It never re-specifies a contract. These documents are normative, and where the kit and one of them disagree, they win and the kit has a bug:
| Authority | For |
|---|---|
MODULE_API.md |
Everything a module may do: module.json, ctx, the register* calls, the client registry, the UI kit, schema-fragment rules, the loader's obligations. |
MODULE_SYSTEM.md |
Why the module system is shaped this way, and how a module is installed and removed. |
link/PLAN.md + INTEGRATION.md |
The shard↔sidecar wire protocol, as one real sidecar implements it. |
The kit teaches: the order to do things in, the reasoning, worked examples, and the mistakes that cost this project time. Where it must show a member list it quotes with a pointer rather than copying, because a guide that restates a contract diverges from it silently — and a reader who follows the divergent copy gets a module that fails validation for reasons the guide cannot explain.
What this repo contains
book/ the chapters
template/ a module that builds — copy this
scripts/ the checks CI runs over both
CI clones core at a pinned commit, asserts the version the template declares
still matches that core's MODULE_API_VERSION, builds the template and runs its
guards, checks every link in the book, holds the template's rename checklist
against the template's own tree, and checks that every path a chapter names is
still there. So a change to the contract breaks this repo's build loudly instead
of leaving a chapter quietly wrong.
None of that can tell you whether a paragraph has become untrue about a file that
still exists. That is a reviewer's job on every pull request, and a
MODULE_API_VERSION bump is when it is owed in full.
Licence
GPL-3.0-or-later, like every Runic Gateway repo — see LICENSE.md.
The template/ directory is meant to be copied and made yours; it carries the
same licence, and so does anything derived from it.