Compare commits

...

12 Commits

Author SHA1 Message Date
2719716c9c Merge pull request 'docs(readme): list the Rust repositories and runicgateway.com' (#7) from docs/repo-table-rust into main
Reviewed-on: #7
2026-09-30 02:05:40 +00:00
8f96dde7ee docs(readme): list the Rust repositories and runicgateway.com
The landing page still said "eight repositories". The org has thirteen: add
Module-Rust, Rust-Link, Rust-Plugins and the new runicnpc-rust (RunicNPC,
docs/runicnpc/PLAN.md stage 0) as a Rust section beside the UO one, and
runicgateway.com with the platform repos. The intro names Rust as the second
game. The quick start is unchanged and still walks the UO path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-29 21:03:43 -05:00
5deaf74049 Merge pull request 'docs(readme): the event system, protocol 7, and module-uo v1.2.2' (#6) from docs/events-system into main
Reviewed-on: #6
2026-09-10 04:28:33 +00:00
0a7d138daa docs(readme): the event system, protocol 7, and module-uo v1.2.2
Phase 16c of the events plan. The landing page is updated when the shape of the
project changes, and a subsystem that lets staff schedule an unattended change to
a live game world is one.

* **A new bullet in "How they fit together"**, beside the engagement one it sits
  next to: an event is written once as phases and steps, published as an
  immutable version and executed unattended. What matters on a front page is the
  posture rather than the feature list — everything arrives switched off, caps are
  enforced in the database rather than in a role check, cleanup is generated from
  a ledger rather than authored, and an event does not edit the world but holds a
  **lease** the game restores on its own deadline even if the site never speaks to
  it again. The game keeps its own switch, separate from the staff write plane.
* **Protocol 5 → 7** in the four values the installer prints. That block is what a
  reader copies into Admin → Shard, so a stale number there is the one that costs
  somebody an afternoon.
* **module-uo v1.1.0 → v1.2.2** in the manifest URL and the `MODULES=` line.
* `docs/website/EVENTS.md` added to "Where to go next".

Verified against the platform rather than assumed: the protocol number is
`link/sidecar/src/main.rs` and `servuo-plugins/overlay.toml` on `main`, and
v1.2.2 is the current Module-uo release, whose own notes name
`module-uo-1.2.2.json` as the manifest to paste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-09 22:04:39 -05:00
159eb53e73 Merge pull request 'docs(readme): the engagement system, protocol 5, and module-uo v1.1.0' (#5) from docs/engagement-and-protocol-5 into main
Reviewed-on: #5
2026-09-01 18:55:28 +00:00
029ba2893e docs(readme): the engagement system, protocol 5, and module-uo v1.1.0
The engagement cutover landed on 2026-09-01 and three things on this page stopped
being true with it.

The capability itself is the reason for the change: the site now talks to its
players, and an operator decides when. It is a headline capability by the org
lead's call, which is the condition ENGAGEMENT.md Phase 13 step 8 attaches to
touching this file at all -- a landing page that lists every feature is a landing
page nobody reads.

The bullet leads with the operator rather than the mechanism, and names the one
property that is a security boundary rather than a feature: a trigger declares
the widest audience a rule may ever be given, so a sensitive game event cannot be
mailed to everyone by a misconfiguration.

The other two are cutover outputs and would mislead an operator directly:

  * the installer's sample output prints "Protocol version 4"; the paired bundle
    is 2026.09.01 and the wire is 5.
  * the quick start pins module-uo v1.0.1 in both the manifest URL and the
    MODULES= line; v1.1.0 is the release that carries the shard triggers.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 13:15:13 -05:00
c9ba2d2fd3 Merge pull request 'docs(readme): protocol 4, and module-uo v1.0.1 in the quick start' (#4) from docs/protocol-4-and-versions into main
Reviewed-on: #4
2026-08-19 23:26:07 +00:00
327bb68091 docs(readme): protocol 4, and module-uo v1.0.1 in the quick start
The landing page is the project's front door, and both of its
copy-pasteable values were stale.

The installer handoff block printed "Protocol version 3". That block
exists to be typed into Admin -> Shard, and protocol 4 shipped on
2026-08-19 as sidecar v2.0.0 and overlay v1.0.0. A stale number there
comes back from the sidecar as a 409, which INSTALL.md's own
troubleshooting table notes "looks exactly like your shard going
offline".

The module quick start still pointed at the v0.3.0 install manifest and
MODULES=uo@0.3.0. module-uo is on v1.0.1; asset names verified against
the release.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 17:59:33 -05:00
545590e7ff Merge pull request 'docs(readme): the module system shipped' (#3) from docs/module-system-shipped into main
Reviewed-on: #3
2026-08-12 23:05:24 +00:00
bd2e1df84a docs(readme): the module system shipped
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>
2026-08-12 18:04:16 -05:00
19f6ba018a Merge pull request 'docs: the front door is a platform, not a UO bridge' (#2) from docs/module-system-reframe into main
Reviewed-on: #2
2026-08-12 19:33:36 +00:00
aa2245b315 docs: the front door is a platform, not a UO bridge
The org landing page still opened with "A website + game bridge for private
Ultima Online (ServUO) shards" and listed six repositories. The module system
made the first untrue and Phase 5 made the second wrong by two: Module-uo and
Integration-kit.

Reframed rather than patched. Runic Gateway is a website platform for game
communities that knows nothing about any particular game; everything
game-specific arrives as an installable module, and Ultima Online is the first
one. The repo table splits accordingly — the platform (website, Android-app,
docs, Integration-kit) and UO support (Module-uo, link, servuo-plugins,
installer) — and "How they fit together" now leads with the three layers any
game needs (plugin, sidecar, module) before the UO specifics, including the rule
that the website process never opens a connection to a game server.

Quick start gains step 2: install a module. Paste a release's install-manifest
URL into Admin -> Modules, or declare MODULES=<id>@<version>=<url> on a
compose-managed host. Both were checked against server/.env.example rather than
written from memory.

One honest blockquote says the module system lives on the website's `edge`
branch until the cutover, so nobody clones `main` looking for a Modules screen
that is not there yet.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 14:29:25 -05:00

142
README.md
View File

@@ -2,17 +2,17 @@
# Runic Gateway
**A website + game bridge for private Ultima Online (ServUO) shards.**
**A website platform for game communities, wired to the live game.**
Runic Gateway is a self-hostable platform that gives a UO shard a public site, wiki,
and admin panel — and wires it to the *live in-game world* so the site can show shard
status, economy, IDOCs, player activity, per-character sheets, a player-vendor marketplace,
and a bestiary built from the shard's own spawn tables, while staff push control commands
back into the game. Players get the same public content and self-service on the web **or**
a native Android app. Which audience sees which shard data is admin-configurable, and
branding is per-instance.
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.
One installer sets up the shard side.
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, *Rust* (Oxide and Carbon) the second, and one installer sets up the game side of either.
</div>
@@ -20,58 +20,104 @@ One installer sets up the shard side.
## The pieces
Runic Gateway is six repositories that deploy together but build independently:
Runic Gateway is thirteen 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). This is the thing players and staff actually visit, and the single backend every client talks to. |
| [**link**](https://gitea.whitlocktech.com/RunicGateway/link) | Rust | The **uo-link** sidecar. Runs next to the shard, terminates a loopback link from the game, and exposes the authenticated WebSocket + REST API the website consumes. The only network-facing half of the bridge. |
| [**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. |
| [**runicgateway.com**](https://gitea.whitlocktech.com/RunicGateway/runicgateway.com) | Astro | The platform's own **public site** at [runicgateway.com](https://runicgateway.com) — what Runic Gateway is, how to install it, and the developer docs. Not a game site. |
**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. |
| [**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 the sidecar or shard. |
| [**docs**](https://gitea.whitlocktech.com/RunicGateway/docs) | Markdown | All project documentation — design docs, the wire-protocol spec, the integration guide, the Android plan, research. Start here when you want the *why*. |
**Rust support** — the second game, the same three layers, built from the Integration-kit:
| Repo | Language | What it is |
|------|----------|------------|
| [**Module-Rust**](https://gitea.whitlocktech.com/RunicGateway/Module-Rust) | JavaScript (Node + React) | The **Rust module** — everything that makes a site a site for Rust. Installed into a website as `modules/rust/`; released as a versioned bundle. |
| [**Rust-Link**](https://gitea.whitlocktech.com/RunicGateway/Rust-Link) | Rust | The **rust-link** sidecar — the Rust mirror of uo-link: terminates the game plugin's loopback link and serves the WebSocket + REST API the website consumes. Ships a Pterodactyl egg. |
| [**Rust-Plugins**](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins) | C# | The **bridge plugin** for Oxide and Carbon — the game side of the bridge. Dials the sidecar over loopback. |
| [**runicnpc-rust**](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust) | C# | **RunicNPC** — Runic Gateway's own NPC plugin for Rust: an API other plugins call and in-game admin commands, with NPCs equipped through Kits. Works on its own too. *In development.* |
## 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 (website, Node) └─▶ Android app REST + push (ntfy)
(servuo-plugins) newline-JSON (link, Rust) bearer-auth (+ Module-uo) └─▶ Android app REST + push (ntfy)
```
- The **shard is never exposed to the internet.** It only dials `127.0.0.1`. The **sidecar**
is the sole network-facing component, and only the website's backend talks to it.
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 shard. It self-configures its server URL
on first run, so one build works against any shard, and receives push notifications through
the shard's self-hosted **ntfy** relay (no Google Play Services required).
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.
- **The site talks to its players, and an operator decides when.** Anything notable — in the
platform or in the game — can be declared as a **trigger**; an operator writes rules against
triggers in the admin panel and picks who is told and on which channel: **email** through any SMTP
provider, an **on-site inbox**, or a **push** to the Android app. Bodies are editable templates
with a live preview, every rule a module ships arrives **switched off**, and each trigger declares
the widest audience a rule may ever be given — so a sensitive game event cannot be mailed to
everyone by a misconfiguration. A module brings its own triggers: for Ultima Online that is
a house about to collapse, a vendor running out of gold, a champion spawn, a new governor.
- **Staff can put an event on the calendar and let it run itself.** An event is written once
as phases and steps, published as an immutable version, and executed unattended — announcing
itself, spawning what it needs, borrowing values the world already had, counting who took
part and publishing the results. Everything it may do arrives **switched off**, every run
spends against per-run caps enforced in the database rather than in a role check, and every
world write is ledgered so the undo is **generated rather than authored** and runs on
completion, cancellation and abort alike. An event does not edit the world; it holds a
**lease** the game restores on its own deadline — even if the site never speaks to it again.
The game keeps its own switch: scheduled events are off in `Bridge.cfg` until an operator
turns them on, separately from the staff write plane.
- **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 shard data is configurable**, not hardcoded: an admin panel decides the
audience for each shard surface, and the public SSE channel can never carry a sensitive
event kind.
- **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 shard is
down, every shard surface degrades gracefully.
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
The core of a live shard is **two** things running: the **website** (site + admin), and the
**shard side** — 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.
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
@@ -107,7 +153,32 @@ admin are created automatically on first boot.
> 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. The shard side — one installer run
### 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/v1.2.2/module-uo-1.2.2.json
```
A compose-managed host can declare the set instead of clicking, with
`MODULES=uo@1.2.2=<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
@@ -133,7 +204,7 @@ unprivileged account, and finishes by printing the four values to paste into **A
```
Base URL http://<shard-host>:8080
WebSocket URL ws://<shard-host>:8080/ws
Protocol version 3
Protocol version 7
Auth token 4f9c…
```
@@ -147,9 +218,9 @@ copied files.
> 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).
### 3. Android-app — the native client (optional)
### 4. Android-app — the native client (optional)
Prereqs: **JDK 17** and the Android SDK. A single build works against any shard — the app
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
@@ -161,7 +232,7 @@ cd Android-app
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 shard's
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).
@@ -169,9 +240,12 @@ and the design contract in [`docs/android/PLAN.md`](https://gitea.whitlocktech.c
## 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 event system** → [`docs/website/EVENTS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/EVENTS.md) — the design of record: leases, the resource ledger, the caps, and the module seam an event's world verbs arrive through.
- **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.