docs(link): clilocs come over the bridge now, and UOFiddler's first job is gone

Phase 2 of the Asset Bridge is built, so the documentation stops telling an
operator to install a GUI tool.

`v8.md` gains §9.1 and §9.2 — what the port cost, what it measured, and where the
base table comes from now. The measurement worth keeping: **67,496 rows in
290 ms**, which is exactly what UOFiddler's own `Ultima.dll` produced from this
same client through the converter this phase deletes. An independent
implementation agreeing to the row is the strongest check available that a format
decoder is correct, and it is not something a subtly-wrong one produces.

§17 records the four shapes the org lead settled before any of it was written.
Two departed from the recommendation: **the bridge always wins** (no source
setting — there is no version of that question an operator benefits from
answering) and **import is admin-triggered** (boot does not call the shard at
all).

`CLILOCS.md` is rewritten around that: where the table comes from, what arrives
and in how many pieces, the refusals — including the two the file pipeline had no
equivalent of (a client patched mid-import, and the base's exemption from the
vanished-source rule, which exists so an upgraded install is not asked to approve
a change the upgrade itself made).

`UOFIDDLER.md` loses Part 1 entirely rather than having it rewritten. What is
left is creature art, which phase 5 takes, after which the page goes away. `v3.md`
§8.6 keeps its reasoning with a note saying what superseded it, because the
argument for why the manual step existed is still the argument for why this was
worth building.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-10 11:13:56 -05:00
parent df9b3fd990
commit bbd69a8e2e
6 changed files with 312 additions and 336 deletions

View File

@@ -714,6 +714,84 @@ Protocol 8 writes rather than calls (§4) — and from that point:
- `website/server/tools/cliloc-export/` is retired, and `UOFIDDLER.md` is deleted rather than
rewritten.
### 9.1 What phase 2 built, and what the port cost
**Built 2026-09-10.** `BridgeCliloc.cs` in the overlay, `GET /cliloc` in the sidecar, and a paging
walk in module-uo that merges the shard's table under the `custom/` overlays.
The port is `MythicDecompress` + `MoveToFront` rewritten against plain arrays: the upstream is
`Span<T>` / `ArrayPool<T>` / `BinaryPrimitives` code and ServUO targets `net48`, which has none of
them without a package this tree does not vendor. The algorithm is unchanged, **including the parts
that read oddly** — the three-region count/cursor/end table and the symbol-table shifts are
upstream's, deliberately, because a tidier rewrite of somebody else's format decoder is a chance to
be subtly wrong in a way that produces plausible text.
Two bounds checks were added, and they are the only behavioural change: the upstream indexes its
payload without checking, which is safe for a file the client wrote and is not safe for a file this
shard was handed. A truncated container now reports `UNREADABLE` with the byte it wanted, instead of
throwing an `IndexOutOfRangeException` from inside a decoder.
**Measured on this machine's stock client:** 4,989,921 bytes read, decompressed and parsed in
**290 ms**, yielding **67,496** non-blank rows in id order, ~5.4 MB on the wire, ~11 pages.
That 67,496 is the acceptance test, and it is worth saying why it is a strong one: it is the number
[`../website/CLILOCS.md`](../website/CLILOCS.md) already recorded for this same client, measured
through **UOFiddler's own `Ultima.dll`** by the converter this phase deletes. An independent
implementation agreeing to the row is not something a subtly-wrong decoder produces. Checked
alongside it: zero U+FFFD, so the UTF-8 survived; 696 rows with non-ASCII text, spot-checked as
correct curly quotes; the longest row is a 12,149-character EULA, which is also why the record
length is read **unsigned** (a signed read turns anything over 32 KB negative).
**The shard drops blanks before they reach the wire.** ~56,000 of the 123,490 entries are empty
strings the client reserves, the website discards them at import anyway, and sending them would
double the transfer for data thrown away on arrival.
**Language is a parameter (`enu` by default) but not a free one.** `Ultima.Files` resolves only the
names in its own table, which for clilocs is `enu`, `deu`, `custom1` and `custom2`; anything else is
refused `NOT_FOUND` rather than answered with an empty table. `custom1`/`custom2` are the
client-side custom cliloc files a shard ships to its players — readable here, and deliberately not
wired into the website's import, because `custom/` on the site is the supported answer for
shard-added ids.
**`assets.error` gained a `code`.** Phase 1 chose between 403 and 400 by looking for the word
"disabled" in the operator-facing sentence, which makes prose load-bearing; the codes are
`DISABLED` (403), `NOT_FOUND` (404), `UNREADABLE` (422), `UNAVAILABLE` (503) and `BAD_REQUEST`
(400). The substring check survives as a fallback, because an overlay and a sidecar are deployed
separately and a phase-1 shard must keep its 403.
### 9.2 Where the base comes from now, and when it is read
**The shard wins whenever uo-link is configured and enabled.** No mode setting: there is no version
of that question an operator benefits from answering. A file on disk remains the source only where
there is no shard link, plus a one-off explicit path — the deprecated pipeline, kept for installs
with no bridge and for development.
**Boot no longer imports on the bridge.** The file path could hash 5 MB locally and skip in 14 ms; a
shard round trip in the boot sequence would be spent answering "no" on every restart but the one
after a client patch — and patching a client is an operator action, so importing became one:
Admin → Shard → Import. Whatever table is loaded keeps serving until then.
Three things about the walk are worth recording because each is a way a shard can hand back a table
that *looks* complete:
- **Only `cut: "end"` finishes it** (§3.4). A short page can equally be a spent budget.
- **The cursor must advance**, or the walk stops rather than spinning.
- **Every page echoes the source's size and mtime.** A client patched mid-import is refused
outright (`SOURCE_CHANGED`) rather than stitched together — half of what arrived came from a file
that no longer exists and nothing later can tell which half.
**The base is exempt from the vanished-source rule**, and that is an upgrade detail rather than a
design preference: an install that used the file pipeline carries its base file's label in the
stored fingerprint, and on the bridge that label is *supposed* to disappear. Counting it as vanished
would make the first import after the upgrade demand approval for a change the upgrade itself made.
Overlays keep the rule in full.
**module-uo's protocol pin moved 7 → 8 in this phase**, which is the third declaration site §15
names and the one nothing enforces. Phase 1 moved the sidecar and the overlay together because the
installer refuses to pair a mismatched bundle; this one had to be moved by hand, in the phase that
first calls a protocol-8 route — the same trap that left the pin at 5 for two phases of the Event
System while every REST call was answered `409`.
**What survives untouched is the `custom/` overlay mechanism.** Shard-added items carry cliloc ids
no client table has, and ServUO has no server-side notion of a custom cliloc — that is a real gap
in the *game*, not an artefact of the manual pipeline, and `CLILOCS.md`'s reasoning for it stands.
@@ -846,7 +924,7 @@ new pipe.
| `assets.manifest` | `assets.manifest.ok` | Stage 2: `[{key, sha256, bytes}]`, paged | phase 3 |
| `assets.fetch` | `assets.fetch.ok` | Content for an explicit key list, paged | phase 3 |
| `assets.bodies` | `assets.bodies.ok` | Slug → body id (§8, Core thread) | phase 3 |
| `cliloc.table` | `cliloc.table.ok` | The converted table, paged | phase 2 |
| `cliloc.table` | `cliloc.table.ok` | The decompressed table, paged (`?lang=`, `?cursor=`) | **phase 2** |
| `tree.manifest` / `tree.fetch` | `.ok` | §10, the ServUO tree files | phase 7 |
Every one of them is refused outright when `Bridge.AssetsEnabled` is off, and every one of them
@@ -854,9 +932,11 @@ requires a `reqId` — a request without one is refused rather than answered, be
reply is by definition an event, and §3.1 is the reason none of this may ever be one.
**Sidecar REST** mirrors those one for one under `/assets/*`, `/cliloc`, `/tree/*`, carrying
`X-UOLink-Version: 8` and forwarding verbatim. `GET /assets/sources` exists as of phase 1; its
responder maps `bridge.busy` to **425** (flow control, and the ordinary answer mid-import rather
than a rare one) and a disabled plane to **403** (an operator's refusal, not a malformed request).
`X-UOLink-Version: 8` and forwarding verbatim. `GET /assets/sources` exists as of phase 1 and
`GET /cliloc` as of phase 2; their responder maps `bridge.busy` to **425** (flow control, and the
ordinary answer mid-import rather than a rare one) and reads the refusal's `code` for the rest —
**403** disabled, **404** no such file, **422** a file the shard cannot decode, **503** a shard that
cannot do this now (§9.1).
**Website admin** (`Admin → Shard`, admin-only): status, **Import**, **Update**, approve/reject for
a vanished key, and the existing path settings. Every action to the admin activity log, as
@@ -875,7 +955,7 @@ disagree, so a split bump means the next bundle silently fails to compose.
| `servuo-plugins/` | Extraction over ServUO's own `Ultima` (§4), the cliloc decompressor (§9), body resolution (§8), the request handlers, `overlay.toml` |
| `link/` | Six command families forwarded, the REST surface, **the inbound line cap (§3.3)**, `PROTOCOL_VERSION` |
| `module-uo/` | Client calls, asset store, the atlas source backend (§10), cliloc ingest, admin surface |
| `website/` | None expected — `ctx.uploads` already suffices (§12) |
| `website/` | Almost none — `ctx.uploads` already suffices (§12). Phase 2 deleted `server/tools/cliloc-export/`, the converter this protocol retires |
| `docs/` | This file; rewrite `CLILOCS.md` §Converting and `SPAWN_ATLAS.md` §Artwork + §Configuring; **delete `UOFIDDLER.md`**; add the libgdiplus prerequisite to `SHARD_PREREQS.md` (§4.4) |
| `installer/` | A `doctor` check for libgdiplus on Linux hosts (§4.4). Bundle pairing already enforces §15 |
| `android-app/` | Consumes images by URL; no parity gate expected until a screen shows one |
@@ -889,7 +969,7 @@ disagree, so a split bump means the next bundle silently fails to compose.
|---|---|---|
| 0 | **DONE 2026-09-10.** Spike: the vendored decoders driven from inside a running ServUO, over a client broken 21 ways. §4 stands; the finding was 22,102 wrong pictures on a *stock* client, and the validator that answers them (§4.5-§4.7) | servuo-plugins |
| 1 | **DONE 2026-09-10.** The transport: `assets.sources`, the single-slot gate (§3.2), the paging envelope (§3.4), the sidecar line cap (§3.3), `EXTRACTOR_VERSION`, `NO_IMAGING`, protocol 7→8. Plus §4.5's validator promoted into the overlay and extended to animations — which found **357 wrong pictures in the body catalogue on a stock client** and cut it from 1,144 to 787 (§4.8) | servuo-plugins, link |
| 2 | Clilocs end to end; retire the converter and `UOFIDDLER.md` §Part 1 | all |
| 2 | **DONE 2026-09-10.** Clilocs end to end (§9.1, §9.2): the Mythic decompressor ported into the overlay, `cliloc.table` + `GET /cliloc`, the paging walk and the source switch on the website, module-uo's protocol pin 7→8. `cliloc-export/` deleted and `UOFIDDLER.md` §Part 1 with it. **67,496 rows, 290 ms, ~11 pages** — the same count UOFiddler's own DLL produced from this client | all |
| 3 | Body resolution (§8) + the **787**-body catalogue (§4.8); `shard_spawn_creatures.art` filled | servuo-plugins, module-uo |
| 4 | **The UOP animation decoder (§4.3)** — the **eight** player bodies the legacy path cannot reach (§4.8 moved the elf ghosts across), the never-sweep rule already enforced in code by phase 1's `ResolveAnimation` | servuo-plugins |
| 5 | Item statics and land on demand, hued keys, the cache | servuo-plugins, module-uo |
@@ -953,7 +1033,24 @@ in the document.
sidecar or left to the website as a convention — the bound belongs where the memory is. And one
**paging envelope** (`more` / `cursor` / `cut`) is defined now, with `assets.sources` as its
first user, rather than left for whichever family pages first to invent.
7. **§4.8: the catalogue is 787 bodies, not 1,144 — measured 2026-09-10, phase 1.** Recorded here
7. **§9: the cliloc pipeline's four shapes — settled 2026-09-10, phase 2.** All four were put to
the org lead before any of it was written, and two of them departed from the recommendation:
- **The wire carries rows, not bytes.** `[{n, f, t}]` inside the paging envelope, rather than a
reconstituted plain-binary file in base64. The container decompresses into *records*; the
plain layout is a file shape that would have had to be synthesised on the shard and re-parsed
on the website, at a 33% base64 premium, to reach the same place.
- **The shard omits blank entries** — ~56,000 of 123,490 — because the website discards them at
import anyway. The transfer halves and nothing observable changes.
- **The bridge always wins; the file upload is deprecated** (org lead, departing from the
proposed `auto`/`bridge`/`file` setting). There is no version of "which source?" an operator
benefits from answering, so there is no setting to answer it with. A file remains the source
only where there is no shard link, plus a one-off explicit path.
- **Import is admin-triggered on the bridge** (org lead, departing from the proposed boot
refresh). Boot does not call the shard at all — see §9.2 for why that is the right trade and
what it costs.
8. **§4.8: the catalogue is 787 bodies, not 1,144 — measured 2026-09-10, phase 1.** Recorded here
because it changes §11's sizing, phase 3's scope and phase 4's, and because of *how* it was
found: the animation path has §4.5's shared-buffer defect too, and 357 ids with no art were
returning the previously-decoded body's bitmap. The elf ghosts moved from "decodes" to "no art"