feat(asset-bridge): the UOP animation reader, and 235 bodies the legacy path cannot see (Phase 4) #31
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/asset-bridge-p4"
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 4 of the Asset Bridge (
docs/link/v8.md§4.3, §4.9). ServUO's vendoredUltima.Animationsbuilds its fiveFileIndexes with the constructor that passesuopFile: null, so it never opensAnimationFrame*.uop— everything a modern client moved there is invisible to it. This adds the one decoder §4.3 reserved for this phase and wires it in beneath the legacy path, as a fallback.What it recovers is not what the plan expected
Measured before anything was written:
anim5.idx; the art is inAnimationFrame3.uop— recoveredlookup -1— and in no UOP packagelength 0(the §4.8 wrong-picture shape) — and in no UOP packagelookup -1— and in no UOP packageThe five packages hold 10,724 entries and the
build/animationlegacyframe/%06d/%02d.binname scheme claims all of them, so there is no other naming ghost art could hide under. Phase 4 recovers 2 of the 8 bodies §4.8 assigned it; six have no art in this client at all.Offsetting that: the same fallback reaches 233 further bodies the catalogue had nothing for. The working set goes 787 → 1,022.
Org-lead decisions taken before building
Bitmap.Save— §4.4 promised this reader is written withoutSystem.Drawing.NO_IMAGINGrefusal, rather than serving a partial catalogue.Three things about the reader
Group.TryOpento check — a hit is proof of identity. Measured: no hash appears in two packages.System.Drawingin it. The decode fills aushort[]of ARGB1555 andBridgePngencodes that directly — zlib written by hand around net48's raw-deflate-onlyDeflateStream, CRC32, one IDAT, filter 0. Phase 3'sToPngis untouched: its input is aBitmapfrom a decoder that needs GDI+ to exist, so encoding it without GDI+ buys nothing.Wire and versioning
EXTRACTOR_VERSION1 → 2 — every client file is byte-identical and the answer is different, which is exactly what that number exists to say. The UOP packages joinassets.sourcesand the catalogue id, so patching one is drift rather than a silent no-op;Ultima.Filescannot resolve them (its table predates UOP animations), soBridgeUop.FindClientFiledoes, case-insensitively by enumeration for Linux hosts. Manifest and fetch rows carry a new additivesourcefield (legacy/uop).Protocol stays 8 — no message shape changed, only fields added. No
module-uoorlinkchange is needed:extractorVersionalready reads as drift there, andplayerBodiesis passed through.Measured on the live rig
Real sidecar, real ServUO, this machine's client:
legacy+ 235uop)legacy, 666/667uop— all at direction 0absentThe gargoyles were rendered and looked at, because §4.3's whole point is that this failure mode produces confident, wrong pictures. A gargoyle looks like a gargoyle.
🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
ServUO's vendored `Ultima.Animations` reads legacy `anim*.mul` only -- it builds its five FileIndexes with the constructor that passes `uopFile: null` -- so everything a modern client moved into `AnimationFrame*.uop` is invisible to it. This adds the one reader docs/link/v8.md 4.3 reserved for phase 4, and wires it in as a fallback beneath the legacy path. What it actually recovers is not what the plan expected, and the difference was measured before any of this was written: - Of the EIGHT player-character bodies 4.8 assigned to this phase, only TWO are in the client at all: gargoyles 666 and 667, in AnimationFrame3.uop. The six ghost bodies (human 402/403, elf 607/608, gargoyle 694/695) are in no package. The five packages hold 10,724 entries between them and the `build/animationlegacyframe/%06d/%02d.bin` name scheme claims every one, so there is no other naming they could be hiding under. - The same fallback reaches 233 further bodies the catalogue had nothing for, so the working set goes from 787 to 1,022 (57 Monster, 26 Animal, 97 Equipment, 50 unlisted, 3 Human, 2 Sea). The catalogue was already 366 Equipment bodies before this, so its character does not change. Decided with the org lead before building: the fallback applies to every body rather than to player bodies alone; ghost ids leave the player-body set entirely (no client has art for any of them, and listing them only advertised keys that cannot exist); the UOP path gets its own PNG encoder rather than Bitmap.Save; and a host without libgdiplus keeps the flat NO_IMAGING refusal rather than serving a partial catalogue. Three things about the reader: - It is not the never-sweep rule being broken. That rule exists because a legacy index is addressed by POSITION, so asking the wrong file returns a giant spider that decodes cleanly. A UOP entry is addressed by the hash of a name containing the body id, and the payload declares that id again for `Group.TryOpen` to check, so a hit is proof of identity. Measured: no hash appears in two packages. - Validate-as-we-go replaces validate-before-calling, because here we ARE the library: the block chain is bounded against the file, the record against the file, the inflated length against the declared one, the frame table against the payload, and every run header against both the record's remaining bytes and the bitmap it writes into. Measured the way 4.5 was -- across every UOP body on a stock client it refuses nothing that carries art. The one body it refuses (286) declares a 0x0 frame, which the vendored decoder treats as absent too. - No System.Drawing anywhere in it, which is what 4.4 promised: the decode fills a ushort[] of ARGB1555 and BridgePng encodes that directly (zlib around net48's raw-deflate-only DeflateStream, CRC32, one IDAT, filter 0). EXTRACTOR_VERSION 1 -> 2: every client file is byte-identical and the answer is different, which is exactly what that number exists to say. The UOP packages join `assets.sources` and the catalogue id, so patching one is drift; `Ultima.Files` cannot resolve them (its table predates UOP animations) so BridgeUop.FindClientFile does it, case-insensitively by enumeration for Linux hosts. Manifest and fetch rows carry a new `source` field (`legacy` / `uop`). Protocol stays 8 -- no message shape changed, only fields added. Measured on the live rig (real sidecar, real ServUO, this machine's client): 1,022 rows in ONE page, 1,409 ms cold; six player bodies, all six with art for the first time (400/401/605/606 legacy, 666/667 uop), all at direction 0; 1,016 at direction 1; the six ghost bodies absent; 45 duplicate-hash groups of which exactly one is new, bodies 1531/1532, two distinct records whose first frames match -- legitimate, and provable only because each payload declares its own body id. The gargoyles were rendered and looked at, because 4.3's whole point is that this failure mode produces confident, wrong pictures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4