Phase 4 built 4.3's UOP animation reader. What it found first changed what the phase
was worth, so the plan is corrected rather than merely annotated.
- New 4.9: what phase 4 measured. Of the EIGHT player bodies 4.8 assigned this
phase, two are in the client at all -- gargoyles 666/667, in AnimationFrame3.uop.
The six ghost bodies are in no package, and that is established rather than
unfound: the five packages hold 10,724 entries and the
build/animationlegacyframe/%06d/%02d.bin scheme claims every one, leaving no room
for another naming. Also the format as read (AMOU, a per-frame ARGB1555 palette,
direction as a slice of the frame table), the nine bodies whose frame count is not
a multiple of five, the validate-as-we-go bounds and the measurement that says
they refuse nothing real, and the live rig.
- 5.2 rewritten: the player-body set is the LIVING pair per race, six ids not
twelve. Ghost ids left it because no client has art for any of them. Still asked
of the shard, never hardcoded -- only the question changed. And with phase 4 in,
all six have art for the first time.
- 4.3 rewritten against what was measured, including why searching five UOP packages
for one body is NOT the never-sweep rule being broken: a legacy index is addressed
by position, a UOP entry by the hash of a name carrying the body id, which the
payload then declares again.
- 11 sizing: the catalogue is 1,022, not 787. The mix is recorded because "add every
body" sounds like it changes what a catalogue is, and it does not -- the legacy
787 was already 366 equipment bodies.
- 14: manifest and fetch rows carry `source` (legacy/uop). Additive, so protocol
stays 8; EXTRACTOR_VERSION 1 -> 2 is the change consumers actually see.
- 16 phase 4 marked DONE; 17.9 records the four org-lead decisions (fallback applies
to every body; ghost ids leave the set; own PNG encoder; NO_IMAGING stays flat).
- 4.8 and 8.1 keep their numbers as the record of what those phases measured, with a
pointer to where the answer landed.
Two consumer docs repeated the ghost claim as fact and are corrected:
website/SPAWN_ATLAS.md (787 -> 1,022, and "two thirds of the playable ghost and
gargoyle bodies have no art" -> about half the addressable body range) and
modules/uo/API.md (same sentence).
Code: servuo-plugins#31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-11 04:59:55 -05:00
3 changed files with 232 additions and 61 deletions
**Four of twelve, not six.** The elf ghosts were counted as decoding for as long as the library's
The catalogue must still **not** treat a missing player body as an error. It is now an unlikely
own answer was the measurement: their index entry has no record, and what came back was whatever
answer rather than the expected one, but a client that ships fewer bodies than this one is an
the catalogue walk had decoded the moment before — the elf female, at her exact dimensions. All
ordinary thing to meet, and a status screen that flags failures on every import teaches an operator
eight are UOP-only and **§4.3 adds a decoder for them** — verified genuinely absent from the legacy
to ignore it. `shard_spawn_creatures.art` staying NULL remains a first-class state everywhere it is
files rather than merely mis-addressed, by rendering what the other anim files hold at those
consumed, which it already is.
indices and finding spiders.
Until that phase lands, and on any client that lacks them entirely, the catalogue must **not** treat
a missing player body as an error. It is the expected answer for two thirds of the set, and a
status screen that flags eight failures on every import teaches an operator to ignore it.
`shard_spawn_creatures.art` staying NULL remains a first-class state everywhere it is consumed,
which it already is.
---
---
@@ -714,7 +851,7 @@ Every prediction in §4.8 and §5.2 held when the bytes were actually rendered a
| `body/607/a0`, `body/608/a0` (elf ghosts) | **absent** | §4.8's two rows that moved; the library returns the elf female here |
| `body/607/a0`, `body/608/a0` (elf ghosts) | **absent** | §4.8's two rows that moved; the library returns the elf female here |
| `body/666/a0` (gargoyle → `anim5`) | **absent** | The spider trap. Nothing swept, nothing found |
| `body/666/a0` (gargoyle → `anim5`) | **absent** | The spider trap. Nothing swept, nothing found |
| `body/400/a0/…` deeper key | **unsupported** | Well-formed under §5, not served until phase 6 |
| `body/400/a0/…` deeper key | **unsupported** | Well-formed under §5, not served until phase 6 |
| direction distribution across 787 | **783 at index 1, 4 at index 0** | Four player bodies, not six — §4.8 again, from `Race.AllRaces` rather than a table |
| direction distribution across 787 | **783 at index 1, 4 at index 0** | Four player bodies, not six — §4.8 again, from `Race.AllRaces` rather than a table. *Phase 4 made it six of six and 1,016/6 across 1,022 rows: §4.9* |
**44 of the 787 hashes are shared by two or three bodies, and that is correct.** It is the exact
**44 of the 787 hashes are shared by two or three bodies, and that is correct.** It is the exact
signature the wrong-picture bug produces, so it was chased rather than assumed: the client's own
signature the wrong-picture bug produces, so it was chased rather than assumed: the client's own
@@ -882,22 +1019,27 @@ project. §1.1's measurements make the sizing question concrete:
|---|---|---|
|---|---|---|
| Item statics | **~49,150** | No — on demand, cached, keyed by `itemId` (+ hue) |
| Item statics | **~49,150** | No — on demand, cached, keyed by `itemId` (+ hue) |
| Land tiles | **16,384** | No — on demand |
| Land tiles | **16,384** | No — on demand |
| Creature/player bodies, first frame | **787** (§4.8 — *not* the 1,144 the library reports) | **Yes** — this is the catalogue |
| 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 |
| 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); 96–210 measured across six bodies | No — on demand, per body |
| All bodies, every action, one direction | **~119,000 frames**, ~117 MB | No — but no longer unthinkable |
| All bodies, every action, one direction | **~119,000 frames**, ~117 MB | No — but no longer unthinkable |
| The same at five directions | ~865,000 frames | Not built (§5.1) |
| The same at five directions | ~865,000 frames | Not built (§5.1) |
| `tree.manifest` / `tree.fetch` | `.ok` | §10, the ServUO tree files | phase 7 |
| `tree.manifest` / `tree.fetch` | `.ok` | §10, the ServUO tree files | phase 7 |
@@ -1025,6 +1167,12 @@ side effect — a few hundred asset keys do not belong in a query string. They a
this link that take one. `422` gains a second meaning on this plane alongside "cannot decode": the
this link that take one. `422` gains a second meaning on this plane alongside "cannot decode": the
mid-import guard, a `catalog` that no longer describes the files on disk.
mid-import guard, a `catalog` that no longer describes the files on disk.
**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
consumer that does not read it is unaffected, and one that does can say which half of the extractor
to doubt when a picture is wrong. `EXTRACTOR_VERSION` went to **2** in the same phase, which is the
change every consumer *does* see, exactly as §7 intends.
**§16 listed phase 3 as `servuo-plugins, module-uo` and that was wrong** — `web.rs` routes every
**§16 listed phase 3 as `servuo-plugins, module-uo` and that was wrong** — `web.rs` routes every
command explicitly and has no generic `/assets/*` forwarder, so `link` is in the phase too. The
command explicitly and has no generic `/assets/*` forwarder, so `link` is in the phase too. The
table now says so.
table now says so.
@@ -1064,7 +1212,7 @@ disagree, so a split bump means the next bundle silently fails to compose.
| 1 | **DONE 2026-09-10.** The transport: `assets.sources`, the single-slot gate (§3.2), the paging envelope (§3.4), the sidecar line cap (§3.3), `EXTRACTOR_VERSION`, `NO_IMAGING`, protocol 7→8. Plus §4.5's validator promoted into the overlay and extended to animations — which found **357 wrong pictures in the body catalogue on a stock client** and cut it from 1,144 to 787 (§4.8) | servuo-plugins, link |
| 1 | **DONE 2026-09-10.** The transport: `assets.sources`, the single-slot gate (§3.2), the paging envelope (§3.4), the sidecar line cap (§3.3), `EXTRACTOR_VERSION`, `NO_IMAGING`, protocol 7→8. Plus §4.5's validator promoted into the overlay and extended to animations — which found **357 wrong pictures in the body catalogue on a stock client** and cut it from 1,144 to 787 (§4.8) | servuo-plugins, link |
| 2 | **DONE 2026-09-10.** Clilocs end to end (§9.1, §9.2): the Mythic decompressor ported into the overlay, `cliloc.table` + `GET /cliloc`, the paging walk and the source switch on the website, module-uo's protocol pin 7→8. `cliloc-export/` deleted and `UOFIDDLER.md` §Part 1 with it. **67,496 rows, 290 ms, ~11 pages** — the same count UOFiddler's own DLL produced from this client | all |
| 2 | **DONE 2026-09-10.** Clilocs end to end (§9.1, §9.2): the Mythic decompressor ported into the overlay, `cliloc.table` + `GET /cliloc`, the paging walk and the source switch on the website, module-uo's protocol pin 7→8. `cliloc-export/` deleted and `UOFIDDLER.md` §Part 1 with it. **67,496 rows, 290 ms, ~11 pages** — the same count UOFiddler's own DLL produced from this client | all |
| 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 | **The UOP animation decoder (§4.3)** — the **eight** player bodies the legacy path cannot reach (§4.8 moved the elf ghosts across), the never-sweep rule already enforced in code by phase 1's `ResolveAnimation` | 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 | Item statics and land on demand, hued keys, the cache | servuo-plugins, module-uo |
| 5 | Item statics and land on demand, hued keys, the cache | 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 | Deep animation keys (`body/<id>/a<n>/f<n>`) for the future project, plus the bulk-fill switch | 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 |
@@ -1077,11 +1225,12 @@ a client that has been patched — *before* building eight phases on top of it.
§1.1 were run from PowerShell against a stock client; neither of those is the environment this will
§1.1 were run from PowerShell against a stock client; neither of those is the environment this will
actually run in.
actually run in.
Phase 4 sits **after** the catalogue rather than inside it on purpose. The catalogue is useful with
Phase 4 sat **after** the catalogue rather than inside it on purpose. The catalogue was useful with
783 of its 787 bodies, the UOP reader is the one piece of genuinely new format work in this
783 of its 787 bodies, the UOP reader is the one piece of genuinely new format work in this
protocol, and putting it on the critical path would hold up every website-side phase behind it. Its
protocol, and putting it on the critical path would have held up every website-side phase behind
acceptance test is not "it decodes" — it is that a gargoyle looks like a gargoyle, checked by eye,
it. Its acceptance test was not "it decodes" — it was that a gargoyle looks like a gargoyle,
because §4.3's whole point is that this failure mode produces confident, wrong pictures.
checked by eye, because §4.3's whole point is that this failure mode produces confident, wrong
pictures. It does, and it was.
---
---
@@ -1091,8 +1240,8 @@ Every item here is settled. Each is recorded because it changes numbers or oblig
in the document.
in the document.
1.**§4: settled 2026-09-10 — call ServUO's vendored `Ultima`,** with one exception added the same
1.**§4: settled 2026-09-10 — call ServUO's vendored `Ultima`,** with one exception added the same
day: **§4.3's UOP animation decoder**, scoped to the player bodies the legacy path cannot reach
day: **§4.3's UOP animation decoder** (built in phase 4; §17.9 widened its scope from the
and to nothing else (phase 4). The crash is confined to the `hasExtra: true` branch that only
player bodies to every body the legacy path cannot reach). The crash is confined to the `hasExtra: true` branch that only
`Gumps` reaches, and nothing here calls `Gumps`. **Phase 0 confirmed both halves in the real
`Gumps` reaches, and nothing here calls `Gumps`. **Phase 0 confirmed both halves in the real
host process** — nothing faulted on a path we call, and one `GetGump(2)` killed the shard
host process** — nothing faulted on a path we call, and one `GetGump(2)` killed the shard
outright (§4.7).
outright (§4.7).
@@ -1148,3 +1297,24 @@ in the document.
found: the animation path has §4.5's shared-buffer defect too, and 357 ids with no art were
found: the animation path has §4.5's shared-buffer defect too, and 357 ids with no art were
returning the previously-decoded body's bitmap. The elf ghosts moved from "decodes" to "no art"
returning the previously-decoded body's bitmap. The elf ghosts moved from "decodes" to "no art"
in §5.2 for the same reason, taking phase 4's set from six player bodies to eight.
in §5.2 for the same reason, taking phase 4's set from six player bodies to eight.
9.**§4.3/§5.2: phase 4's four, settled 2026-09-11.** Put to the org lead after the packages were
opened and before the reader was written, because the first measurement changed what the phase
was worth:
- **The UOP fallback applies to every body, not only to player bodies.** It is the same rule
either way — "legacy first, UOP when it has nothing" — and restricting it would have needed an
extra filter to *refuse* art the client has. 787 → 1,022 rather than 787 → 789.
- **Ghost ids leave the player-body set** (org lead, over keeping them with a new status or
leaving them as `absent`). No client has art for any of the six, so listing them advertised
keys that cannot exist. The shard is still asked rather than told — the question is now the
living pair per race.
- **The UOP path gets its own PNG encoder** rather than `Bitmap.Save`, which is what §4.4's
sentence about this reader and `System.Drawing` was always promising. Phase 3's `ToPng` is
untouched: its input comes from a decoder that needs GDI+ to produce a pixel, so encoding it
without GDI+ buys nothing.
- **A host without libgdiplus keeps the flat `NO_IMAGING` refusal** (org lead, over serving the
~238 bodies that no longer need it). One answer beats a quarter-full catalogue nobody can tell
from a complete one. The door §4.4 wanted open stays open; phase 4 simply does not walk
@@ -82,7 +82,7 @@ account-linking routes and the sidecar config under `/admin/uo-link` are in the
| GET | `/shard/clilocs` | cliloc-table status (`adminOnly`): every source found now (base first, then `custom/` overlays in merge order), what each contributed at the last import, readability, drift across the set, the entry count, and `missingSources`. `configured:false` is a supported state — item names then render as ids. No public counterpart: the table is never served *as* a table. |
| GET | `/shard/clilocs` | cliloc-table status (`adminOnly`): every source found now (base first, then `custom/` overlays in merge order), what each contributed at the last import, readability, drift across the set, the entry count, and `missingSources`. `configured:false` is a supported state — item names then render as ids. No public counterpart: the table is never served *as* a table. |
| POST | `/shard/clilocs/import` | reload after a client patch or an overlay edit; `{force}` ignores the hash gate, `{approve}` accepts a **vanished** source (refused by default — see the table notes above). **A missing path — or the likely mistake of pointing at the client's own COMPRESSED `Cliloc.enu` — answers 200 with `status:"unavailable"` and a `code`, not 500.**`COMPRESSED` is called out by name: a 500 would say only "something broke", and the operator needs to be told which file to convert. |
| POST | `/shard/clilocs/import` | reload after a client patch or an overlay edit; `{force}` ignores the hash gate, `{approve}` accepts a **vanished** source (refused by default — see the table notes above). **A missing path — or the likely mistake of pointing at the client's own COMPRESSED `Cliloc.enu` — answers 200 with `status:"unavailable"` and a `code`, not 500.**`COMPRESSED` is called out by name: a 500 would say only "something broke", and the operator needs to be told which file to convert. |
| GET | `/shard/assets` | client-asset import status (`adminOnly`, Protocol 8): the imported body catalogue, how many sprites are on disk, how many atlas creatures resolved to a body id, and the shard's own client files beside them. `drift:true` means the client was patched. `shard.imaging.ok:false` is the named `NO_IMAGING` state — a Linux shard host with no `libgdiplus` cannot decode a sprite at all, and the reason names the package. No public counterpart: the pictures are served as ordinary files under `/uploads`. |
| GET | `/shard/assets` | client-asset import status (`adminOnly`, Protocol 8): the imported body catalogue, how many sprites are on disk, how many atlas creatures resolved to a body id, and the shard's own client files beside them. `drift:true` means the client was patched. `shard.imaging.ok:false` is the named `NO_IMAGING` state — a Linux shard host with no `libgdiplus` cannot decode a sprite at all, and the reason names the package. No public counterpart: the pictures are served as ordinary files under `/uploads`. |
| POST | `/shard/assets/import` | import creature artwork from the shard's UO client; `{force}` ignores the hash gate, `{approve}` accepts a catalogue that no longer offers assets this site holds (refused by default — an unmounted client volume and a deliberate downgrade are indistinguishable, and the wrong guess deletes artwork). **This is the ONLY thing that imports** — boot deliberately never calls the shard. An operator's `spawnAtlas.art.json` always wins over an imported sprite. A body this client has no art for is **not** a failure: two thirds of the playable ghost and gargoyle bodies are in that state on a stock client. |
| POST | `/shard/assets/import` | import creature artwork from the shard's UO client; `{force}` ignores the hash gate, `{approve}` accepts a catalogue that no longer offers assets this site holds (refused by default — an unmounted client volume and a deliberate downgrade are indistinguishable, and the wrong guess deletes artwork). **This is the ONLY thing that imports** — boot deliberately never calls the shard. An operator's `spawnAtlas.art.json` always wins over an imported sprite. A body this client has no art for is **not** a failure: about half the addressable body range is in that state on a stock client. |
| PUT | `/shard/clilocs/path` | point the site at a different cliloc base file or directory (persisted as `cliloc_client_path`, which wins over `UO_CLIENT_PATH`). Overlays are read from `custom/` beside it either way. Blank clears it. Deliberately **does not import**, same reasoning as the atlas path. |
| PUT | `/shard/clilocs/path` | point the site at a different cliloc base file or directory (persisted as `cliloc_client_path`, which wins over `UO_CLIENT_PATH`). Overlays are read from `custom/` beside it either way. Blank clears it. Deliberately **does not import**, same reasoning as the atlas path. |
## 4. Shard visibility — the audience boundary (Protocol 3.0)
## 4. Shard visibility — the audience boundary (Protocol 3.0)
@@ -233,15 +233,16 @@ 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 **787 creature portraits**, about a
On this machine's stock client that is **1,022 creature portraits**, about a
megabyte in total.
megabyte in total — 787 out of the legacy `anim*.mul` files and 235 more out of
`AnimationFrame*.uop`, which ServUO's own decoder never opens
([`../link/v8.md`](../link/v8.md) §4.9).
**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
render a sprite at all (a named `NO_IMAGING` status, not an error); and even
render a sprite at all (a named `NO_IMAGING` status, not an error); and about
after a complete import, two thirds of the playable ghost and gargoyle bodies
half the addressable body range has no art in any client file. Pages render
have no art in the client. Pages render without images, which is normal and
without images, which is normal and supported, not degraded.
supported, not degraded.
### The operator's own artwork still wins
### The operator's own artwork still wins
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.