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
Showing only changes of commit b62d0b6307 - Show all commits

View File

@@ -169,10 +169,12 @@ application, and a minimal PNG writer over `System.IO.Compression.DeflateStream`
into a `Bitmap` via `LockBits`, so System.Drawing is in the *decode*, not just the encode — a
Linux/Mono shard needs libgdiplus even to read a sprite. Writing our own pixels and our own PNG
makes the feature portable by construction.
3. **It removes the UOP gap.** ServUO's vendored `Animations` reads legacy `anim*.mul` only. This
client has a full 195 MB `anim.mul` so most bodies resolve — but **gargoyle bodies 666/667
returned nothing**, because gargoyles live in `AnimationFrame*.uop`. A player race missing from
an asset store built for "player models and everything" is not a caveat, it is a defect.
3. **It removes the UOP gap, which lands squarely on the player bodies.** ServUO's vendored
`Animations` reads legacy `anim*.mul` only, never `AnimationFrame*.uop`. This client has a full
195 MB `anim.mul` so most creatures resolve — but **six of the twelve stock player-character
bodies return nothing**, including every gargoyle and both human ghosts (§5.2). A missing
playable race in an asset store built for "player models and everything" is not a caveat, it is
a defect, and it is in the highest-attention part of the scope.
4. **We are already in this business.** §9 writes a cliloc decompressor from scratch regardless.
A2 also ends the dependency on whatever version of `Ultima` a given ServUO happens to vendor,
@@ -210,13 +212,20 @@ Three properties this shape buys:
### 5.1 There is no direction segment, because only one direction is wanted
Bodies are stored in **five** directions and the client mirrors three of them to reach eight. Only
one is needed here — the one facing the viewer — so **direction is fixed by the extractor and is
not part of the key**. Leaving it in would advertise a choice nobody is going to make and would
five-fold every count in §11 for nothing.
one is needed here, so **direction is fixed by the extractor and is not part of the key**. Leaving
it in would advertise a choice nobody is going to vary and would five-fold every count in §11 for
nothing.
**The front-facing index is 0.** This was determined by rendering all five for a human, a wolf and
a dragon rather than from a table, because the answer is not obvious and the small-thumbnail version
of the same test suggested the opposite:
**Which one depends on whether the body is a player character:**
| Body | Direction | Why |
|---|---|---|
| A player character body | **0** — head-on, facing the viewer | A character is a portrait; it should look at you |
| Everything else | **1** — front three-quarter | The view that actually reads as a creature (see the caveat below) |
Which index is which was determined by **rendering all five** for a human, a wolf and a dragon
rather than from a table, because the answer is not obvious and the small-thumbnail version of the
same test suggested the exact opposite:
| Index | View |
|---|---|
@@ -226,12 +235,45 @@ of the same test suggested the opposite:
| 3 | Rear three-quarter |
| 4 | Directly away — back of the head, and a quadruped's tail toward the camera |
One caveat the render made obvious, and it is the reason this is written down rather than assumed:
**index 0 is the least legible view for four-legged and long-bodied creatures.** A wolf seen head-on
is a dark blob; the same wolf at index 2 is unmistakably a wolf, and at index 1 is the view
UOFiddler's own thumbnail list picks. For a bestiary tile that matters. The extractor therefore
takes the index as a **configuration value defaulting to 0**, so changing the catalogue's mind later
is a setting and a re-import, not a protocol change.
The caveat the render made obvious is what produced the split: **index 0 is the least legible view
for four-legged and long-bodied creatures.** A wolf seen head-on is a dark blob; the same wolf at
index 1 is unmistakably a wolf, which is also why UOFiddler's own thumbnail list picks that view. A
humanoid has no such problem — it reads fine head-on, and head-on is what a character portrait
wants.
Both indices stay **configuration values** (defaulting to 0 and 1), so changing the catalogue's mind
later is a setting and a re-import, not a protocol change.
### 5.2 "Player character body" is asked of the shard, never hardcoded
`Server.Race.AllRaces` gives every registered race, and each carries `MaleBody`, `FemaleBody`,
`MaleGhostBody` and `FemaleGhostBody`. The plugin enumerates those four ids per race and that set —
nothing else — takes index 0. On stock ServUO 57.4 that is twelve ids:
| Race | Male | Female | Male ghost | Female ghost |
|---|---|---|---|---|
| Human | 400 | 401 | 402 | 403 |
| Elf | 605 | 606 | 607 | 608 |
| Gargoyle | 666 | 667 | **695** | **694** |
This is the §8 argument again in miniature: only code inside ServUO can answer it, and asking is
the only thing that works on a shard with a custom race. Two details make the case that 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.
**Half of that set does not decode with ServUO's vendored library.** Measured:
| Decodes | Does not |
|---|---|
| Human male/female (400, 401) | **Human ghosts (402, 403)** |
| Elf male/female (605, 606) | **Every gargoyle body (666, 667, 694, 695)** |
| Elf ghosts (607, 608) | |
Six of twelve, including a whole playable race. So the one part of the asset scope with the most
attention on it — the player character, head-on — is precisely the part the vendored `Animations`
serves worst, and §4's recommendation to own the decoders is what fixes it. This is the single
strongest piece of evidence for that choice.
---
@@ -505,8 +547,9 @@ prove byte-identical output *before* building six phases on top of it.
libgdiplus dependency, the UOP gap and the vendor-drift risk all going away at once.
2. **§11: gump art deferred.** Confirm that paperdoll and equipment gump art is genuinely
out of scope for 8, given it is the one kind whose existing decoder crashes.
3. **§5.1: direction is fixed at index 0** (head-on) and is not in the key — settled 2026-09-10.
What is *not* settled is whether the catalogue should default to index 1 or 2 instead, since a
head-on wolf is a dark blob and a side-on wolf is a wolf. It is a setting either way.
3. **§5.1/§5.2: direction — settled 2026-09-10.** Player character bodies use index 0, everything
else index 1, direction is not in the key, and the player-body set is enumerated from
`Race.AllRaces` rather than hardcoded. Nothing outstanding; recorded here because it changes
every count in §11.
4. **§13: the default audience** for asset serving — inheriting the using page's audience is
proposed; the operator sets the policy either way.