feat(asset-bridge): the body catalogue and slug to body id (Phase 3)

Two request families, and they run on opposite threads on purpose.

`assets.bodies` (BridgeBodies) answers the question only code inside ServUO
can: the atlas knows a creature by the class name in Spawns/*.xml, the client
knows it by a body id, and nothing in the tree declares the mapping. Construct
the type, read Body.BodyID, Delete(). That is world mutation, so it answers on
the CORE thread and is the one family here that does not take the asset
worker's slot -- and the batch is capped at 100 names, REFUSED rather than
truncated, because a truncated answer is indistinguishable from a complete one
from the website's side.

`assets.manifest` / `assets.fetch` (BridgeCatalog) are the catalogue, on the
worker. The manifest carries { key, sha256, bytes, width, height } and no
pixels, so an Update fetches only what moved; the fetch carries base64 PNG.
The scan keeps the bytes it hashed rather than decoding all 787 sprites twice.

Three things worth stating about the shapes:

- It pages on the WALL CLOCK as well as on bytes. The rows are ~90 bytes and
  the whole catalogue is one page by the byte budget, but building it means
  decoding hundreds of sprites against a 10 s reply timeout.
- `catalog` is derived from the client files (sizes, mtimes, both direction
  settings, EXTRACTOR_VERSION), not minted per build -- the cache is released
  when idle, and a fresh id per build would force a restart mid-import although
  nothing about the client moved.
- ARGB1555 is expanded to 32bpp here rather than handed to GDI+, because what
  it does with a one-bit alpha channel varies by platform and a black rectangle
  behind every sprite would pass any test that only checked the bytes decoded.

Nothing trusts the library's success. Every body goes through CheckEntry and
AnimationRecordSane before it is decoded, which is what keeps the 357 bodies
whose index entry reads `length 0` -- and which the decoder hands back the
PREVIOUS creature's bitmap for -- out of the catalogue.

Walked on a live shard: 787 rows in one 734 ms page; bodies 320, 607, 666 all
absent rather than wrong; 783 at direction 1 and 4 at direction 0; all 455 stock
creature classes resolved at ~190 ms per 100 with zero mobiles leaked.

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 18:40:12 -05:00
parent c79a2a3b2b
commit 64c0ec00b1
6 changed files with 1375 additions and 0 deletions

View File

@@ -307,6 +307,39 @@ AssetsEnabled=true
# still fits.
AssetBatchBytes=524288
# How many ServUO class names one `assets.bodies` request may carry (phase 3). The only
# bound on this plane counted in items rather than bytes, because what it bounds is not
# reply size -- it is constructing and deleting that many real mobiles ON THE CORE
# THREAD, between two ticks of the world. A larger request is refused, never truncated.
# Clamped to [1, 500].
AssetBodyBatch=100
# How many keys one `assets.fetch` request may name. The byte budget above still decides
# where a page is cut; this only bounds how large a request the shard will parse at all.
# Clamped to [1, 10000].
AssetFetchKeys=2000
# The wall-clock budget for one catalogue page, in milliseconds. The catalogue's manifest
# rows are ~90 bytes so the byte budget never stops it -- but building them means
# decoding hundreds of animations, and the sidecar waits 10 s for a reply. Kept well
# under that, because the page still has to be serialised and written afterwards.
# Clamped to [250, 5000].
AssetScanMs=3000
# Which direction the catalogue renders. NOT part of the asset key: five directions
# would five-fold every count in the working set to express a choice nobody varies.
#
# The split was found by RENDERING all five, not from a table. 0 is head-on, facing the
# viewer -- what a character portrait wants, and the least legible view there is of a
# four-legged creature (a wolf seen from the front is a dark blob). 1 is the front
# three-quarter, where the same wolf is unmistakably a wolf.
#
# Which bodies count as player bodies is asked of the shard (every registered race's
# male/female/ghost ids), never hardcoded. Clamped to [0, 4]: 5-7 are the client
# mirroring 1-3 through a decode branch this overlay has not verified.
AssetPlayerDirection=0
AssetCreatureDirection=1
# The test scaffolding in tools/scaffolding/ reads its own flags from this file
# (SeedOnStart, CensusOnStart, ProbeOnStart). They are absent here on purpose:
# Config.Get returns the default of false when a key is missing, so a deployed