# module-uo — the Ultima Online module for Runic Gateway The Runic Gateway website is becoming **game-agnostic**: core keeps accounts, sessions, the wiki, posts, branding, theming and the admin panel, and everything that knows what a *shard* is moves out into an installable module. This repo is that module — the first one, and the reference for every module that follows. ``` RunicGateway/website (core — game-agnostic) │ loads modules at boot, synchronously, from the filesystem ▼ ┌───────────────────────────────────────────┐ │ module-uo (>>> HERE <<<) │ │ shard status · spawn atlas · marketplace │ │ governors · cliloc · town crier · uo-link│ └───────────────────────────────────────────┘ │ server half: routers, models, schema fragment │ client half: prebuilt ESM chunk, SPA routes + nav ▼ the shard bridge (RunicGateway/link → RunicGateway/servuo-plugins) ``` The module's **id** is `uo` — that is what appears in `module.json`, in the `installed_modules` table, in the `modules//` path on disk and in the URL segment (`/uo/*`, `/admin/uo/*`, `/player/uo/*`). `Module-uo` is the repository; `module-uo` is the module and its release artifact. ## Status: the bundle skeleton exists; the extraction has started The design of record is [`website/MODULE_SYSTEM.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md) and the normative contract is [`website/MODULE_API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md) in the docs repo — **read them before opening a PR here.** Where the two differ, the contract wins. | Phase | Where it happens | State | |---|---|---| | 0 — CI trigger fix, cut `website` `edge`, bootstrap this repo | `website`, here | ✅ done | | 1 — module API contract (`docs/website/MODULE_API.md`) + the atlas spike | `docs`, `website` | ✅ done | | 2 — core scaffolding: loader, `installed_modules`, registries, client registry | `website` | ✅ done | | 3 — extract the UO half of the site into this repo | `website`, here | 🟡 in progress | | 4 — delivery: the admin Modules screen + the Docker path | `website` | ⬜ | Phase 3 moves the UO half of `website/` here in ten slices (`MODULE_SYSTEM.md` §2.7.1), server-first and then client. Each slice is one PR here that adds, and one PR in `website` that deletes — this one merging first, so `website`'s `edge` branch serves the feature from core right up to the moment core drops it. **Slice 0 is the bundle skeleton, and it registers nothing on purpose.** What it proves is the delivery path itself: core discovers the module, validates `module.json`, calls `register()`, serves the client chunk, injects it, and reports the module `started` — and the chunk resolves React, the renderer and the router from core's `window.__rg` rather than bundling its own. Every slice after this one adds registrations to `server/index.js` and `client/src/entry.jsx`. ## Working on it ```bash npm ci --prefix server && npm test --prefix server && npm run check:imports --prefix server npm ci --prefix client && npm test --prefix client && npm run build --prefix client npm run check:externals --prefix client # asks the BUILT chunk, so it runs after the build ``` The two `check:*` scripts are the contract's acceptance criteria rather than this module's own tests: no import may escape the module root (`MODULE_API.md` §5.1), and no bare specifier may survive into the built chunk (§3.6). The matching failure — a shared dependency being *bundled* — fails the build itself, from a guard inside `vite.config.js`. Running it against a real core means checking this repo out as `website/modules/uo`, building the client half, and booting core. The four-step browser smoke in `MODULE_API.md` §7.7 is the only thing that proves the client half works: its real failure modes are timing and module resolution, and neither has a shape a DOM-less test runner can see. ## What it contains One repo, one bundle: the server half and the client half live side by side and version together, so a route and the screen that calls it can never be mismatched. A ✅ is in the tree today. ``` module.json ✅ id, version, coreApi range, mounts, extensions server/index.js ✅ the entry point — register(ctx, api), synchronous, no database server/scripts/ ✅ checkImports.js — the §5.1 boundary check server/test/ ✅ node --test, with a fake ctx standing in for core server/ routers, controllers, models, utils server/db/schema.sql idempotent fragment, replayed by core's ensureSchema() server/db/purge.sql destructive; only ever run by an explicit purge client/src/entry.jsx ✅ the chunk's entry — registers routes, nav, feature provider client/src/shim/ ✅ react, react-dom, react-router-dom, jsx-runtime, from window.__rg client/vite.config.js ✅ the library build, the aliases, the not-bundled guard client/src/ route components, nav registrations, feature provider client/dist/ ✅ PREBUILT ESM chunk, built by CI — never by an operator ``` Release artifact: `module-uo-.tar.gz`, plus a manifest carrying its `sha256`. ## How it reaches an operator **An operator never builds anything.** Installing a module is the WordPress-plugin experience: an admin-panel action, or a directory mounted into the Docker container — never a build step, because production runs a prebuilt, pull-only image. That constraint is why the client half ships as a prebuilt ESM chunk that resolves React from a `window.__rg` global rather than an import map (an import map must be inline, and the site's CSP is `script-src 'self'`). The [installer](https://gitea.whitlocktech.com/RunicGateway/installer) is **not** the delivery path. It deploys the *shard* side — the plugin overlay and the uo-link sidecar — and never contacts the website. Module delivery is website-side only. ## Compatibility `module.json` declares a `coreApi` semver range, checked at boot against core's `MODULE_API_VERSION`. A mismatch fails **loudly** — the module is marked `startup_failed` and the site comes up without it, rather than mis-loading. This is a separate number from `PROTOCOL_VERSION`, which versions the shard wire protocol and says nothing about a website module. A module that fails to load must never take the site down. ## Related repos | Repo | What | |------|------| | **this** — `RunicGateway/Module-uo` | The UO module: the game-specific half of the website. | | [RunicGateway/website](https://gitea.whitlocktech.com/RunicGateway/website) | Core — the site, admin panel and API that loads this module. | | [RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link) | The **uo-link sidecar** — the network-facing half of the game bridge this module talks to. | | [RunicGateway/servuo-plugins](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) | The **C# ServUO plugin** that feeds the sidecar. | | [RunicGateway/installer](https://gitea.whitlocktech.com/RunicGateway/installer) | Deploys the shard side. Not the module delivery path. | | [RunicGateway/docs](https://gitea.whitlocktech.com/RunicGateway/docs) | All project documentation, including the module system design and this module's docs under `modules/uo/`. | ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). Contributions are welcome, AI assistance must be disclosed, and security problems go to [SECURITY.md](SECURITY.md) rather than a public issue. ## License **GNU General Public License v3.0 or later** — see [LICENSE.md](LICENSE.md).