diff --git a/public/screens/admin-client-files.webp b/public/screens/admin-client-files.webp new file mode 100644 index 0000000..73c5484 Binary files /dev/null and b/public/screens/admin-client-files.webp differ diff --git a/public/screens/marketplace.webp b/public/screens/marketplace.webp index eb8212f..7d35217 100644 Binary files a/public/screens/marketplace.webp and b/public/screens/marketplace.webp differ diff --git a/public/screens/spawn-atlas.webp b/public/screens/spawn-atlas.webp index d82b851..7563b80 100644 Binary files a/public/screens/spawn-atlas.webp and b/public/screens/spawn-atlas.webp differ diff --git a/scripts/seedDemo.mjs b/scripts/seedDemo.mjs index c3ecd11..b9161cc 100644 --- a/scripts/seedDemo.mjs +++ b/scripts/seedDemo.mjs @@ -14,7 +14,7 @@ * UOLINK_BASE http://127.0.0.1:8080 sidecar REST, written to Admin → Shard * UOLINK_WS ws://127.0.0.1:8080/ws sidecar WebSocket * UOLINK_TOKEN (unset) sidecar auth token; skipped when absent - * UOLINK_PROTOCOL 4 wire protocol to pin — see the note below + * UOLINK_PROTOCOL (platform.json) wire protocol to pin — see the note below * * --------------------------------------------------------------------------------------- * WHY THE SEED DRIVES THE API AND NEVER THE DATABASE @@ -45,6 +45,8 @@ import { readFileSync } from 'node:fs'; +import platform from '../src/data/platform.json' with { type: 'json' }; + const BASE = (process.env.RG_BASE || 'http://localhost:3000').replace(/\/+$/, ''); const API = `${BASE}/api/v1`; const ADMIN_USER = process.env.RG_ADMIN_USER || 'demoadmin'; @@ -53,16 +55,21 @@ const DEMO_PASS = process.env.RG_DEMO_PASS || 'DemoReview!2026'; const UOLINK_BASE = process.env.UOLINK_BASE || 'http://127.0.0.1:8080'; const UOLINK_WS = process.env.UOLINK_WS || 'ws://127.0.0.1:8080/ws'; const UOLINK_TOKEN = process.env.UOLINK_TOKEN || ''; -// The pinned wire protocol has to be STATED, not left to the module's default. +// The pinned wire protocol, read from `platform.json` rather than written down here. // -// `module-uo`'s schema fragment still carries `protocol INT NOT NULL DEFAULT 3`, from the -// protocol-3 cutover; the sidecar on `link` `main` speaks 4. The module handles protocol 4's -// frames — `guild.roster` and `guild.leave` ingest landed with the Teams cutover — but a -// FRESH install pins 3, and the sidecar answers a 3 with `409 protocol version mismatch` on -// every REST call. So a new deployment reads nothing from its shard until somebody edits the -// number in Admin → Shard. Raised with the org lead rather than patched from here: the fix -// belongs in `module-uo`, not in this repo's screenshot rig (PLAN.md §13 phase 9). -const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || 4); +// It was a literal `4` until the Asset Bridge cutover, with a note explaining that +// `module-uo` pinned 3 on a fresh install while the sidecar spoke 4, so a new deployment +// read nothing from its shard until somebody edited the number in Admin → Shard. That debt +// has since been paid: the module's schema fragment defaults the column to the protocol its +// build speaks and carries a one-shot migration per bump, so both a fresh install and an +// upgraded one land on the right number by themselves. +// +// What remains is the rig's own reason to state it: this seed points a demo deployment at a +// sidecar, and if it pins the wrong number every REST call comes back `409`. Reading it from +// `platform.json` means the number is the one `checkFacts.mjs` verified against `link`'s +// `main` — so the rig cannot quietly drift two protocols behind the platform again, which is +// exactly what the literal did. +const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.protocol); const DRY = process.argv.includes('--dry-run'); diff --git a/src/config/sidebar.mjs b/src/config/sidebar.mjs index b7baadd..b954176 100644 --- a/src/config/sidebar.mjs +++ b/src/config/sidebar.mjs @@ -43,6 +43,7 @@ export const docsSidebar = [ { label: 'Message templates', slug: 'docs/administration/message-templates' }, { label: 'Managing modules', slug: 'docs/administration/managing-modules' }, { label: 'The shard connection', slug: 'docs/administration/the-shard-connection' }, + { label: 'Client files', slug: 'docs/administration/client-files' }, { label: 'Maintenance and upgrades', slug: 'docs/administration/maintenance-and-upgrades' }, { label: 'Troubleshooting', slug: 'docs/administration/troubleshooting' }, ], @@ -122,6 +123,7 @@ export const plannedSidebar = { 'Message templates', 'Managing modules', 'The shard connection', + 'Client files', 'Maintenance and upgrades', 'Troubleshooting', ], diff --git a/src/content/docs/docs/administration/client-files.mdx b/src/content/docs/docs/administration/client-files.mdx new file mode 100644 index 0000000..ddb08e9 --- /dev/null +++ b/src/content/docs/docs/administration/client-files.mdx @@ -0,0 +1,103 @@ +--- +title: Client files +description: Creature portraits, item pictures and the game's own name table — where they come from, the one button that imports them, and why nothing here happens on a restart. +--- + +import Screenshot from '../../../../components/Screenshot.astro'; +import { Aside } from '@astrojs/starlight/components'; + +Most of what a game shows you is not text. Ultima Online keeps its creature artwork, its +item graphics and even its item *names* inside the client files, and a site that cannot read +them shows a bestiary of words and a marketplace of numbers. + +With the `uo` module installed, **Client files** appears in the admin sidebar at +`/admin/uo/files`. It is where those three things arrive. + + + +## Where they come from + +A ServUO shard cannot boot without a UO client — it resolves one at startup to read the +world's own data. So the files were already on the shard host, and the shard reads and +decodes them there, handing the results over the bridge like everything else. + +**Nothing is converted on a desktop and nothing is uploaded.** Earlier versions of this +platform asked an operator to install a third-party tool, build a converter against it and +copy the output onto the web host. That path is gone. + +## Three things, one page + +| Section | Fills | How it arrives | +|---|---|---| +| **Creature portraits** | The bestiary and the spawn atlas | One picture per creature body, imported as a **set** | +| **Item and land pictures** | Marketplace listings and character sheets | **One at a time**, shortly after a page asks for one | +| **Item and title names (clilocs)** | Anywhere an item is named | The whole table at once — tens of thousands of names | + +They are one page because they are one job: they come out of one client install, and they +all change at the same moment — when you patch it. + + + +## Update, or re-import everything + +Every section offers the same pair, and the difference is worth knowing: + +- **Update** asks the shard what changed first and transfers only that. When nothing has, it + costs one small round trip and answers *"unchanged"*. +- **Re-import everything** fetches the lot. It is for the case the first cannot see — you + restored a backup, or lost the uploads volume, and the database still remembers pictures + that are no longer on disk. + +Item and land pictures work differently, because there are tens of thousands of item +graphics times every dye colour and importing them as a set would be absurd. They arrive +lazily instead. The two buttons there — *Fetch waiting pictures* and *Refresh the ones I +have* — exist for the two moments waiting is the wrong answer: you have just linked a shard, +or you have just patched a client. + +## When the page says something is wrong + +Every one of these is a reported state with a reason, not an error. The site keeps serving +whatever is already imported in all of them. + +| What you see | What it means | +|---|---| +| **The shard is busy with another client-file request** | Not a fault. The shard serves one of these at a time, and an import — or the item-picture pass refilling itself — is holding it. It frees itself. | +| **The shard is not answering for client files** | The ordinary bridge problem: see [The shard connection](/docs/administration/the-shard-connection/). | +| **…set `AssetsEnabled` on the shard** | The asset plane is switched off in [`Bridge.cfg`](/docs/reference/bridge-cfg/). It is a separate switch on purpose — turning it on is consenting to the website reading this host's client files. | +| **The shard host cannot render images** | A Linux host with no `libgdiplus`. Names are unaffected, because they have no pixels in them. | +| **Waiting for you: *n* pictures … no longer offered** | The shard stopped offering artwork this site holds. A deletion is never silent here; it waits for you to approve or dismiss it. | + + + +## What it will not do + +- **It never writes to the game.** Everything on this plane is a read. +- **It never overwrites your own artwork.** A portrait you drew and named yourself always + wins over an imported one. +- **Creatures with no artwork stay as text.** That is normal rather than a failure — a stock + client has no animation for most ghost and gargoyle bodies, and the shard reports nothing + rather than guessing. A wrong picture is worse than no picture. + +## Canonical documents + +[`link/v8.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.md) +is the asset plane's design of record; +[`link/SHARD_PREREQS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/SHARD_PREREQS.md) +covers what a shard host needs first, and +[`website/UPGRADE_NOTES.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/UPGRADE_NOTES.md) +is what to do on a site that was running before this existed. diff --git a/src/content/docs/docs/administration/maintenance-and-upgrades.mdx b/src/content/docs/docs/administration/maintenance-and-upgrades.mdx index ec64545..a167ac1 100644 --- a/src/content/docs/docs/administration/maintenance-and-upgrades.mdx +++ b/src/content/docs/docs/administration/maintenance-and-upgrades.mdx @@ -116,6 +116,13 @@ when ServUO needs restarting — it never restarts your shard itself. Because it **bundle**, the sidecar and the plugin move together and cannot end up disagreeing about the protocol. + + +## The bestiary has no pictures, or items show numbers + +Those come out of the UO client on the shard host, and **nothing imports them on a +restart** — a button on **Admin → Client files** is the only thing that does. Check that +page first: it reports why rather than failing. + +| What it says | What to do | +|---|---| +| Counts are zero and no import is recorded | Press *Update*. On a shard that was linked before this existed, nobody ever has. | +| *…set `AssetsEnabled` on the shard* | The asset plane is off in `Bridge.cfg`. It is a separate switch on purpose. | +| *The shard host cannot render images* | A Linux host with no `libgdiplus`. Install it and press *Update* again. Names are unaffected either way. | +| *The shard is busy with another client-file request* | Not a fault. Something ordinary holds the slot; it frees itself. | +| Pictures were fine and went blank | Check the uploads volume before anything else — the database still remembers pictures that are no longer on disk, and *Re-import everything* is the button for exactly that. | + +Items reading as numbers rather than names is the same page, different section: it means the +cliloc table has not been imported. See [Client files](/docs/administration/client-files/). + ## Teams are missing Check the sync panel on **Admin → Teams** before anything else: *last success: never* with diff --git a/src/content/docs/docs/architecture/protocol-versions.mdx b/src/content/docs/docs/architecture/protocol-versions.mdx index 893b863..7f67737 100644 --- a/src/content/docs/docs/architecture/protocol-versions.mdx +++ b/src/content/docs/docs/architecture/protocol-versions.mdx @@ -46,28 +46,35 @@ allowed to be chosen independently. ## What a bump obliges Changing a message shape means editing every side plus the specification. The most recent -bump touched five repositories: +bump — **8**, which taught the bridge to carry a game's own client files — touched four +repositories: | Repository | What had to change | |---|---| -| `servuo-plugins` | The handlers, the caps and switches in `Bridge.cfg`, and `overlay.toml` | -| `link` | `PROTOCOL_VERSION`, and the endpoints that carry the new commands | -| `module-uo` | The verbs it declares, their option sources, and the ingest | -| `website` | Core learned a shape it had not had — a lease aimed at one named target | +| `servuo-plugins` | The extractors and the decoders they call, the switches and caps in `Bridge.cfg`, and `overlay.toml` | +| `link` | `PROTOCOL_VERSION`, a cap on how large a line the shard may send, and the endpoints that carry the new commands | +| `module-uo` | The importers, the admin screen, and the pages that render a picture | | `docs` | The protocol document and the integration guide | -The `website` row is the one worth noticing. Core holds no game connection and names no -game noun, so most protocol bumps do not reach it at all — the two before this one did not. -This one did, because what changed was not a game *noun* but the shape of a thing core owns -the ledger for. +**`website` is not on that list, and its absence is the interesting part.** Core holds no +game connection and names no game noun, so most protocol bumps do not reach it at all. The +one before this did, because what changed then was not a game *noun* but the shape of a +thing core owns the ledger for. This one did not reach core because everything it needed — +somewhere to put a picture — core already offered every module. Its entire share of eight +phases of work was a **deletion**: a developer tool it no longer needed. **A protocol bump can also require a store migration**, because the sidecar persists what it forwards. That is not automatic, and it has happened once: version 4 added a column to a -table that already existed. Versions 5, 6 and 7 needed none, because every frame is +table that already existed. Versions 5, 6, 7 and 8 needed none, because every frame is persisted whole — a bump that only widens a frame, or adds a kind, or adds a guarantee about how a command is executed, asks nothing of a store that defines no schema for a frame's contents. That is the dumb-forwarder property paying for itself. +Version 8 puts it more sharply still. It is the largest bump this protocol has had, and it +moves megabytes of artwork rather than events — and it changed **no line** of the sidecar's +store, because the things it carries are answers to requests rather than events to keep. A +forwarder that holds no opinion about what it forwards has nothing to migrate. + ## This is not the module API version Two different numbers, versioning two different contracts, and confusing them is easy. @@ -102,9 +109,9 @@ What is worth inheriting is the **shape**: ## Canonical documents -[`link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v7.md) +[`link/v8.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.md) is the current protocol's record, including its cross-repository obligations, and -[`link/v6.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v6.md) +[`link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v7.md) the one before it; [`link/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §7 is the wire protocol, and diff --git a/src/content/docs/docs/architecture/the-bridge.mdx b/src/content/docs/docs/architecture/the-bridge.mdx index dcafcf3..e616358 100644 --- a/src/content/docs/docs/architecture/the-bridge.mdx +++ b/src/content/docs/docs/architecture/the-bridge.mdx @@ -68,7 +68,7 @@ responsibility. The sidecar is a **dumb forwarder** — it makes no access-contr and holds no policy. Access control and the admin-toggleable visibility scope live on the **website**, where an administrator can see and change them. -## Two ways in +## Three ways in **Live events** arrive over an outbound **WebSocket** and are routed by the module's ingest dispatcher. Kinds are handled differently by nature: state-changing kinds update tables, @@ -77,6 +77,23 @@ than accumulating history. **Point-in-time reads and commands** go over **REST**, through a client that never throws. +**Bulk reads** — a game's own client artwork, its string table, its spawn files — are the +newest and the least obvious. They go over the request/reply path in **pages**, with **one +request in flight at a time** and a hard cap on how large a single line may be. + + + Every call carries `Authorization: Bearer ` and an `X-UOLink-Version` header. **A protocol mismatch fails fast with `409`** rather than being mis-parsed — see [Protocol versions](/docs/architecture/protocol-versions/). diff --git a/src/content/docs/docs/getting-started/requirements.mdx b/src/content/docs/docs/getting-started/requirements.mdx index 233510f..ed24586 100644 --- a/src/content/docs/docs/getting-started/requirements.mdx +++ b/src/content/docs/docs/getting-started/requirements.mdx @@ -36,11 +36,20 @@ the [`uo` module](/docs/getting-started/install-a-game-module/) and the installe |---|---| | **A working ServUO install** | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. | | **ServUO {platform.bundle.servuoMin}** *(patch tier only)* | The base install works on any reasonably current ServUO. The optional patch tier is written and tested against stock {platform.bundle.servuoMin}; on any other version it is unsupported, and skipping it still leaves you with a working bridge. | +| **`libgdiplus`** *(Linux shard hosts only)* | Only needed for **artwork**. ServUO runs under Mono on Linux, and the library it decodes sprites with is a thin layer over this one — in the decode path, not merely the encode. Without it creature portraits and item pictures stay empty, and names and the spawn atlas are unaffected because neither touches a pixel. `sudo apt-get install libgdiplus`; `doctor` checks for it. **Windows hosts need nothing.** | | **The shard stopped** | `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit. The installer refuses to deploy under a running shard. | | **Administrator / root** | It writes into system directories and registers a service. | | **Outbound HTTPS** | To fetch the bundle and its two artifacts. No Gitea account and no git client are needed. | | **The sidecar on the same host as the shard** | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — that loopback socket *is* the trust boundary for inbound commands. | + +