docs(link): the Asset Bridge (Protocol 8) — client assets without UOFiddler #234

Merged
whitlocktech merged 6 commits from docs/client-assets-v8 into main 2026-09-10 07:03:59 +00:00
Member

Call it the Asset Bridge. Design of record for moving the cliloc table, the creature/item art and the spawn atlas's own source files off the operator's desktop and onto the bridge. Retires UOFiddler, the manual conversion step, the hand-written slug→filename map, and the website's shared-filesystem view of the ServUO tree.

No code yet — this is the doc to approve before any of it is built.

The premise

A ServUO shard cannot boot without a UO client. Config/DataPath.cfg resolves into Server.Core.DataDirectories at runtime, so the files the operator has been converting on their desktop are already on the shard host, in a directory the shard already knows, in the process the bridge already runs inside.

Architecture

Shard extracts → sidecar forwards → website decides. It is the only arrangement that keeps the sidecar a dumb forwarder while still resolving creature slug → body id, which only code running inside ServUO can do (BridgeWorld.cs:350 already has the mechanism).

Extraction calls ServUO's own vendored Ultimaoverlay/Scripts/Scripts.csproj:39 already references it. Two decoders are written rather than called: the Mythic cliloc reader (§9) and a UOP animation reader scoped to the player bodies the legacy path cannot reach (§4.3).

Measured, not assumed

Against this machine's ServUO 57.4 tree and client:

Call Result
Art.GetStatic / GetLand 49,150 statics + 16,384 land tiles, 0 faults
Animations.GetAnimation(0…2047) 1,144 bodies decodable, 0 faults
body 400 depth 35 actions × 5 directions = 1,050 frames — one body
Gumps.GetGump(2) hard crash, 0xC0000005
new StringList("enu", "Cliloc.enu") throws

The crash, and why it does not block the decision

All three decoders build a FileIndex, but only Gumps passes hasExtra: true — and FileIndex.cs's own comment says that branch exists for gumpartlegacy.uop, the one UOP layout carrying an extra field. Art passes false and probed clean across ~66,000 ids; Animations touches no UOP at all.

So the access violation is a bug on a branch exactly one decoder reaches, and that decoder is already out of scope. "Nothing calls Ultima.Gumps" is a safety rule (§4.1).

The spider (§4.3) — the most dangerous finding here

Bodyconv.def maps gargoyle 666 to anim5, and BodyConverter.Convert returns fileType 5, where this client has nothing. Asking the other anim files for index 666 does not fail — it returns 175 decodable action/direction combinations of a giant spider, because something unrelated lives at that index in anim2.mul. FileTypes 3 and 4 return misaligned colour fragments. Rendered and confirmed by eye; every one of those reads reports success.

The extractor takes Convert's answer and reports nothing when that yields nothing. It must never sweep file types looking for a hit — that doesn't find missing art, it silently puts a spider on the gargoyle page, and nothing downstream can detect it.

That is also how we know the six missing player bodies are genuinely UOP-only, which is why they get a decoder (phase 4) rather than a caveat.

Directions

One direction per body, not in the key: player bodies index 0 (head-on), everything else index 1 (front three-quarter — a head-on wolf is a dark blob). Which bodies are player characters is asked of Race.AllRaces, never hardcoded — RaceDefinitions.cs passes the gargoyle's ghost bodies in the opposite order to the other races.

Cuts every depth count 5×: body 400 goes 1,050 → 210 frames.

Requirement: libgdiplus on Linux hosts (§4.4)

ServUO targets net48, so Linux runs it under Mono, and Mono's System.Drawing is a thin layer over libgdiplus — which sits in the decode, not just the encode. Windows hosts need nothing. Handled three ways: a SHARD_PREREQS.md entry, an installer doctor check, and a named NO_IMAGING status rather than an error. Cliloc and atlas import are unaffected.

Noted because depending on something unmaintained should be deliberate: mono/libgdiplus was archived in March 2025. Distros still package it, so apt-get install libgdiplus is ordinary and supported — but it is the long-term argument for moving off System.Drawing, and phase 4's UOP reader is written without it so that door stays open.

Other findings

The shard → sidecar direction has no line cap (shard.rs uses read_line unbounded), while sidecar → shard caps at 1 MiB. Protocol 8 closes that before it starts sending large lines deliberately.

UOFiddler is Beerware, so its Mythic cliloc decompressor can be ported into this GPL-3.0-or-later tree. Client files remain the operator's own, extracted on their own host — nothing committed, nothing redistributed.

Contents

Ten phases (§16). Phase 0 tries to break the vendored decoders on purpose, from inside a running ServUO against a deliberately patched client — the probes above ran in PowerShell against a stock client, and neither is the real environment.

§17: three items settled, one open (the default audience for asset serving), which does not block starting.

Bumps PROTOCOL_VERSION 7 → 8 in link/ and servuo-plugins/overlay.toml in the same PR when the code lands.


  • AI-assisted — Claude Code (Opus 5)

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

**Call it the Asset Bridge.** Design of record for moving the cliloc table, the creature/item art and the spawn atlas's own source files off the operator's desktop and onto the bridge. Retires UOFiddler, the manual conversion step, the hand-written slug→filename map, and the website's shared-filesystem view of the ServUO tree. **No code yet** — this is the doc to approve before any of it is built. ## The premise A ServUO shard cannot boot without a UO client. `Config/DataPath.cfg` resolves into `Server.Core.DataDirectories` at runtime, so the files the operator has been converting on their desktop are already on the shard host, in a directory the shard already knows, in the process the bridge already runs inside. ## Architecture Shard extracts → sidecar forwards → website decides. It is the only arrangement that keeps the sidecar a dumb forwarder while still resolving creature slug → body id, which only code running inside ServUO can do (`BridgeWorld.cs:350` already has the mechanism). **Extraction calls ServUO's own vendored `Ultima`** — `overlay/Scripts/Scripts.csproj:39` already references it. Two decoders are written rather than called: the Mythic cliloc reader (§9) and a UOP animation reader scoped to the player bodies the legacy path cannot reach (§4.3). ## Measured, not assumed Against this machine's ServUO 57.4 tree and client: | Call | Result | |---|---| | `Art.GetStatic` / `GetLand` | 49,150 statics + 16,384 land tiles, **0 faults** | | `Animations.GetAnimation(0…2047)` | **1,144** bodies decodable, **0 faults** | | body 400 depth | 35 actions × 5 directions = 1,050 frames — one body | | `Gumps.GetGump(2)` | **hard crash**, `0xC0000005` | | `new StringList("enu", "Cliloc.enu")` | throws | ## The crash, and why it does not block the decision All three decoders build a `FileIndex`, but **only `Gumps` passes `hasExtra: true`** — and `FileIndex.cs`'s own comment says that branch exists *for* `gumpartlegacy.uop`, the one UOP layout carrying an extra field. `Art` passes `false` and probed clean across ~66,000 ids; `Animations` touches no UOP at all. So the access violation is a bug on a branch exactly one decoder reaches, and that decoder is already out of scope. **"Nothing calls `Ultima.Gumps`" is a safety rule** (§4.1). ## The spider (§4.3) — the most dangerous finding here `Bodyconv.def` maps gargoyle 666 to `anim5`, and `BodyConverter.Convert` returns fileType 5, where this client has nothing. Asking the *other* anim files for index 666 **does not fail** — it returns 175 decodable action/direction combinations of **a giant spider**, because something unrelated lives at that index in `anim2.mul`. FileTypes 3 and 4 return misaligned colour fragments. Rendered and confirmed by eye; every one of those reads reports success. **The extractor takes `Convert`'s answer and reports nothing when that yields nothing. It must never sweep file types looking for a hit** — that doesn't find missing art, it silently puts a spider on the gargoyle page, and nothing downstream can detect it. That is also how we know the six missing player bodies are genuinely UOP-only, which is why they get a decoder (phase 4) rather than a caveat. ## Directions One direction per body, not in the key: **player bodies index 0** (head-on), **everything else index 1** (front three-quarter — a head-on wolf is a dark blob). Which bodies are player characters is asked of `Race.AllRaces`, never hardcoded — `RaceDefinitions.cs` passes the gargoyle's ghost bodies in the opposite order to the other races. Cuts every depth count 5×: body 400 goes 1,050 → 210 frames. ## Requirement: libgdiplus on Linux hosts (§4.4) ServUO targets `net48`, so Linux runs it under Mono, and Mono's `System.Drawing` is a thin layer over **libgdiplus** — which sits in the *decode*, not just the encode. Windows hosts need nothing. Handled three ways: a `SHARD_PREREQS.md` entry, an installer `doctor` check, and a named `NO_IMAGING` status rather than an error. Cliloc and atlas import are unaffected. Noted because depending on something unmaintained should be deliberate: [mono/libgdiplus](https://github.com/mono/libgdiplus) was **archived in March 2025**. Distros still package it, so `apt-get install libgdiplus` is ordinary and supported — but it is the long-term argument for moving off `System.Drawing`, and phase 4's UOP reader is written without it so that door stays open. ## Other findings **The shard → sidecar direction has no line cap** (`shard.rs` uses `read_line` unbounded), while sidecar → shard caps at 1 MiB. Protocol 8 closes that before it starts sending large lines deliberately. **UOFiddler is Beerware**, so its Mythic cliloc decompressor can be ported into this GPL-3.0-or-later tree. Client files remain the operator's own, extracted on their own host — nothing committed, nothing redistributed. ## Contents **Ten phases** (§16). Phase 0 tries to *break* the vendored decoders on purpose, from inside a running ServUO against a deliberately patched client — the probes above ran in PowerShell against a stock client, and neither is the real environment. §17: three items settled, **one open** (the default audience for asset serving), which does not block starting. Bumps `PROTOCOL_VERSION` 7 → 8 in `link/` **and** `servuo-plugins/overlay.toml` in the same PR when the code lands. --- - [x] AI-assisted — Claude Code (Opus 5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 05:26:06 +00:00
The shard host already has the UO client — ServUO cannot boot without one, and
Config/DataPath.cfg resolves into Server.Core.DataDirectories at runtime. So the
cliloc table, the creature and item art, and the spawn atlas's own source files
can all reach the website over the bridge that already exists, and UOFiddler,
the desktop conversion step and the website's shared-filesystem view of the
ServUO tree all go away.

Design of record for the work: the shard extracts, the sidecar forwards, the
website decides — which is the only arrangement that keeps the sidecar a dumb
forwarder while still resolving creature slug -> body id, something only code
running inside ServUO can do.

Measured against this machine's ServUO 57.4 tree and client rather than assumed:
Art.GetStatic and GetLand decode ~66,000 ids with no faults, 1,144 bodies have a
decodable first frame, and body 400 alone is 1,050 frames across its actions and
directions — which is what makes the bulk set one thumbnail per body and
everything deeper on demand.

Two findings shape the build. Gumps.GetGump(2) does not fail, it corrupts the
process (AccessViolationException, 0xC0000005) — uncatchable on .NET Framework
4.8 and a shard crash in-process — so §4 recommends owning bounds-checked
decoders rather than calling ServUO's vendored Ultima, which also removes the
System.Drawing/libgdiplus dependency and the UOP gap that leaves gargoyle bodies
666/667 empty. And the shard -> sidecar direction has no line cap today, which
Protocol 8 must close before it starts sending large lines deliberately.

UOFiddler is Beerware, so its Mythic cliloc decompressor can be ported into this
GPL-3.0-or-later tree and the conversion step retired entirely.

Nine phases, four decisions still open in §17.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 05:47:29 +00:00
Only the viewer-facing direction is wanted for players and monsters, so
direction leaves the key entirely (§5.1) rather than being a segment nobody
varies. Every depth count in §11 falls by exactly 5x.

Which index that is was rendered, not looked up: all five directions for a
human, a wolf and a dragon. Index 0 is head-on — face, chest and front legs —
and index 4 is directly away, with a quadruped's tail toward the camera. The
small-thumbnail version of the same test suggested the opposite, which is why
the finding is in the doc rather than in someone's head.

Measured consequence: body 400 drops 1,050 -> 210 frames, and a complete
one-direction set for all 1,144 bodies is ~173,000 frames (~170 MB) rather than
~865,000. That moves a bulk-fill-everything switch from "never" to something
phase 5 should leave room for.

One caveat kept as an open question: index 0 is the least legible view for
four-legged creatures — a head-on wolf is a dark blob, a side-on wolf is a wolf
— so the extractor takes the index as configuration defaulting to 0, and §17
asks whether the catalogue should default to 1 or 2 instead.

No client-derived image is committed; the render was inspected and discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 06:06:45 +00:00
Player character bodies take direction index 0 (head-on); every other body takes
index 1 (front three-quarter). Direction still does not appear in the key — both
indices are extractor configuration.

The split follows the legibility caveat rather than fighting it: a humanoid
reads fine head-on and a character portrait should look at you, while a wolf
seen head-on is a dark blob and the same wolf at index 1 is obviously a wolf.

§5.2 is new: which bodies count as player characters is asked of the shard, via
Race.AllRaces and each race's MaleBody/FemaleBody/MaleGhostBody/FemaleGhostBody,
never hardcoded. Twelve ids on stock 57.4. Two things say a hardcoded list would
have been wrong — RaceDefinitions.cs passes the gargoyle's ghost bodies in the
opposite order to the other two races (695 male, 694 female), and a shard that
calls RegisterRace adds ids no table of ours would contain.

And the finding that matters most: SIX OF THOSE TWELVE do not decode at all with
ServUO's vendored Animations — both human ghosts and every gargoyle body,
because they live in AnimationFrame*.uop which that library never reads. The one
part of the scope with the most attention on it is the part the vendored library
serves worst, which is now the strongest single argument for §4's recommendation
to own the decoders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 06:11:20 +00:00
Decision: no decoders are reimplemented. Extraction goes through ServUO's own
vendored Ultima, which Scripts.csproj already references, so the art half of
this protocol is plumbing and the Mythic cliloc reader (§9) becomes the only
decoder Protocol 8 writes rather than calls.

What makes that safe rather than merely cheap is a distinction §1.1 did not
draw. All three decoders build a FileIndex, but only Gumps passes
hasExtra: true — and FileIndex.cs's own comment says that branch exists FOR
gumpartlegacy.uop, the one UOP layout with an extra field. Art passes
hasExtra: false and probed 49,150 statics plus 16,384 land tiles with zero
faults; Animations touches no UOP at all and probed 1,144 bodies clean. The
access violation is a bug on a branch exactly one decoder reaches, and that
decoder was already out of scope. So "nothing calls Ultima.Gumps" is now a
safety rule, and adding gump art later means fixing that path first.

§4.2 records the three costs this accepts: six of twelve stock player bodies
have no art (UOP-only, not reachable by calling the existing code differently),
System.Drawing stays in the decode path, and we inherit whatever Ultima a shard
vendors — EXTRACTOR_VERSION already covers the last one.

Phase 0 changes shape with it. It was going to prove new decoders byte-identical;
it now tries to BREAK the vendored ones on purpose, from inside a running ServUO
against a deliberately patched client, because the probes behind §1.1 ran in
PowerShell against a stock client and neither is the real environment.

§17 is down to one real question: libgdiplus on Linux/Mono shards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 06:53:57 +00:00
The six player-character bodies the vendored Animations cannot reach are the
player character, and the scope says player models, so they get a decoder rather
than a caveat. New §4.3, new phase 4, scoped as narrowly as possible: a reader
for AnimationFrame*.uop used ONLY for bodies the legacy path cannot resolve.
Everything the vendored code already decodes keeps going through it.

Verified genuinely absent rather than mis-addressed, and the way that was
established is now the most important warning in the document.

Bodyconv.def maps gargoyle 666 to anim5 and BodyConverter.Convert faithfully
returns fileType 5, where this client has nothing. Asking the OTHER anim files
for index 666 does not fail — it returns 175 decodable action/direction
combinations of a giant spider, because something unrelated occupies that index
in anim2.mul, while fileTypes 3 and 4 return misaligned colour fragments. All of
it rendered and looked at, which is the only reason it was caught: every one of
those reads reports success.

So the extractor takes Convert's answer and reports nothing when that yields
nothing. It must never sweep file types looking for a hit. That does not find
missing art — it silently puts a spider on the gargoyle page, with no error
raised anywhere and nothing downstream able to detect it. A "0 rows" outcome is
correct behaviour; a confident wrong picture is the failure this protocol most
needs to avoid.

Phase 4 sits after the catalogue, not inside it: the catalogue is useful with
1,138 of 1,144 bodies, and the UOP reader is the only genuinely new format work
here, so putting it on the critical path would hold up every website-side phase
behind it. Its acceptance test is that a gargoyle looks like a gargoyle, checked
by eye.

References available and license-compatible: ClassicUO's animation loader
(GPL-3) and UOFiddler 4.22 (Beerware).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 07:01:14 +00:00
The work has a name now — the Asset Bridge — for commits, PR titles, branches
(feat/asset-bridge-p<n>) and conversation. Protocol number stays 8 and the file
stays docs/link/v8.md.

§4.4 closes the last real open question rather than deferring it to phase 0, and
takes all three answers instead of choosing one. ServUO targets net48, so a Linux
host runs it under Mono, and Mono's System.Drawing is a thin layer over
libgdiplus — which §4.2 put in the decode path, not just the encode. So on Linux
it is a hard prerequisite for art. Cliloc and atlas import are unaffected; neither
touches pixels. Windows hosts need nothing at all.

It is now written down as: a SHARD_PREREQS.md entry, a doctor check in the
installer, and a named NO_IMAGING status when it is missing, in the same family
as the cliloc reader's COMPRESSED — never a stack trace, never a 500. Install
routes per distro are in the section, apt-get install libgdiplus being the
normal one.

One fact recorded because depending on something unmaintained should be a
conscious act: github.com/mono/libgdiplus was ARCHIVED in March 2025 and is
read-only. Distributions still package and patch it, so installing it today is
supported and ordinary — but it is the strongest long-term argument for moving
extraction off System.Drawing eventually, and phase 4's UOP reader is written
without it so that door stays open.

§17 restructured: three settled items kept because each changes numbers
elsewhere, and one genuinely open question (the default audience) that does not
block starting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude changed title from docs(link): Protocol 8 — client assets over the bridge to docs(link): the Asset Bridge (Protocol 8) — client assets without UOFiddler 2026-09-10 07:01:18 +00:00
whitlocktech merged commit f2e074c3f7 into main 2026-09-10 07:03:59 +00:00
whitlocktech deleted branch docs/client-assets-v8 2026-09-10 07:04:00 +00:00
Sign in to join this conversation.
No description provided.