The kit was pinned to website 963d734 -- MODULE_API 1.6.0, the Teams cutover --
and the platform is on 1.9.0. Three registrations and two calls arrived in
between, and a reader building against this book would have found no mention of
any of them: a module can now declare what its game can announce, and never who
is told.
Moving `ci/core-ref.json` is the mechanism for exactly this. The pin is now
66bb3b9a (website `main`, the engagement cutover) and `template/module.json`
declares `^1.9.0`.
What chapter 2 gained, under "Telling core something happened":
* a TRIGGER is a payload contract, not a notification stream -- the two share
one id namespace and are constantly confused;
* `ceiling` is required, has no default, and is a CONTAINMENT tree rather than
a size ladder (a `staff` ceiling does not permit `owner`);
* an AUDIENCE resolver returns user ids and nothing else, resolves to NOBODY
on failure, and takes CONSTANT params -- the constraint worth knowing before
you design around it;
* templates re-ensure per seedVersion, rule groups are offered ONCE per group
key, so a rule appended to an existing group reaches fresh installs only;
* `ctx.events.emit` binds the owner and is fire-and-forget; `ctx.inbox.push`
is the direct write, for when there is nothing for an operator to decide.
The template builds all of it: one trigger, one audience over the clan roster it
already had, one seeded body and one seeded rule group, and an emitter in
`boot.js` that fires on the TRANSITION rather than on the poll. Seven new tests,
including the audience that resolves to nobody when its query throws.
Three claims were wrong and are corrected here rather than shipped:
* core validates `subjectKey` against the declared variables and refuses the
module; the draft taught a cooldown keyed on `undefined`, which the check
exists to prevent and a reader will never see.
* `emit` throws OUTSIDE production and only drops-and-logs inside it. Teaching
the second half alone leaves a developer meeting a throw the book says
cannot happen.
* the seeded body itself was malformed -- heading `level: 2` where the block
registry takes 'h2', and no block ids at all.
The third is the one worth keeping: `registerEngagementSeeds` checks that
`blocks` is a non-empty array and stops, so that body would have registered,
seeded, and failed the first time an operator opened it. Found by running the
template's `register()` through core's real registry at the pinned ref -- which
CI does not do, and cannot: the template job checks the version and runs the
template against fakes. A fake accepts what core refuses. The gap is now named
in the chapter, beside the code, and in the pin's own comment, and the rule that
bit has a test that fails on it.
Also: `checkLinks` skipped `.core/`. Bumping this pin means cloning core into
that directory first, and the walk then reported nine broken links in someone
else's README. CI never saw it -- the clone happens in the `template` job and
the check runs in `prose` -- so it was a failure only a person could meet.
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 genuinely delivers events on a
surface of its own needs a thin sidecar, not none — but check that it delivers
events rather than answering questions, because a channel built for an operator
typing commands can only be polled, and polling turns "someone left at 14:02" into
"the count was different at 14:03".
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.