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:
111
link/v8.md
111
link/v8.md
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user