docs(link): the guide an integrator reads stopped at protocol 6 (Phase 9a)
The Asset Bridge's docs pass, and the acceptance walk that shaped it (v8.md §16 row 9a, §17.13-14). Phase 9 is three legs now: this one, the edge->main cutover, and the site. ## INTEGRATION.md had stopped at 6 and contradicted itself Its §2 said "the current version is 6" above examples already carrying `X-UOLink-Version: 8`, there was no protocol-7 paragraph, and `assets.` appeared zero times in 1,306 lines. It is the only document an integrator outside this org has, so it is carried the whole way: the version block corrected, v7 (the event plane's command half) and v8 (the asset plane) written, a §5 section for the five routes, 425/422 in the status table, and a caveat that the asset plane is a working set rather than a stream. Protocol 7's absence is the Events workstream's debt rather than this one's, but it cannot be stepped over on the way to 8. ## The operator-facing half `UPGRADE_NOTES.md` gains the entry an operator reads when this ships: what changed, the one required action on a Linux host, and the thing that will not announce itself -- nothing here happens on a restart, so a patched client keeps serving the old pictures until somebody presses a button. `installer/INSTALL.md` gains libgdiplus as a prerequisite row and the `doctor` row that checks it. The index rows for SPAWN_ATLAS and CLILOCS described the workflows this protocol deleted; v8.md now has an index row of its own, and v7 is marked as the released protocol. ## The walk Wiped every asset row and every imported sprite, then walked it as a new operator: 1,095 portraits in 3.18 s, 67,496 names in 1.42 s, 313 item pictures in 1.38 s, the atlas over the bridge in ~2.0 s, an Update with no drift answered in 0.99 s. Bestiary portraits are the right animals by eye; the marketplace shows hued item art with cliloc names. It found two defects (§17.14) and one cutover hazard: module-uo's `edge` is behind its `main`, missing #35, so the walk measured 0 of 6,455 spawners carrying a UniqueId. 9b's row says to sync before merging or the cutover ships a regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -55,14 +55,14 @@ That is the same set of values Admin → Shard asks for — base URL and WS URL
|
||||
|
||||
The wire protocol is versioned so a mismatch is caught immediately instead of failing weirdly.
|
||||
|
||||
The current version is **6**. It is not released yet — it lives on `edge` and ships with the event system's cutover; the last released pairing is protocol **5**, sidecar **v2.1.0** + overlay **v1.1.0**, resolved as bundle **2026.09.01**, never as "latest of each".
|
||||
The current version is **8**. It is not released yet — it lives on `edge` and ships with the Asset Bridge's cutover; the last released pairing is protocol **7**, sidecar **v2.2.0** + overlay **v1.2.0**, resolved as bundle **2026.09.10**, never as "latest of each".
|
||||
|
||||
- Every response carries an **`X-UOLink-Version: 8`** header.
|
||||
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 6`.
|
||||
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 8`.
|
||||
- **Optionally**, send `X-UOLink-Version: 8` on your requests. If it disagrees with the sidecar, the request is rejected **409 Conflict**:
|
||||
|
||||
```json
|
||||
{ "error": "protocol version mismatch", "sidecar_protocol": 6, "client_protocol": "5" }
|
||||
{ "error": "protocol version mismatch", "sidecar_protocol": 8, "client_protocol": "7" }
|
||||
```
|
||||
|
||||
Pin the version you built against and compare it to the header (or `/health.protocol`) at startup.
|
||||
@@ -118,6 +118,41 @@ the work.
|
||||
Additive again: no existing field changed shape, **no new endpoint**, and a client that sends no key
|
||||
behaves exactly as it did under v5.
|
||||
|
||||
**v7 (Protocol 7)** adds the **event plane's command half** ([`v7.md`](v7.md)) — the verbs a
|
||||
website-authored event needs in order to happen in the world: spawn a bounded, named, hued set of
|
||||
creatures; place decoration from the shard's own `Data/Decoration` vocabulary; open a temporary
|
||||
gate; run an oracle NPC's dialogue; grant an item to a run's participants; lease a property on an
|
||||
existing spawner; toggle one of ServUO's seasonal events; start a world save. Every one of them is
|
||||
capped on the shard, ledgered so it can be reverted, and gated by `Bridge.EventsEnabled` — an
|
||||
operator who has not switched the plane on gets **403**, not a half-applied event.
|
||||
|
||||
Additive on the feed, like every bump before it, and the new endpoints require a v7 sidecar. The
|
||||
one thing to know as a consumer is that these are **world writes**, so they are exactly the calls
|
||||
v6's `idempotencyKey` exists for: send one, and a retry after a lost acknowledgement is answered by
|
||||
the original reply instead of spawning the creatures twice.
|
||||
|
||||
**v8 (Protocol 8)** adds the **asset plane** ([`v8.md`](v8.md)) — the shard reads its own UO client
|
||||
files and serves what is in them, so nothing has to be converted on somebody's desktop and no
|
||||
component but the shard ever needs a copy of the client. It carries four things: creature and
|
||||
player-body artwork, item and land art on demand, the decompressed cliloc table, and the shard's own
|
||||
`Spawns/*.xml` tree. See §5's *Client assets* for the routes.
|
||||
|
||||
Three properties of this plane are unlike the rest of this API, and a consumer that does not know
|
||||
them will misread healthy behaviour as failure:
|
||||
|
||||
- **The shard serves one asset request at a time**, and says so: `425 Too Early` is *flow control*,
|
||||
the ordinary answer during an import rather than a rare collision. Back off and retry; do not
|
||||
treat it as an error and do not abandon a transfer over it.
|
||||
- **Every response is paged, and the caller drives the paging** — echo the previous reply's
|
||||
`cursor` until one says `more: false`, then read `cut` to learn why that page was the last. Only
|
||||
`cut: "end"` means you have the whole thing.
|
||||
- **A key the shard cannot serve comes back as a row with a `status`, not as a failed request.** A
|
||||
body this client has no art for is the expected answer for most ghost and gargoyle bodies on a
|
||||
stock client; failing a page over one would make an import impossible.
|
||||
|
||||
Additive again — no existing field changed shape, and a v7 client that never calls `/assets/*` or
|
||||
`/cliloc` behaves exactly as it did.
|
||||
|
||||
**Upgrading a pinned client.** Every bump is an operator-visible hard break in one direction only: a
|
||||
client still declaring the old number gets a 409 on every protected route and, on the WebSocket, a
|
||||
closed connection on the `ws.hello` mismatch. So update the pinned version at the same time you
|
||||
@@ -743,7 +778,7 @@ Full character sheet: stats, all trained skills, worn equipment with flattened i
|
||||
Field notes:
|
||||
- `skills[].base` is trained value, `value` includes item/temp bonuses, `cap` is the cap. **Do not assume `base <= cap`** — GM characters can exceed it.
|
||||
- `equipment[].mods` is a flattened map of every non-zero AOS attribute on the item (weapon or armor). Empty `{}` for plain items.
|
||||
- Item names are usually **clilocs**, not strings: use `name` when present, otherwise resolve `cliloc` against a UO cliloc table on the site. **Do not expect the shard to resolve them for you** — on any modern client ServUO's own `Ultima.StringList` cannot read the client's compressed cliloc files, so `VendorSearch.GetItemName` returns `item.Name` and the in-game Vendor Search gump has the same gap. Building that table is a consumer-side job; the website's is described in [`website/CLILOCS.md`](../website/CLILOCS.md).
|
||||
- Item names are usually **clilocs**, not strings: use `name` when present, otherwise resolve `cliloc` against a UO cliloc table on the site. **Do not expect the shard to resolve them for you per item** — on any modern client ServUO's own `Ultima.StringList` cannot read the client's compressed cliloc files, so `VendorSearch.GetItemName` returns `item.Name` and the in-game Vendor Search gump has the same gap. As of Protocol 8 the shard will hand you the whole table instead: `GET /cliloc` (§5), which is where the website's own comes from ([`website/CLILOCS.md`](../website/CLILOCS.md)).
|
||||
- `titles` (Protocol 2.0): `selected` is the index into `reward` currently displayed (`-1` if none). `fameKarma`/`skill` are computed display titles, omitted when the character has none. `reward` entries may be a **cliloc number as a string** or a literal string — resolve numeric ones against your cliloc table, same as item names.
|
||||
- Errors: unknown account → **404** `{"kind":"bridge.error","reason":"unknown account"}`; bad slot → **404**/**400** similarly.
|
||||
|
||||
@@ -778,6 +813,90 @@ Every player vendor owned by any character on the account, with held gold and cu
|
||||
] } ] }
|
||||
```
|
||||
|
||||
### Client assets — artwork, names and the shard's own files (Protocol 8)
|
||||
|
||||
Five routes, and they are unlike everything above them: they read the **UO client installed on the
|
||||
shard host** rather than the live world. Nothing here changes unless an operator patches that
|
||||
client, so these are the only reads on this API you should cache indefinitely and refresh on an
|
||||
event you decide, not on a timer. The design is [`v8.md`](v8.md); what follows is what a consumer
|
||||
needs.
|
||||
|
||||
Everything on this plane obeys three rules stated in §2: **425 is flow control**, **the caller
|
||||
drives the paging** (`more` / `cursor` / `cut`), and **an unserveable key is a row, not a failure**.
|
||||
|
||||
```
|
||||
GET /assets/sources # what the shard's client files currently are
|
||||
GET /assets/manifest?family=&cursor= # one row per asset: key, hash, size — no pixels
|
||||
POST /assets/fetch # the bytes, for keys you name
|
||||
POST /assets/bodies # creature class name → body id
|
||||
GET /cliloc?lang=&cursor= # the decompressed cliloc table
|
||||
```
|
||||
|
||||
**`GET /assets/sources` is the gate, and you call it first.** It is cheap, it touches no pixels, and
|
||||
its answer decides whether there is anything to do at all:
|
||||
|
||||
```json
|
||||
{ "kind":"assets.sources.ok", "assetsEnabled":true, "treeEnabled":true,
|
||||
"extractorVersion":3, "families":["body","land","static","tree"],
|
||||
"hashing":false, "complete":true, "imaging":{"ok":true},
|
||||
"files":[ {"name":"anim.mul","size":194950053,"mtime":1778566017000,"sha256":"2d89…"} ],
|
||||
"more":false, "cut":"end" }
|
||||
```
|
||||
|
||||
- **`files[].sha256` is the whole update story.** Store the set; on the next run, compare. Equal
|
||||
means the client has not been patched and there is nothing to transfer — which is the normal case
|
||||
and must cost one round trip, not a re-download.
|
||||
- **`hashing: true` means "not computed yet", never "changed".** The shard fingerprints a few
|
||||
hundred megabytes in the background after a restart; a null hash during that window is an absence
|
||||
of an answer, not an answer of absence.
|
||||
- **`extractorVersion` is ours, not the client's.** It changes when the shard's *derivation* changes
|
||||
— a better reader, a different frame — so a bump invalidates stored pictures even though the
|
||||
client files are byte-identical. Treat a change in it exactly like a changed hash.
|
||||
- **`families` says what this overlay can serve.** An older overlay answers `["body"]`; asking it
|
||||
for `static` fails per key, per pass, forever. Read this and say "update your plugin" instead.
|
||||
- **`imaging.ok: false` is `NO_IMAGING`** — a Linux shard host with no `libgdiplus` cannot decode a
|
||||
sprite at all ([`SHARD_PREREQS.md`](SHARD_PREREQS.md)). Names and tree files are unaffected: they
|
||||
have no pixels in them.
|
||||
|
||||
**`GET /assets/manifest?family=body`** lists what exists, with a hash and a size and no pixels — so
|
||||
you can diff it against what you hold and fetch only what moved. Rows carry
|
||||
`{key, sha256, bytes, width, height, body, action, source}`; `source` is `legacy` or `uop` (which
|
||||
reader produced the bytes) and `action` is which animation action the thumbnail came from, because a
|
||||
body with nothing at action 0 is catalogued at the first action that has anything — `body/820/a23`
|
||||
is a horse. This family pages on the shard's wall clock, so expect several pages of a few hundred
|
||||
rows.
|
||||
|
||||
**`POST /assets/fetch`** takes `{"keys":[…], "catalog":"…", "cursor":"…"}` and returns a row per key
|
||||
carrying the sprite as **base64 PNG** (the `tree` family returns gzipped chunks instead). Two things
|
||||
are load-bearing:
|
||||
|
||||
- **Pass back the `catalog` id the manifest gave you.** It is derived from the client files
|
||||
themselves, and it makes the shard refuse (**422**) if those files moved mid-import. Without it,
|
||||
an operator who patches their client halfway through gets one asset set stitched out of two, with
|
||||
no error anywhere.
|
||||
- **A batch must be of one family**, derived from the keys rather than named as a field. Mixing is
|
||||
refused (400), because a reply carries a single `catalog` and two families have two fingerprints.
|
||||
|
||||
**`POST /assets/bodies`** takes `{"types":["GiantSpider", …]}` and answers the one question only code
|
||||
running inside ServUO can: which body id a creature class actually uses. It runs on the Core thread,
|
||||
so the shard **caps the batch and refuses rather than truncates** — a 400 here names the cap; chunk
|
||||
your list.
|
||||
|
||||
**`GET /cliloc?lang=enu`** is the client's id → text table, decompressed on the shard and delivered
|
||||
as rows (`[{n, f, t}]`) inside the ordinary paging envelope. Blank entries are omitted — roughly
|
||||
56,000 of them — because every consumer discards them anyway. This is the table you resolve
|
||||
`cliloc` numbers against in `vendor.listing`, `char.profile` equipment and title rows; §6's note
|
||||
that the shard cannot resolve them for you is **why this route exists**.
|
||||
|
||||
Two absences worth stating, because both look like bugs and neither is:
|
||||
|
||||
- **A body with no art is normal**, and on a stock client that is most ghost and gargoyle bodies.
|
||||
The row says so; render text.
|
||||
- **The shard never sweeps file types looking for a hit.** If the client's own mapping yields
|
||||
nothing, the answer is nothing — asking the other animation files for the same index returns a
|
||||
*different creature's* art that reports success, which is a wrong picture nothing downstream can
|
||||
detect ([`v8.md`](v8.md) §4.6).
|
||||
|
||||
---
|
||||
|
||||
## 6. REST — commands & history
|
||||
@@ -1254,13 +1373,13 @@ sidecar defines no audiences. Deciding who may see what is the consuming site's
|
||||
| 200 | OK |
|
||||
| 400 | Bad request (malformed body, invalid parameter, or a shard `*.error` that isn't a not-found) |
|
||||
| 401 | Missing or invalid auth token |
|
||||
| 403 | Refused by the operator — the admin write plane, or the event plane (`Bridge.EventsEnabled`), is switched off on the shard |
|
||||
| 404 | Not found (unknown account / character / id, a not-linked account, an unoffered lease key, or a run the shard is not counting) |
|
||||
| 403 | Refused by the operator — the admin write plane, the event plane (`Bridge.EventsEnabled`), or the asset plane (`Bridge.AssetsEnabled` / `Bridge.TreeEnabled`), is switched off on the shard |
|
||||
| 404 | Not found (unknown account / character / id, a not-linked account, an unoffered lease key, a run the shard is not counting, or a client file this install does not have) |
|
||||
| 409 | Conflict — protocol version mismatch, or an account name already taken on `POST /accounts/create` |
|
||||
| 425 | Too Early — a command with this `idempotencyKey` is still in flight on the shard (Protocol 6). Nothing ran; retry |
|
||||
| 425 | Too Early — a command with this `idempotencyKey` is still in flight (Protocol 6), or the shard's single asset slot is held by another `/assets/*` or `/cliloc` call (Protocol 8). Nothing ran; back off and retry |
|
||||
| 429 | Too many requests — the shard's per-IP account cap was hit on `POST /accounts/create` |
|
||||
| 500 | Internal error (e.g. database) |
|
||||
| 503 | Shard not connected — the query needs the live game and it's down |
|
||||
| 503 | Shard not connected — the query needs the live game and it's down. On the asset plane it also carries `NO_IMAGING`: the host cannot render images at all |
|
||||
| 504 | Shard connected but didn't reply within 10s |
|
||||
|
||||
`503` vs `404`: a `503` is transient (shard restarting — retry), a `404` is a real "doesn't exist."
|
||||
@@ -1270,6 +1389,10 @@ deployment fault — your pinned protocol version disagrees with the sidecar's
|
||||
never help. A `425` is a retry that will succeed on its own. They are deliberately different codes
|
||||
so a retry loop cannot quietly swallow a mismatched deployment.
|
||||
|
||||
`422` exists only on the asset plane, and it means one of two things, both of which are the client
|
||||
files moving under you: a file the shard cannot decode, or a `catalog` id that no longer describes
|
||||
what is on disk — the mid-import guard. Start the import again rather than retrying the page.
|
||||
|
||||
---
|
||||
|
||||
## 8. Putting it together
|
||||
@@ -1304,3 +1427,8 @@ const sales = await fetch(`${BASE}/history?kind=vendor.sale&limit=20`, { headers
|
||||
- **Cache freshness.** `GET /char/serial/...` may serve a stale cached profile when the shard is down; the account+slot form always goes live (503 if down).
|
||||
- **`bootId`** on `server.hello` is your signal to invalidate site-side caches: if it changed, the shard restarted.
|
||||
- **Protocol changes** bump `X-UOLink-Version`. Compare it on startup and fail fast rather than mis-parsing a newer shape.
|
||||
- **The asset plane is a working set, not a stream.** `/assets/*` and `/cliloc` read files that only
|
||||
change when an operator patches the shard's UO client, and the shard serves one such request at a
|
||||
time. Import on an operator's action or a hash change — never on your own boot, and never on a
|
||||
timer: a client-file pass costs hundreds of megabytes of hashing to discover that nothing moved,
|
||||
and it holds the slot every other caller is waiting for.
|
||||
|
||||
Reference in New Issue
Block a user