All checks were successful
PR checks / checks (pull_request) Successful in 2m37s
PLAN.md §16 phase 17, built on the org lead's answers of 2026-10-22, recorded as D75-D79 (count of record 79). - D75: core marketing pages describe what a game module adds in core's words. "Game intelligence" became "What a game module adds"; each game's own list moved to src/data/games/ and renders only on the new /modules/uo/ and /modules/rust/. /modules/ is a card per game. - The page-side capability check is per game (assertGameCoverage), the item phase 13 moved here. - D76: the homepage keeps its game screenshot, labelled. screens.mjs entries may carry `game` (D67); Screenshot.astro and the app grid print the game's name and mark the figure data-game. - D77: /app/ describes the Rust screens (released the same day). - D78: /runicnpc/, the standalone page, is in the header and footer; the narrow header was re-measured so five links keep two rows. - D79: checkTerms.mjs --built reads every built page's visible text after the build; game routes and data-game elements allow their game's words. Proven against ten plants. Still the twelfth check. - Re-read fixes: /architecture/'s "two machines is also the maximum", Discord "guild" on /integrations/, and collection.mjs against what Steam sign-in and the Rust module keep (PLAY_DATA_SAFETY.md regenerated; the Play answers are unchanged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
399 lines
13 KiB
Plaintext
399 lines
13 KiB
Plaintext
---
|
|
import Base from '../layouts/Base.astro';
|
|
import PageHeader from '../components/PageHeader.astro';
|
|
import NotBuilt from '../components/NotBuilt.astro';
|
|
import SupportedGames from '../components/SupportedGames.astro';
|
|
|
|
import platform from '../data/platform.json';
|
|
import { games } from '../data/games/index.mjs';
|
|
|
|
/**
|
|
* `/modules/` — PLAN.md §13 phase 4, restructured in §16 phase 17.
|
|
*
|
|
* §10 gave this page four jobs: what a module is, a worked example, writing your own, and
|
|
* the Integration Kit with its draft badge (D8). They are in that order because they are
|
|
* increasing commitment — a reader deciding whether to install one, a reader wondering what
|
|
* they get, a reader considering building one.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* TWO GAMES, EACH ON ITS OWN PAGE
|
|
* ---------------------------------------------------------------------------------------
|
|
* The worked example was the Ultima Online module, listed in that game's words. Since phase
|
|
* 17 there are two modules, and D63 keeps a game's vocabulary on its own pages, so this page
|
|
* is core: a card per game, in core's words, from `games/index.mjs`, each linking to
|
|
* `/modules/uo/` or `/modules/rust/`, where the game's real feature list is (D75). What the
|
|
* modules share — a sidecar, their own tables, their own releases — stays here, said once.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* THE DRAFT CHIP IS A DECISION, NOT A DISCLAIMER
|
|
* ---------------------------------------------------------------------------------------
|
|
* D8 marks the Integration Kit draft until a second module is successfully built against
|
|
* it by somebody outside this project, and requires that status to carry its removal
|
|
* condition. Both are here: the chip, and the sentence that says what takes it down. The
|
|
* same absence appears in `notBuilt.mjs`, so a reader who scrolls past the chip meets it
|
|
* again in the list of things that do not exist.
|
|
*/
|
|
const title = 'Modules';
|
|
const description =
|
|
'What a module is, the two game modules that exist — Ultima Online and Rust — and what it ' +
|
|
'takes to write one for another game.';
|
|
|
|
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
|
|
const docs = `${gitea}/docs/src/branch/main`;
|
|
|
|
/** The three ways a module reaches a running deployment. None of them is a build. */
|
|
const installPaths = [
|
|
{
|
|
name: 'From the admin panel',
|
|
body:
|
|
'Paste the URL of a release manifest into Admin → Modules and press restart when it ' +
|
|
'asks. The site downloads the artifact, verifies the checksum the manifest declares, ' +
|
|
'inspects the whole archive before writing a single file, and unpacks it.',
|
|
fits: 'The click path, for a host you have no shell on.',
|
|
},
|
|
{
|
|
name: 'From your environment',
|
|
body:
|
|
'Name the module and its version in one environment variable and the container ' +
|
|
'resolves it at every start. Already at that version means no network call at all, so ' +
|
|
'a restart with the internet down comes up unchanged.',
|
|
fits: 'A compose-managed host, where the running set should be a line you version-control.',
|
|
},
|
|
{
|
|
name: 'By hand',
|
|
body:
|
|
'Unpack the tarball into the modules directory and restart. The bundle is already ' +
|
|
'assembled — the client half is prebuilt and its one runtime dependency ships inside.',
|
|
fits: 'Development, and any host where the other two do not fit.',
|
|
},
|
|
];
|
|
---
|
|
|
|
<Base title={title} description={description}>
|
|
<PageHeader eyebrow="The extension model" title="One platform, whichever game you run">
|
|
<p>
|
|
A module is the entire game-specific half of a deployment, packaged: its routes, its
|
|
screens, its database tables, its navigation rows and its slice of the API
|
|
documentation. The core site holds accounts, Teams, the wiki, posts, moderation and the
|
|
admin panel, and knows nothing about any game at all.
|
|
</p>
|
|
<p>
|
|
That division is not an aspiration bolted on afterwards. The Ultima Online support was
|
|
extracted out of the site into a module, and every URL it had before the move it still
|
|
has — which is the only version of this claim worth making. Rust followed, built
|
|
against the same seam.
|
|
</p>
|
|
<SupportedGames class="mod__supported" />
|
|
</PageHeader>
|
|
|
|
<section class="page section mod" id="what">
|
|
<p class="eyebrow">What you get</p>
|
|
<h2>What installing one actually does</h2>
|
|
|
|
<ul class="mod__grid">
|
|
<li class="panel">
|
|
<h3>It brings its own everything</h3>
|
|
<p>
|
|
Server routes, React screens, tables, nav rows and an OpenAPI fragment the site
|
|
merges into its own spec. Nothing about it is a patch to the core site, so
|
|
upgrading either half does not involve reconciling the other.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>You never build it</h3>
|
|
<p>
|
|
The client half ships prebuilt and the artifact is verified against a published
|
|
checksum before anything is written to disk. Production runs an image you pulled;
|
|
an operator who has to compile something has been handed a maintenance job.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>It cannot take the site down</h3>
|
|
<p>
|
|
A module whose declared interface version does not match is marked failed and the
|
|
site starts without it — loudly, rather than half-loading. Disabling one closes its
|
|
connections and stops its routes answering.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>Your data outlives it</h3>
|
|
<p>
|
|
Uninstalling removes the module and keeps its tables, so reinstalling picks up
|
|
exactly where it was. Destroying the data is a separate, opt-in choice made in its
|
|
own dialog, and it says what it is about to do.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="page section mod" id="installing">
|
|
<p class="eyebrow">Installing</p>
|
|
<h2>Three ways in, and none of them is a build</h2>
|
|
<p class="prose mod__lede">
|
|
Which one you use is a question about your host, not about the module. All three end
|
|
the same way: a restart, and the module's screens appear in the navigation.
|
|
</p>
|
|
|
|
<ol class="mod__paths">
|
|
{
|
|
installPaths.map((path) => (
|
|
<li class="panel">
|
|
<h3>{path.name}</h3>
|
|
<p>{path.body}</p>
|
|
<p class="mod__fits">{path.fits}</p>
|
|
</li>
|
|
))
|
|
}
|
|
</ol>
|
|
</section>
|
|
|
|
<section class="page section mod" id="games">
|
|
<p class="eyebrow">The game modules</p>
|
|
<h2>Two games, one module each</h2>
|
|
<p class="prose mod__lede">
|
|
Both were built inside this project, and a deployment runs one of them — the site
|
|
refuses to start a second. Each has a page of its own, in its own game's words, and a
|
|
documentation group that starts from installing it.
|
|
</p>
|
|
|
|
<ul class="mod__games">
|
|
{
|
|
games.map((game) => (
|
|
<li class="panel mod__game">
|
|
<div class="mod__head">
|
|
<h3>{game.name}</h3>
|
|
<span class="chip chip--version">
|
|
{game.release}
|
|
</span>
|
|
</div>
|
|
<p>{game.blurb}</p>
|
|
<div class="mod__game-links">
|
|
<a class="btn btn--primary" href={game.href}>
|
|
What the {game.name} module adds
|
|
</a>
|
|
<a class="btn btn--ghost" href={game.docs}>
|
|
Its documentation
|
|
</a>
|
|
</div>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
|
|
<div class="mod__facts mod__facts--shared">
|
|
<section>
|
|
<h3>Each connects to a real server</h3>
|
|
<p>
|
|
A module talks to the sidecar beside your game server, not to the game. You deploy
|
|
that side with the installer and give the admin panel its address and token;
|
|
nothing here requires the game to exist, and with no server configured the site
|
|
renders normally and shows it offline.
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<h3>Each owns its own tables</h3>
|
|
<p>
|
|
Its schema is applied by the site on every boot and its data is its own. A module
|
|
declares which versions of the core interface it speaks — the site runs
|
|
{' '}{platform.moduleApi} — and refuses to load against one it does not.
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<h3>Each is a separate release</h3>
|
|
<p>
|
|
Versioned, tagged and published on its own cadence, independently of the site.
|
|
Upgrading one does not mean upgrading the other, as long as the declared interface
|
|
range still holds.
|
|
</p>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="page section mod" id="writing">
|
|
<div class="mod__head">
|
|
<p class="eyebrow">Writing your own</p>
|
|
<h2>The Integration Kit</h2>
|
|
<span class="chip chip--draft">Draft</span>
|
|
</div>
|
|
|
|
<p class="prose mod__lede">
|
|
A four-chapter book on putting a different game on this platform — the module, the
|
|
sidecar beside your game server, the plugin inside it — plus a template module that
|
|
continuous integration builds against a pinned version of the core site, so the
|
|
instructions cannot quietly stop working.
|
|
</p>
|
|
|
|
<div class="panel mod__draft">
|
|
<h3>Why it says draft</h3>
|
|
<p>
|
|
Because nobody outside this project has yet followed it to a working module, and that
|
|
is the only test of a set of instructions that counts. The badge comes off when
|
|
somebody does — that is the stated condition, not a mood, and it is written down so a
|
|
future reader knows when to take it down.
|
|
</p>
|
|
<p>
|
|
Everything it teaches is real and in use. What is untested is whether it is
|
|
<em>sufficient</em>: whether someone with no access to this project's context can get
|
|
from an empty repository to a running module using it alone.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mod__links">
|
|
<a class="btn btn--primary" href={`${gitea}/Integration-kit`} rel="noopener noreferrer">
|
|
Read the Integration Kit
|
|
</a>
|
|
<a class="btn btn--ghost" href={`${docs}/website/MODULE_API.md`} rel="noopener noreferrer">
|
|
The module contract
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<NotBuilt scope="modules" title="What the module system does not do" />
|
|
</Base>
|
|
|
|
<style>
|
|
.mod h2 {
|
|
margin: 0 0 0.75rem;
|
|
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
|
}
|
|
|
|
.mod__head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.mod__head .eyebrow {
|
|
flex-basis: 100%;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.mod__head h2 {
|
|
margin: 0;
|
|
}
|
|
|
|
.mod__lede {
|
|
margin: 0.85rem 0 0;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.mod__grid,
|
|
.mod__paths {
|
|
display: grid;
|
|
gap: 1rem;
|
|
margin: 2rem 0 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
|
|
}
|
|
|
|
.mod__grid h3,
|
|
.mod__paths h3,
|
|
.mod__facts h3,
|
|
.mod__draft h3 {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--gold);
|
|
font-size: 1.02rem;
|
|
}
|
|
|
|
.mod__grid p,
|
|
.mod__paths p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__paths li {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Which host each path suits, set apart from what it does — a reader is choosing
|
|
between three, so the distinguishing line should not be buried in the paragraph. */
|
|
.mod__fits {
|
|
margin-top: auto;
|
|
padding-top: 0.85rem;
|
|
color: var(--dim);
|
|
font-size: 0.88rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
.mod__games {
|
|
display: grid;
|
|
gap: 1rem;
|
|
margin: 2rem 0 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
|
|
}
|
|
|
|
.mod__game {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.mod__game h3 {
|
|
margin: 0;
|
|
color: var(--gold);
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.mod__game p {
|
|
margin: 0.75rem 0 0;
|
|
color: var(--muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__game-links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.6rem;
|
|
margin-top: auto;
|
|
padding-top: 1.25rem;
|
|
}
|
|
|
|
.mod__supported {
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.mod__facts--shared {
|
|
display: grid;
|
|
gap: 1.4rem;
|
|
margin-top: 2.25rem;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
|
|
}
|
|
|
|
.mod__facts--shared section + section {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.mod__facts p {
|
|
margin: 0;
|
|
max-width: var(--measure);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.mod__draft {
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.mod__draft p {
|
|
margin: 0;
|
|
max-width: var(--measure);
|
|
color: var(--muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__draft p + p {
|
|
margin-top: 0.85rem;
|
|
}
|
|
|
|
.mod__links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.7rem;
|
|
margin-top: 1.75rem;
|
|
}
|
|
</style>
|