feat(asset-bridge): the cliloc table, decompressed on the shard (Phase 2) #29
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/asset-bridge-p2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 of the Asset Bridge (
docs/link/v8.md§9), shard half. Part of a five-repo change: link#42, module-uo, website, docs.The shard reads its own client's
Cliloc.enuand serves it over the bridge, so the operator stops installing UOFiddler, building a converter against itsUltima.dll, and copying a 5 MB file to the web host every time they patch their client.What this adds
BridgeCliloc.cs— the one decoder protocol 8 writes rather than calls (§4): a port of UOFiddler'sMythicDecompress+MoveToFront(Beerware, so clean to bring into a GPL-3.0-or-later tree), rewritten against plain arrays because the upstream isSpan<T>/ArrayPool<T>/BinaryPrimitivescode and ServUO targetsnet48.The algorithm is deliberately unchanged, including the parts that read oddly — the three-region count/cursor/end table and the symbol-table shifts are upstream's, 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 are the only behavioural difference: the upstream indexes its payload unchecked, which is safe for a file the client wrote and is not safe for a file this shard was handed.
cliloc.tablerides phase 1's rails unchanged — the single slot, the byte-budget page builder, themore/cursor/cutenvelope. The cursor is a cliloc number, not an offset, because the decoded table is cached for five idle minutes and released after the last page: it can be dropped and rebuilt between two pages of one import, and an index would then silently mean something else.Also on this plane:
assets.errorgains acode. Phase 1 chose between 403 and 400 by looking for the word "disabled" in an operator-facing sentence, which makes prose load-bearing.Measured, on a stock client
4,989,921 bytes read, decompressed and parsed in 290 ms → 67,496 non-blank rows in id order.
That count is the acceptance test. It is exactly what UOFiddler's own
Ultima.dllproduced from this same client through the converter this phase deletes — an independent implementation agreeing to the row is not something a subtly-wrong decoder produces. Alongside it: zero U+FFFD, the 696 non-ASCII rows carry correct curly quotes, and the longest row is a 12,149-character EULA (which is why the record length is read unsigned).Blanks never reach the wire: ~56,000 of the 123,490 entries are empty strings the client reserves, and the website discards them at import anyway.
Verified live
Deployed to a real ServUO tree, booted, and driven through a real sidecar: 12 pages, every page inside the 512 KiB budget, the whole table in 1.4 s,
?lang=fra→ 404, a malformed cursor → 400, and three concurrent callers → 425 while one is served.AI disclosure
Written with Claude Code (Opus 5); commits carry the
Co-Authored-Bytrailer.🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The shard reads its own client's `Cliloc.enu` and serves it over the bridge, so the operator stops installing UOFiddler, building a converter against its `Ultima.dll`, and copying a 5 MB file to the web host every time they patch. `BridgeCliloc.cs` is the one decoder protocol 8 writes rather than calls (docs/link/v8.md §4, §9): a port of UOFiddler's `MythicDecompress` + `MoveToFront` — Beerware, so clean to bring into a GPL-3.0-or-later tree — rewritten against plain arrays, because the upstream is `Span<T>` / `ArrayPool<T>` / `BinaryPrimitives` code and ServUO targets `net48`. The algorithm is deliberately unchanged, including the parts that read oddly. The three-region count/cursor/end table and the symbol-table shifts are upstream's, 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 difference: the upstream indexes its payload unchecked, which is safe for a file the client wrote and is not safe for a file this shard was handed. Measured on a stock client: 4,989,921 bytes read, decompressed and parsed in **290 ms**, yielding **67,496** non-blank rows in id order. That number is the acceptance test — it is what UOFiddler's own DLL produced from this same client through the converter this phase deletes, so an independent implementation agrees to the row. Zero U+FFFD; the 696 non-ASCII rows carry correct curly quotes; the longest row is a 12,149-character EULA, which is why the record length is read unsigned. Blanks never reach the wire — ~56,000 of the 123,490 entries are empty strings the client reserves, and the website discards them at import anyway. Also on this plane: * `assets.error` gains a `code`. Phase 1 chose between 403 and 400 by looking for the word "disabled" in an operator-facing sentence, which makes prose load-bearing; `DISABLED` / `NOT_FOUND` / `UNREADABLE` / `UNAVAILABLE` / `BAD_REQUEST` say it directly. * `Accept` and `Fail` are internal rather than private, because the asset plane's single slot and its refusal frame are shared by every family on it. The cursor is a cliloc NUMBER, not an offset: the decoded table is cached for five idle minutes and released after the last page, so it can be dropped and rebuilt between two pages of one import, and an index would then silently mean something else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4frombefore the page opens, not after it closes cbdbc9fe5c