feat(asset-bridge): item and land art on demand, hued where the files are (Phase 5) #32

Merged
whitlocktech merged 1 commits from feat/asset-bridge-p5 into edge 2026-09-11 11:26:42 +00:00
Member

Phase 5 of the Asset Bridge (docs/link/v8.md §5, §11, §11.1). Protocol stays 8; EXTRACTOR_VERSION stays 2.

The body catalogue is a set. This is not: this client addresses 49,152 static ids and has art for 39,189, plus 4,244 land tiles of 16,384 — and hues multiply the statics by three thousand. So there is no manifest and no scan. assets.fetch grows two families and answers the keys the website's own data names.

static/3922        static/3922/h33        land/3

assets.fetch becomes shared plumbing

Phase 3 gave the command to the body catalogue outright, which was right with one family and wrong with three. BridgeAssets now owns it, does the reqId / consent / key-ceiling checks once, derives the family from the keys themselves and dispatches to the reader that registered it.

§5 made the key the address of an asset; a request that also named its family would have two places to be wrong and one of them silent. A batch must be of one family — mixing is refused 400 — because the reply carries a single catalog id and two families have two fingerprints.

assets.sources gains families. Additive, so the protocol stays 8 — and without it a website talking to a phase-4 overlay discovers the gap as a refusal per key, per pass, forever, with no picture ever appearing.

Two traps, both §4.5's failure mode: a confident, plausible, wrong picture

The library's cache poisons a hued sprite. Art.GetStatic and Art.GetLand memoise into a static Bitmap[0xFFFF] and hand back the same instance; Hue.ApplyTo repaints in place. Measured before the fix: hue item 3922 once, and every later request for the plain 3922 comes back hued, with the next hue stacking on the first. The key is right, the dimensions are right, the hash is stable — nothing downstream can see it.

Files.CacheData is now off for the life of the process. It pays twice: that array is never trimmed, so decoding this client's 39,189 statics would otherwise leave 74 MB of Bitmap in a static field of a game server. TryHue re-checks the flag and refuses rather than risk it, and the same flag decides whether a bitmap is ours to dispose. Copying instead does not solve the retention — and new Bitmap(src) throws on the Format16bppArgb1555 these decoders produce.

PartialHue decides the picture, and only the shard can read it. A hue is a 32-entry ramp out of hues.mul indexed by each pixel's red channel; whether it replaces every pixel or only the grey ones is a per-item-id flag in tiledata.mul13,259 of 65,536 ids on this client. Item 597 is a wooden screen with painted flowers: hued 33 one way the flowers turn red, the other way the whole screen does. Both decode, both are 44×112, both report success.

So land takes no hue segment (the mode is an item flag; land has no equivalent, and guessing is how you get a wrong picture) and h0 is not a key (hue 0 means "not hued", so the plain key already names it).

The namespace trap that compiled

The first cut wrote TileData.ItemTable unqualified. ServUO declares its own Server.TileData / Server.ItemData / Server.TileFlag — with a PartialHue member — and this file lives in Server.Custom.Bridge, where the enclosing namespace beats using Ultima;. It compiled, and at runtime read a file resolved through Core.DataDirectories instead of Ultima.Files — §4.6's rule broken in a new place. The live rig caught it as a TypeInitializationException refusing every hued key, from a class the code never meant to name. Every such type is spelled Ultima. now.

Measured

Through the reader itself, over the whole range on a stock client:

Statics served 39,189
Land tiles served 4,244
Refusals 9,963 + 12,140 — exactly the empty index slots §4.5 predicted, and nothing else
Whole set as PNG 43,433 files, 81 MB, mean 1.9 KB

Nothing that carries art is refused, which is the half of the measurement that says the boundary is in the right place (§4.5).

Live walk, real ServUO + real sidecar

  • assets.sourcesfamilies: ["body","land","static"], artDataFile: artlegacymul.uop
  • static/597/h33partialHue: true; static/1/h33false; rendered and checked by eye (the flowers go red, the wood does not)
  • hued-first-then-plain returns the correct plain hash — the poisoning regression
  • land/3, land/0, land/16383 ok; land/3/h33, static/3922/h0, static/3922/h3001, static/60000unsupported, each with a reason
  • mixing families → 400; a wrong catalog422
  • 900 keys → cut at the byte budget after 129 rows (496 KB), cursor: k:129, resumes at static/130
  • body/400/a0 (legacy) and body/666/a0 (uop) unchanged — the catalogue is untouched

Scripts compile clean against ServUO 57.4; no shard console warnings across the walk.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Phase 5 of the Asset Bridge (`docs/link/v8.md` §5, §11, §11.1). Protocol stays **8**; `EXTRACTOR_VERSION` stays **2**. The body catalogue is a *set*. This is not: this client addresses **49,152 static ids** and has art for **39,189**, plus **4,244** land tiles of 16,384 — and hues multiply the statics by three thousand. So there is no manifest and no scan. `assets.fetch` grows two families and answers the keys the website's own data names. ``` static/3922 static/3922/h33 land/3 ``` ## `assets.fetch` becomes shared plumbing Phase 3 gave the command to the body catalogue outright, which was right with one family and wrong with three. `BridgeAssets` now owns it, does the `reqId` / consent / key-ceiling checks once, **derives the family from the keys themselves** and dispatches to the reader that registered it. §5 made the key the address of an asset; a request that *also* named its family would have two places to be wrong and one of them silent. A batch must be of one family — mixing is refused **400** — because the reply carries a single `catalog` id and two families have two fingerprints. `assets.sources` gains **`families`**. Additive, so the protocol stays 8 — and without it a website talking to a phase-4 overlay discovers the gap as a refusal per key, per pass, forever, with no picture ever appearing. ## Two traps, both §4.5's failure mode: a confident, plausible, wrong picture **The library's cache poisons a hued sprite.** `Art.GetStatic` and `Art.GetLand` memoise into a static `Bitmap[0xFFFF]` and hand back **the same instance**; `Hue.ApplyTo` repaints **in place**. Measured before the fix: hue item 3922 once, and every later request for the *plain* 3922 comes back hued, with the next hue stacking on the first. The key is right, the dimensions are right, the hash is stable — nothing downstream can see it. `Files.CacheData` is now off for the life of the process. It pays twice: that array is never trimmed, so decoding this client's 39,189 statics would otherwise leave **74 MB of `Bitmap` in a static field of a game server**. `TryHue` re-checks the flag and refuses rather than risk it, and the same flag decides whether a bitmap is ours to dispose. Copying instead does not solve the retention — and `new Bitmap(src)` **throws** on the `Format16bppArgb1555` these decoders produce. **`PartialHue` decides the picture, and only the shard can read it.** A hue is a 32-entry ramp out of `hues.mul` indexed by each pixel's red channel; whether it replaces every pixel or only the grey ones is a per-item-id flag in `tiledata.mul` — **13,259 of 65,536** ids on this client. Item 597 is a wooden screen with painted flowers: hued 33 one way the flowers turn red, the other way the whole screen does. Both decode, both are 44×112, both report success. So **land takes no hue segment** (the mode is an item flag; land has no equivalent, and guessing is how you get a wrong picture) and **`h0` is not a key** (hue 0 means "not hued", so the plain key already names it). ## The namespace trap that compiled The first cut wrote `TileData.ItemTable` unqualified. ServUO declares its **own** `Server.TileData` / `Server.ItemData` / `Server.TileFlag` — with a `PartialHue` member — and this file lives in `Server.Custom.Bridge`, where the enclosing namespace beats `using Ultima;`. It compiled, and at runtime read a file resolved through `Core.DataDirectories` instead of `Ultima.Files` — §4.6's rule broken in a new place. The live rig caught it as a `TypeInitializationException` refusing every hued key, from a class the code never meant to name. Every such type is spelled `Ultima.` now. ## Measured Through the reader itself, over the whole range on a stock client: | | | |---|---| | Statics served | **39,189** | | Land tiles served | **4,244** | | Refusals | **9,963 + 12,140** — exactly the empty index slots §4.5 predicted, and nothing else | | Whole set as PNG | 43,433 files, 81 MB, mean 1.9 KB | **Nothing that carries art is refused**, which is the half of the measurement that says the boundary is in the right place (§4.5). ## Live walk, real ServUO + real sidecar - `assets.sources` → `families: ["body","land","static"]`, `artDataFile: artlegacymul.uop` - `static/597/h33` → `partialHue: true`; `static/1/h33` → `false`; rendered and checked **by eye** (the flowers go red, the wood does not) - hued-first-then-plain returns the correct plain hash — the poisoning regression - `land/3`, `land/0`, `land/16383` ok; `land/3/h33`, `static/3922/h0`, `static/3922/h3001`, `static/60000` → `unsupported`, each with a reason - mixing families → **400**; a wrong `catalog` → **422** - 900 keys → cut at the byte budget after 129 rows (496 KB), `cursor: k:129`, resumes at `static/130` - `body/400/a0` (legacy) and `body/666/a0` (uop) unchanged — the catalogue is untouched Scripts compile clean against ServUO 57.4; no shard console warnings across the walk. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-11 11:17:09 +00:00
The body catalogue is a set; this is not. This client addresses 49,152 static
ids and has art for 39,189 of them, plus 4,244 land tiles of 16,384 -- and hues
multiply the statics by three thousand. So there is no manifest and no scan:
`assets.fetch` grows two more families (`static`, `land`) and answers the keys
the website's own data names.

`assets.fetch` becomes shared plumbing. BridgeAssets now owns the command, does
the reqId/consent/key-ceiling checks once, derives the family from the keys
themselves (§5 made the key the address; a request that also named its family
would have two places to be wrong and one of them silent) and dispatches to the
reader that registered it. A batch must be of one family, because the reply
carries one `catalog` id. `assets.sources` gains `families` -- additive, so the
protocol stays 8, and EXTRACTOR_VERSION stays 2 because no existing key's bytes
change.

Two traps, both in §4.5's family -- a confident, plausible, wrong picture:

- `Art.GetStatic` memoises into a static Bitmap[0xFFFF] and hands back the SAME
  instance, while `Hue.ApplyTo` repaints in place. Hue a static once and the
  plain key comes back hued from then on, and the next hue stacks on the last.
  Measured on this client before the fix. `Files.CacheData` is now off for the
  life of the process; `TryHue` re-checks it and refuses rather than risk it,
  and the same flag decides whether a bitmap is ours to dispose.

- `PartialHue` decides whether a hue repaints every pixel or only the grey ones,
  per item id, out of `tiledata.mul` -- 13,259 of 65,536 ids on this client.
  Item 597 is a wooden screen with painted flowers: one mode reddens the
  flowers, the other the whole screen. Both decode. The first cut of this reader
  bound `TileData` to ServUO's OWN `Server.TileData` (the enclosing namespace
  beats `using Ultima;`, and it has a PartialHue flag too), which compiled and
  refused every hued key at runtime. Every such type is spelled `Ultima.` now.

Land takes no hue segment: the mode that decides how is an item flag and land
has no equivalent, so `land/3/h33` is refused rather than guessed. `h0` is not a
key either -- hue 0 means "not hued", and the plain key already names it.

Measured through the reader over the whole range: 39,189 statics and 4,244 land
tiles served, and the only refusals are the 9,963 + 12,140 empty index slots
§4.5 predicted. Nothing that carries art is refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit b68aac41c6 into edge 2026-09-11 11:26:42 +00:00
whitlocktech deleted branch feat/asset-bridge-p5 2026-09-11 11:26:43 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/servuo-plugins#32
No description provided.