docs(link): player bodies face you, everything else does not

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
This commit is contained in:
2026-09-10 01:06:32 -05:00
parent 0c3f0d65cc
commit b62d0b6307

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.