§16 planned deep animation keys and a bulk-fill switch. Measuring first changed
the phase: a complete one-direction animation set is 174,453 frames / 281.5 MB
(not the ~119,000 / ~117 MB §11 estimated), every frame carries its own centre
and 23,818 of 26,274 actions have frames of differing size — and the site
displays still pictures. So none of it was built (§11.2, org lead 2026-09-11);
`body/<id>/a<n>/f<k>` stays named in §5 and refused in practice.
§4.10 is what the walk found instead. A body's actions are a contiguous band and
the next body's begin immediately after, so one action past the ceiling is a real
record of another creature: 643 of 795 legacy bodies pass every check there and
452 are byte-identical to body+1's action 0. `Animations.GetAnimLength` is not
the ceiling — it disagrees with the index arithmetic on one body of this client,
by nine actions.
What shipped: the 73 bodies with no art at action 0 (a horse at `body/820/a23`),
the catalogue key carrying its action, the atlas join that reads it instead of
hardcoding `a0`, and the ceiling that makes the fallback walk safe. Catalogue
1,022 -> 1,095; EXTRACTOR_VERSION 2 -> 3; protocol stays 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-14 01:10:01 -05:00
3 changed files with 195 additions and 29 deletions
@@ -635,6 +635,45 @@ needed its own lookup, because `Ultima.Files`' table of known client files preda
and answers null for every `AnimationFrame*.uop` however present they are. The replacement matches
and answers null for every `AnimationFrame*.uop` however present they are. The replacement matches
**case-insensitively by enumeration**, which is a Linux-host concern rather than a tidiness one.
**case-insensitively by enumeration**, which is a Linux-host concern rather than a tidiness one.
### 4.10 What phase 6 measured: the ceiling, and 452 validated pictures of the wrong body
§4.8 found the library returning the previously-decoded body's bitmap for an index entry that
reads `length 0`. Phase 6 went looking one axis over — along **actions** rather than bodies — and
found the same class of failure with none of the tells.
An index entry for an animation is `bodyBase + action * 5 + direction`, and `bodyBase` comes from a
**band**: 110 slots for a high-detail body (22 actions), 65 for a low-detail one (13), 175 for a
people body (35). The bands are contiguous, so the slots immediately after a body's band are the
**next body's**. Ask for one action past the ceiling and the arithmetic lands on a real entry, at a
real offset, holding a real animation record — of a different creature.
Measured on this machine's stock client, over the 795 bodies the legacy path serves:
| Asking one action past the band | Bodies |
|---|---|
| Refused by §4.5's validator (the entry is absent or unreadable) | 152 |
| **Passes `CheckEntry` and `AnimationSane` and decodes** | **643** |
| ...and the picture is **byte-identical to body+1's action 0** | **452** |
Body 1 action 22 is an ettin. Body 3 action 22 is an imp. Both confirmed by rendering them beside
the body they belong to, because a count would have said the walk was fine — phase 0's validator
cannot catch this and is not wrong to miss it: there is nothing defective about the record. The
only defence is to **refuse the address**, so the ceiling lives inside
`BridgeAssetValidator.ResolveAnimation`, where every caller already goes and no caller can skip it.
**The ceiling is the banding, not the library's own `GetAnimLength`.** That function exists, looks
authoritative and disagrees with the index arithmetic on exactly one body of this client: a body
reaching file type 5 as id 34 is excluded from the first band by `Animations.GetFileIndex`'s own
"looks strange, though it works" special case, so it owns **13** actions while `GetAnimLength`
answers **22**. Taking the larger number is nine actions of somebody else's art, reached on this
client by translation from body 276. So the count is derived from the same switch that produces the
offset, in the same file, where the two cannot drift apart.
This is the §4.3 never-sweep rule again in a third disguise. Sweeping file types puts a spider on
the gargoyle page; trusting a `length 0` entry puts the last creature decoded on this one; walking
past a band puts the *next* creature on it. All three decode cleanly, all three report success, and
all three are caught by refusing to ask rather than by checking the answer.
---
---
## 5. Addressing: one key for every asset
## 5. Addressing: one key for every asset
@@ -647,11 +686,22 @@ static/3922 one item graphic
static/3922/h33 the same graphic, hue 33 applied
static/3922/h33 the same graphic, hue 33 applied
land/3 one land tile
land/3 one land tile
body/34/a0 creature body 34, action 0, first frame
body/34/a0 creature body 34, action 0, first frame
body/400/a0/f0..f9 human male, action 0, all ten frames
body/820/a23 body 820, whose action 0 is empty — a horse (§11.2)
body/400/a0/f0..f9 human male, action 0, all ten frames — NOT SERVED (§11.2)
cliloc/enu the whole converted string table (not an image)
cliloc/enu the whole converted string table (not an image)
tree/Spawns/Trammel.xml a ServUO tree file (§10)
tree/Spawns/Trammel.xml a ServUO tree file (§10)
```
```
**The catalogue's key names whichever action its picture came from.** One row per body either way,
and for all but 73 of this client's bodies that action is 0 — but a body with no art there is
catalogued at the first action that has any, and the key says which (§11.2). Calling it `a0`
regardless would have been fewer changes downstream and a key that lies about its content, which
is the failure §4.5, §4.8 and §4.10 each describe from a different direction.
**The frame depth is defined and not served.**`body/400/a0/f0..f9` is well-formed under this
scheme and every request for it is refused `unsupported` — see §11.2 for the decision and what it
would cost to change.
Three properties this shape buys:
Three properties this shape buys:
- **Hue is part of the key, not a transform.** `itemId` and `hue` are already on the wire together
- **Hue is part of the key, not a transform.** `itemId` and `hue` are already on the wire together
@@ -659,8 +709,10 @@ Three properties this shape buys:
key for its own picture. Applying hues website-side would mean shipping `Hues.mul` semantics into
key for its own picture. Applying hues website-side would mean shipping `Hues.mul` semantics into
Node for no gain.
Node for no gain.
- **Depth is expressible without being mandatory.** `body/400/a0` and `body/400/a0/f0..f9` are the
- **Depth is expressible without being mandatory.** `body/400/a0` and `body/400/a0/f0..f9` are the
same addressing scheme at two depths, which is what lets §11 bulk-import thumbnails and fetch full
same addressing scheme at two depths, which is what lets §11 bulk-import thumbnails and, if
animations on demand without a second protocol.
anything ever wants them, fetch full animations on demand without a second protocol. Phase 6
measured what that second depth costs and deferred it (§11.2); the point of the scheme is that
deferring it is a decision about *what to serve*, not about what can be named.
- **Nothing in the key is client-version-specific**, so a client patch changes an asset's *bytes*,
- **Nothing in the key is client-version-specific**, so a client patch changes an asset's *bytes*,
not its name — which is what makes §7's delta work.
not its name — which is what makes §7's delta work.
@@ -1019,9 +1071,9 @@ project. §1.1's measurements make the sizing question concrete:
|---|---|---|
|---|---|---|
| Item statics | **49,152** addressable, **39,189** with art (§11.1, phase 5) | No — on demand, cached, keyed by `itemId` (+ hue) |
| Item statics | **49,152** addressable, **39,189** with art (§11.1, phase 5) | No — on demand, cached, keyed by `itemId` (+ hue) |
| Land tiles | **16,384** addressable, **4,244** with art | No — on demand |
| Land tiles | **16,384** addressable, **4,244** with art | No — on demand |
| Creature/player bodies, first frame | **1,022** — 787 legacy (§4.8, *not* the 1,144 the library reports) + 235 UOP (§4.9) | **Yes** — this is the catalogue |
| Creature/player bodies, first frame | **1,095** — 787 legacy (§4.8, *not* the 1,144 the library reports) + 235 UOP (§4.9) + 73 at a later action (§11.2) | **Yes** — this is the catalogue |
| One body, every action, one direction | **210 frames** (body 400); 96–210 measured across six bodies | No — on demand, per body |
| One body, every action, one direction | **210 frames** (body 400); median 118 KB per body, max 9.6 MB (§11.2) | **Not served** (§11.2) |
| All bodies, every action, one direction | **~119,000 frames**, ~117 MB | No — but no longer unthinkable |
| All bodies, every action, one direction | **174,453 frames, 281.5 MB** — measured, not the ~119,000 estimated here | **Not served** (§11.2) |
| The same at five directions | ~865,000 frames | Not built (§5.1) |
| The same at five directions | ~865,000 frames | Not built (§5.1) |
`COALESCE` because a row written before the column existed has NULL there, and a NULL inside
`CONCAT` makes the whole comparison NULL — which would have taken every portrait off the site on
upgrade, with the database perfectly correct and nothing to see in a log. The `action` is stored
rather than parsed back out of the key because this join needs it in SQL, and re-deriving it there
would put a second, weaker parser of §5's key scheme in the schema.
`shard_creature_bodies` also answers §8 without a schema change on the atlas side:
`shard_creature_bodies` also answers §8 without a schema change on the atlas side:
`shard_spawn_creatures.name` already holds the ServUO **class name** — the atlas build picks the
`shard_spawn_creatures.name` already holds the ServUO **class name** — the atlas build picks the
@@ -1279,6 +1409,14 @@ discovers the gap as a refusal *per key, per pass, forever*, with no picture eve
warning in the log every few minutes. With it, that is one reported state carrying a sentence naming
warning in the log every few minutes. With it, that is one reported state carrying a sentence naming
the fix.
the fix.
**Phase 6 added one field and no command.** Manifest and fetch rows carry `action` — which action
of the body the thumbnail came from (§11.2). Additive, so **the protocol stays 8**; a consumer that
ignores it sees the catalogue it always saw plus 73 rows, and one that reads it can build the right
URL for a body catalogued at `a23`. `EXTRACTOR_VERSION` goes **2 → 3**, which is the change every
consumer does see. A fetch for a key naming an action the catalogue did not choose is answered
`unsupported` with the chosen action alongside it — never by decoding the asked-for action, which
is §4.10's wrong picture reached politely.
**Phase 4 added one field and no command.**`source` on a manifest or fetch row is `legacy` or
**Phase 4 added one field and no command.**`source` on a manifest or fetch row is `legacy` or
`uop` — which reader produced the bytes (§4.9). It is additive, so **the protocol stays 8**: a
`uop` — which reader produced the bytes (§4.9). It is additive, so **the protocol stays 8**: a
consumer that does not read it is unaffected, and one that does can say which half of the extractor
consumer that does not read it is unaffected, and one that does can say which half of the extractor
@@ -1326,7 +1464,7 @@ disagree, so a split bump means the next bundle silently fails to compose.
| 3 | **DONE 2026-09-10.** Body resolution (§8) + the **787**-body catalogue (§4.8), `assets.manifest` / `assets.fetch` / `assets.bodies` and their REST mirrors, `shard_spawn_creatures.art` filled and rendered (§8.1, §12.1). **787 rows in one 734 ms page; 455 types resolved at ~190 ms per 100 on the Core thread; zero mobiles leaked.**`UOFIDDLER.md` deleted, two phases early | servuo-plugins, **link**, module-uo |
| 3 | **DONE 2026-09-10.** Body resolution (§8) + the **787**-body catalogue (§4.8), `assets.manifest` / `assets.fetch` / `assets.bodies` and their REST mirrors, `shard_spawn_creatures.art` filled and rendered (§8.1, §12.1). **787 rows in one 734 ms page; 455 types resolved at ~190 ms per 100 on the Core thread; zero mobiles leaked.**`UOFIDDLER.md` deleted, two phases early | servuo-plugins, **link**, module-uo |
| 4 | **DONE 2026-09-11.** The UOP animation decoder (§4.3, §4.9): `BridgeUop` + a PNG encoder that never touches `System.Drawing`, wired in beneath the legacy reader. **Two of the eight player bodies turned out to exist** (gargoyles 666/667); the other six are in no client file, and ghost ids left the player-body set (§5.2, §17.9). The same fallback added **233 other bodies**: the catalogue is **1,022 rows, 1,409 ms cold**, and all six player bodies have art for the first time. `EXTRACTOR_VERSION` 1 → 2 | servuo-plugins |
| 4 | **DONE 2026-09-11.** The UOP animation decoder (§4.3, §4.9): `BridgeUop` + a PNG encoder that never touches `System.Drawing`, wired in beneath the legacy reader. **Two of the eight player bodies turned out to exist** (gargoyles 666/667); the other six are in no client file, and ghost ids left the player-body set (§5.2, §17.9). The same fallback added **233 other bodies**: the catalogue is **1,022 rows, 1,409 ms cold**, and all six player bodies have art for the first time. `EXTRACTOR_VERSION` 1 → 2 | servuo-plugins |
| 5 | **DONE 2026-09-11.** Item statics and land on demand (§11.1): the `static` and `land` families, hue applied on the shard from `tiledata.mul`, the byte-bounded art cache, `assets.fetch` made family-aware, `families` on `assets.sources`. Website side: the warm pass, per-row `catalog` staleness, and pictures on the marketplace and the character sheet. **39,189 statics and 4,244 land tiles served; the only refusals are the 9,963 + 12,140 empty index slots §4.5 predicted.** Two traps found — the library's bitmap cache poisons a hued sprite, and `PartialHue` decides the picture from a file only the shard has. Protocol stays 8; `EXTRACTOR_VERSION` stays 2 | servuo-plugins, module-uo |
| 5 | **DONE 2026-09-11.** Item statics and land on demand (§11.1): the `static` and `land` families, hue applied on the shard from `tiledata.mul`, the byte-bounded art cache, `assets.fetch` made family-aware, `families` on `assets.sources`. Website side: the warm pass, per-row `catalog` staleness, and pictures on the marketplace and the character sheet. **39,189 statics and 4,244 land tiles served; the only refusals are the 9,963 + 12,140 empty index slots §4.5 predicted.** Two traps found — the library's bitmap cache poisons a hued sprite, and `PartialHue` decides the picture from a file only the shard has. Protocol stays 8; `EXTRACTOR_VERSION` stays 2 | servuo-plugins, module-uo |
| 6 | Deep animation keys (`body/<id>/a<n>/f<n>`) for the future project, plus the bulk-fill switch | servuo-plugins, module-uo |
| 6 | **DONE 2026-09-11, and not what this row said.** The measurement came first and changed the phase: a complete one-direction animation set is **174,453 frames / 281.5 MB**, not the ~119,000 estimated, and **the site displays still pictures** — so the deep keys and the bulk-fill switch were **not built** (§11.2, org lead 2026-09-11). What shipped is what the still-picture site was missing: the **73 bodies with no art at action 0 and real art deeper** (a horse at `body/820/a23`), the catalogue key carrying its action, the atlas join that reads it, and §4.10's per-body **action ceiling** — without which the fallback walk itself would serve **452 validated pictures of the next body**. Catalogue **1,022 → 1,095**; `EXTRACTOR_VERSION` 2 → 3; protocol stays 8 | servuo-plugins, module-uo |
| 7 | The atlas over the sidecar (§10); shared-filesystem requirement retired | module-uo |
| 7 | The atlas over the sidecar (§10); shared-filesystem requirement retired | module-uo |
@@ -233,10 +233,12 @@ custom creatures) and points each `shard_spawn_creatures.art` at its picture.
Boot never calls the shard for this: the files change when an operator patches
Boot never calls the shard for this: the files change when an operator patches
their client, which is an event they know about and the site does not.
their client, which is an event they know about and the site does not.
On this machine's stock client that is **1,022 creature portraits**, about a
On this machine's stock client that is **1,095 creature portraits**, about a
megabyte in total — 787 out of the legacy `anim*.mul` files and 235 more out of
megabyte in total — 787 out of the legacy `anim*.mul` files, 235 more out of
`AnimationFrame*.uop`, which ServUO's own decoder never opens
`AnimationFrame*.uop`, which ServUO's own decoder never opens
([`../link/v8.md`](../link/v8.md) §4.9).
([`../link/v8.md`](../link/v8.md) §4.9), and 73 more that have no art at the
walk's first action and real art at a later one, which the import now falls back
to (§11.2). Body 820 is one of them, and it is a horse.
**NULL stays a first-class state, and always will be.** An install with no shard
**NULL stays a first-class state, and always will be.** An install with no shard
link has never imported one; a Linux shard host without `libgdiplus` cannot
link has never imported one; a Linux shard host without `libgdiplus` cannot
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.