The quick start carried a caveat telling a visitor that the Modules screen it had just described was not on `main` yet, and that a checkout served the Ultima Online features from core directly. The cutover merged on 2026-08-12 (website#150), so both halves of that are now false and the caveat contradicts the page around it. Replaced rather than deleted: a reader who saw the old note deserves to know it resolved, and the sentence that replaces it is the one fact the front page owes about the change - a `main` checkout is a game-agnostic core, and Ultima Online arrives as the module the section above tells you to install. Nothing else on the page needed touching. It was written describing the module system as the shape of the project rather than as work in progress, which is what left exactly one paragraph to retire. Co-Authored-By: Claude <noreply@anthropic.com>
226 lines
14 KiB
Markdown
226 lines
14 KiB
Markdown
<div align="center">
|
|
|
|
# Runic Gateway
|
|
|
|
**A website platform for game communities, wired to the live game.**
|
|
|
|
Runic Gateway gives a game community a public site, wiki, and admin panel — and bridges
|
|
it to the *running game server*, so the site can show live status, economy, player
|
|
activity, per-character detail and more, while staff push commands back the other way.
|
|
Players get the same public content and self-service on the web **or** a native Android app.
|
|
|
|
The platform itself knows nothing about any particular game. Everything game-specific —
|
|
routes, tables, pages, navigation, notifications — arrives as an installable **module**,
|
|
installed from the admin panel with nothing to build. *Ultima Online* (ServUO) is the
|
|
first game, and one installer sets up its shard side.
|
|
|
|
</div>
|
|
|
|
---
|
|
|
|
## The pieces
|
|
|
|
Runic Gateway is eight repositories that deploy together but build independently.
|
|
|
|
**The platform** — the same for every game:
|
|
|
|
| Repo | Language | What it is |
|
|
|------|----------|------------|
|
|
| [**website**](https://gitea.whitlocktech.com/RunicGateway/website) | JavaScript (Node + React) | The full-stack app — Express REST API + MariaDB + a React/Vite SPA (public site, wiki, admin panel), plus the module loader that game support plugs into. This is the thing players and staff actually visit, and the single backend every client talks to. |
|
|
| [**Android-app**](https://gitea.whitlocktech.com/RunicGateway/Android-app) | Kotlin (Jetpack Compose) | The **native Android client** — public content + player self-service, purely an API client of the website backend. Same features as the browser client *minus* every admin console. Never touches a game server. |
|
|
| [**docs**](https://gitea.whitlocktech.com/RunicGateway/docs) | Markdown | All project documentation — design docs, the module contract, the wire-protocol spec, the integration guide, the Android plan, research. Start here when you want the *why*. |
|
|
| [**Integration-kit**](https://gitea.whitlocktech.com/RunicGateway/Integration-kit) | Markdown + a buildable template | **How to put a *different* game on a Runic Gateway site.** A four-chapter book and a module template that builds and loads. Read this if your game is not Ultima Online. |
|
|
|
|
**Ultima Online support** — the first game, and the worked example everything else is measured against:
|
|
|
|
| Repo | Language | What it is |
|
|
|------|----------|------------|
|
|
| [**Module-uo**](https://gitea.whitlocktech.com/RunicGateway/Module-uo) | JavaScript (Node + React) | The **UO module** — every UO-specific route, table, page and notification the site serves: shard status, economy, IDOCs, character sheets, a player-vendor marketplace, a bestiary built from the shard's own spawn tables, and the staff control consoles. Installed into a website; released as a versioned bundle. |
|
|
| [**link**](https://gitea.whitlocktech.com/RunicGateway/link) | Rust | The **uo-link** sidecar. Runs next to the shard, terminates a loopback link from the game, persists what the game says, and exposes the authenticated WebSocket + REST API the website consumes. The only network-facing half of the bridge. |
|
|
| [**servuo-plugins**](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) | C# | The **ServUO plugin** — the shard side of the bridge. Compiled by ServUO at boot; dials the sidecar over loopback and emits game events / accepts commands. |
|
|
| [**installer**](https://gitea.whitlocktech.com/RunicGateway/installer) | Rust | The **installer** — one binary per OS that deploys the plugin and the sidecar onto a shard host as a matched, protocol-checked pair, registers the service, and hands you the values the website needs. How you set a shard up. |
|
|
|
|
## How they fit together
|
|
|
|
```
|
|
┌─▶ browser same-origin JSON / SSE
|
|
ServUO shard ──loopback TCP,──▶ uo-link sidecar ──WS + REST──▶ website backend ─┤
|
|
(servuo-plugins) newline-JSON (link, Rust) bearer-auth (+ Module-uo) └─▶ Android app REST + push (ntfy)
|
|
```
|
|
|
|
Three layers, and the shape is the same for any game:
|
|
|
|
1. **A game-side plugin** feeds a sidecar without ever letting the sidecar stall the game —
|
|
a bounded drop-oldest queue and a dedicated writer thread.
|
|
2. **A sidecar** owns the connection to the game and the durable copy of what it said. It is
|
|
the *listener*: the **game is never exposed to the internet** and only dials `127.0.0.1`.
|
|
The sidecar persists before it forwards, so a website that is down or mid-deploy loses
|
|
nothing and a page shows the last thing the game said instead of going blank.
|
|
3. **A website module** turns that feed into routes, tables and pages. The website process
|
|
never opens a connection to a game server — that is a rule in the module contract, not a
|
|
style preference.
|
|
|
|
- The **website backend** ingests a live event stream from the sidecar (logins, vitals,
|
|
economy, vendor sales, deaths, IDOC decay, staff audit…) and makes point-in-time REST
|
|
calls for rosters and character sheets. It then fans that out to browsers over
|
|
Server-Sent Events — a public channel (safe kinds only) and an admin channel (everything).
|
|
- The **Android app** is *just another client of the website backend* — it speaks the same
|
|
public REST API and never talks to the sidecar or the game. It self-configures its server URL
|
|
on first run, so one build works against any site, and receives push notifications through
|
|
the self-hosted **ntfy** relay (no Google Play Services required).
|
|
- Players **link** a game account to a website account with a one-time in-game code, which
|
|
is what authorizes character reads. Staff can push town-crier messages and control
|
|
commands back into the game.
|
|
- **Accounts are hardened out of the box** — TOTP two-factor with trusted devices, optional
|
|
SSO (Google / Discord / any OIDC provider, link-only: an external identity must already
|
|
belong to an account), bot scoring with automatic IP bans, and rate limiting.
|
|
- **Who sees which game data is configurable**, not hardcoded: an admin panel decides the
|
|
audience for each surface, and the public SSE channel can never carry a sensitive event kind.
|
|
|
|
The connection between website and sidecar (URL, shared-secret token, protocol version) is
|
|
**admin-managed in the database**, not env — the installer prints those values and you paste
|
|
them once into the site's **Admin → Shard** panel. If the sidecar is absent or the game is
|
|
down, every game surface degrades gracefully.
|
|
|
|
---
|
|
|
|
## Quick start
|
|
|
|
A live site is **three** things: the **website**, a **module** for your game, and the
|
|
**game side** — for Ultima Online, the sidecar plus the plugin, which the **installer**
|
|
deploys in a single run on the machine that hosts your ServUO server. The **Android-app**
|
|
is an optional client you point at your running website.
|
|
|
|
### 1. Website — the site + admin panel
|
|
|
|
Prereqs: **Node.js 20+** and **Docker** (for MariaDB).
|
|
|
|
```bash
|
|
git clone https://gitea.whitlocktech.com/RunicGateway/website.git
|
|
cd website
|
|
|
|
# Start a MariaDB the backend can reach
|
|
docker run -d --name rg-db -p 3306:3306 \
|
|
-e MARIADB_DATABASE=runic_gateway -e MARIADB_USER=runic \
|
|
-e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11
|
|
|
|
# Configure + start the backend (terminal 1)
|
|
cp server/.env.example server/.env
|
|
# set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=runic, DB_PASSWORD=devpass,
|
|
# JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
|
|
npm run install-server
|
|
npm run server # nodemon → http://localhost:3000
|
|
|
|
# Start the frontend (terminal 2)
|
|
npm run install-client
|
|
npm run client # Vite → http://localhost:5173
|
|
```
|
|
|
|
Open **http://localhost:5173**, sign in at **`/admin/login`** with the admin credentials
|
|
you set, then flip **Maintenance → Live** on the Dashboard. Tables, defaults, and the first
|
|
admin are created automatically on first boot.
|
|
|
|
> **Production (Docker Compose):** the website ships a production-shaped
|
|
> `docker-compose.yml` that *pulls* prebuilt `app` + `bot` images and serves the built SPA
|
|
> from Express — `cp .env.example .env`, fill it in, then
|
|
> `docker compose pull && docker compose up -d`. See the website README for the full options.
|
|
|
|
### 2. Your game — install a module
|
|
|
|
Nothing to build and no core release needed. In **Admin → Modules**, paste the URL of a
|
|
module release's install manifest; the site downloads the bundle it names, verifies its
|
|
`sha256`, unpacks it onto the modules volume, and offers you the restart that loads it. For
|
|
Ultima Online that manifest is `module-uo-<version>.json` from the
|
|
[Module-uo releases](https://gitea.whitlocktech.com/RunicGateway/Module-uo/releases):
|
|
|
|
```
|
|
https://gitea.whitlocktech.com/RunicGateway/Module-uo/releases/download/v0.3.0/module-uo-0.3.0.json
|
|
```
|
|
|
|
A compose-managed host can declare the set instead of clicking, with
|
|
`MODULES=uo@0.3.0=<that URL>` — resolution at container start is idempotent and offline-safe,
|
|
so a restart with the network down brings the site up exactly as it was.
|
|
|
|
> **The module system shipped on 2026-08-12** and this is now simply how the site works — a
|
|
> `main` checkout is a game-agnostic core with a Modules screen, and Ultima Online arrives as
|
|
> the module above or not at all.
|
|
|
|
**A game that is not Ultima Online?** That is what the
|
|
[**Integration-kit**](https://gitea.whitlocktech.com/RunicGateway/Integration-kit) is for —
|
|
a module template that builds and loads, and a book covering all three layers: the website
|
|
module, the sidecar, and the game-side plugin.
|
|
|
|
### 3. The shard side — one installer run
|
|
|
|
Prereqs: a **working ServUO install**, currently **stopped**, and Administrator/root. Nothing to
|
|
build and no Gitea account needed. Grab a binary and `SHA256SUMS` from the
|
|
[installer releases](https://gitea.whitlocktech.com/RunicGateway/installer/releases) — releases are
|
|
unsigned, so the checksum is the trust anchor:
|
|
|
|
```bash
|
|
sha256sum -c SHA256SUMS --ignore-missing
|
|
chmod +x runicgateway-installer-linux-x86_64
|
|
sudo ./runicgateway-installer-linux-x86_64 install
|
|
```
|
|
|
|
```powershell
|
|
# Windows, from an elevated PowerShell
|
|
.\runicgateway-installer-windows-x86_64.exe install
|
|
```
|
|
|
|
It resolves a published **bundle** — an exact sidecar + plugin pair CI has checked speak the same
|
|
protocol, rather than "latest of each" — syncs the plugin overlay into your server tree, offers the
|
|
optional stock-file patch tier, installs the sidecar and registers it as a service under an
|
|
unprivileged account, and finishes by printing the four values to paste into **Admin → Shard**:
|
|
|
|
```
|
|
Base URL http://<shard-host>:8080
|
|
WebSocket URL ws://<shard-host>:8080/ws
|
|
Protocol version 3
|
|
Auth token 4f9c…
|
|
```
|
|
|
|
Keep that binary — it does not install itself, and `doctor`, `update` and `uninstall` are run from
|
|
it later. Start ServUO, then `./runicgateway-installer-… doctor` checks the deployment end to end,
|
|
through to *"has the shard actually dialed in?"* — the only check that tells a working bridge from
|
|
copied files.
|
|
|
|
> Installing by hand is supported too — [INSTALL.md Appendix A](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md#appendix-a--installing-by-hand)
|
|
> is the same deployment with `curl`, `tar` and `systemctl`, for a host that cannot run the binary
|
|
> or for developing on the bridge from a source tree. The full operator guide is
|
|
> [`docs/installer/INSTALL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md).
|
|
|
|
### 4. Android-app — the native client (optional)
|
|
|
|
Prereqs: **JDK 17** and the Android SDK. A single build works against any site — the app
|
|
prompts for your website's URL on first run.
|
|
|
|
```bash
|
|
git clone https://gitea.whitlocktech.com/RunicGateway/Android-app.git
|
|
cd Android-app
|
|
./gradlew assembleDebug # debug APK → app/build/outputs/apk/debug/
|
|
./gradlew installDebug # install on a connected device / emulator
|
|
```
|
|
|
|
Kotlin + Jetpack Compose (Material 3), min SDK Android 10 (API 29). It surfaces the same
|
|
public content and player self-service as the browser — home/status, news, wiki, the shard
|
|
hub, account linking, character sheets — and opts into push notifications through the
|
|
self-hosted **ntfy** relay. See the [Android-app README](https://gitea.whitlocktech.com/RunicGateway/Android-app)
|
|
and the design contract in [`docs/android/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs).
|
|
|
|
---
|
|
|
|
## Where to go next
|
|
|
|
- **Putting a new game on the platform** → [Integration-kit](https://gitea.whitlocktech.com/RunicGateway/Integration-kit) — the template, and the book: the website module, the sidecar, the game-side plugin.
|
|
- **The module contract** → [`docs/website/MODULE_API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md) — normative: `module.json`, what a module is handed, the registries, the client chunk, the rules a loader enforces. [`MODULE_SYSTEM.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md) is the *why*.
|
|
- **Setting up a shard** → [`docs/installer/INSTALL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md) — the operator guide: what the run asks, where it writes, the patch tier, `doctor` / `update` / `uninstall`, troubleshooting, and the by-hand path.
|
|
- **Running / customizing the site** → [website README](https://gitea.whitlocktech.com/RunicGateway/website) (tech stack, API endpoints, env vars, security, branding, Swagger at `/api/docs`).
|
|
- **The Android client** → [Android-app README](https://gitea.whitlocktech.com/RunicGateway/Android-app) and [`docs/android/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs) — the authoritative design contract, milestones, and the push-notification architecture.
|
|
- **The bridge internals** → [docs](https://gitea.whitlocktech.com/RunicGateway/docs) — design docs, the canonical wire-protocol spec, and the integration guide.
|
|
- **Working on the sidecar or plugin** → [link README](https://gitea.whitlocktech.com/RunicGateway/link) and [servuo-plugins](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) — building from source, the loopback protocol, and the compatibility rules.
|
|
|
|
<div align="center">
|
|
<sub>Runic Gateway · whitlocktech@gmail.com</sub>
|
|
</div>
|