feat(asset-bridge): the body catalogue and slug → body id (Phase 3) #30

Merged
whitlocktech merged 1 commits from feat/asset-bridge-p3 into edge 2026-09-10 23:57:34 +00:00
Member

Phase 3 of the Asset Bridge (docs/link/v8.md §4.8, §5, §6, §8). Two request families, on opposite threads on purpose.

assets.bodies — the Core thread (BridgeBodies.cs)

The atlas knows a creature by the class name in Spawns/*.xml; the client knows it by a body id; nothing in the ServUO tree declares the mapping. Construct the type, read Body.BodyID, Delete(). That is world mutation, so it answers on the Core thread — and it is the one family here that deliberately does not take the asset worker's single slot, because the work is not on the worker and the reply is a couple of kilobytes.

The batch is capped at 100 names and an over-long request is refused, never truncated: a silently shortened answer is indistinguishable from a complete one from the website's side, and the names that fell off the end would be recorded as "asked and unanswerable" rather than "never asked".

Every outcome is a row, not a failed request — ok / unknown / notCreature / failed. A shard is expected to be asked about types it does not have, and a pass that failed over one would teach an operator to stop pressing the button.

assets.manifest / assets.fetch — the worker (BridgeCatalog.cs)

The manifest carries { key, sha256, bytes, width, height, body, direction } 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 a second time moments later.

Three shapes worth flagging for review:

  • 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, not minted per build. The cache is released when idle, and a fresh id per build would force a restart halfway through a slow import although nothing about the client had moved.
  • ARGB1555 is expanded to 32bpp here rather than handed to GDI+. What GDI+ does with a one-bit alpha channel varies by platform (Mono is a different implementation entirely), 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. That is what keeps the 357 bodies whose index entry reads length 0 — and for which the decoder hands back the previously decoded creature's bitmap — out of the catalogue.

Walked on a live shard (43k mobiles, 210k items, real sidecar)

Check Result
Catalogue size 787, in one page, 734 ms cold
body/320/a0, body/607/a0, body/666/a0 absent — not another creature's picture
Direction split across 787 783 at index 1, 4 at index 0 — four player bodies, not six
All 455 stock creature classes resolved at ~190 ms per 100 on the Core thread
Mobiles after 455 constructions 42,924 vs 43,000 at boot — nothing leaked, items fell too
Refusals unknown family → 400, stale catalog → 422, 101 types → 400 naming the cap

The pictures were rendered and looked at, which is the only method that catches this failure mode — a dragon, a giant spider, a wolf at three-quarter, and human male / human female / elf male head-on. 44 of the 787 hashes are shared by two or three bodies; that is the client's own Body.def aliasing, chased to the source rather than assumed (see the docs PR).

dotnet build Scripts.csproj -c Release — 0 warnings, 0 errors.

Review notes

  • Config: AssetBodyBatch, AssetFetchKeys, AssetScanMs, AssetPlayerDirection, AssetCreatureDirection, all documented in Bridge.cfg.
  • Companion PRs: link (the three REST routes — §16 omitted this repo and was wrong), Module-uo, docs.

AI disclosure

  • This contribution was AI-assisted (Claude Code).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Phase 3 of the Asset Bridge ([`docs/link/v8.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.md) §4.8, §5, §6, §8). Two request families, on **opposite threads on purpose**. ### `assets.bodies` — the Core thread (`BridgeBodies.cs`) The atlas knows a creature by the class name in `Spawns/*.xml`; the client knows it by a body id; nothing in the ServUO tree declares the mapping. Construct the type, read `Body.BodyID`, `Delete()`. That is world mutation, so it answers on the **Core thread** — and it is the one family here that deliberately does *not* take the asset worker's single slot, because the work is not on the worker and the reply is a couple of kilobytes. The batch is capped at 100 names and an over-long request is **refused, never truncated**: a silently shortened answer is indistinguishable from a complete one from the website's side, and the names that fell off the end would be recorded as "asked and unanswerable" rather than "never asked". Every outcome is a **row**, not a failed request — `ok` / `unknown` / `notCreature` / `failed`. A shard is expected to be asked about types it does not have, and a pass that failed over one would teach an operator to stop pressing the button. ### `assets.manifest` / `assets.fetch` — the worker (`BridgeCatalog.cs`) The manifest carries `{ key, sha256, bytes, width, height, body, direction }` 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 a second time moments later. Three shapes worth flagging for review: - **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, not minted per build.** The cache is released when idle, and a fresh id per build would force a restart halfway through a slow import although nothing about the client had moved. - **ARGB1555 is expanded to 32bpp here rather than handed to GDI+.** What GDI+ does with a one-bit alpha channel varies by platform (Mono is a different implementation entirely), 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. That is what keeps the 357 bodies whose index entry reads `length 0` — and for which the decoder hands back the *previously decoded* creature's bitmap — out of the catalogue. ### Walked on a live shard (43k mobiles, 210k items, real sidecar) | Check | Result | |---|---| | Catalogue size | **787**, in one page, **734 ms** cold | | `body/320/a0`, `body/607/a0`, `body/666/a0` | **absent** — not another creature's picture | | Direction split across 787 | **783 at index 1, 4 at index 0** — four player bodies, not six | | All 455 stock creature classes | resolved at **~190 ms per 100** on the Core thread | | Mobiles after 455 constructions | 42,924 vs 43,000 at boot — **nothing leaked**, items fell too | | Refusals | unknown `family` → 400, stale `catalog` → 422, 101 types → 400 naming the cap | The pictures were **rendered and looked at**, which is the only method that catches this failure mode — a dragon, a giant spider, a wolf at three-quarter, and human male / human female / elf male head-on. 44 of the 787 hashes are shared by two or three bodies; that is the client's own `Body.def` aliasing, chased to the source rather than assumed (see the docs PR). `dotnet build Scripts.csproj -c Release` — 0 warnings, 0 errors. ### Review notes - Config: `AssetBodyBatch`, `AssetFetchKeys`, `AssetScanMs`, `AssetPlayerDirection`, `AssetCreatureDirection`, all documented in `Bridge.cfg`. - Companion PRs: `link` (the three REST routes — §16 omitted this repo and was wrong), `Module-uo`, `docs`. ## AI disclosure - [x] This contribution was AI-assisted (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 23:42:05 +00:00
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
whitlocktech merged commit c71712c734 into edge 2026-09-10 23:57:34 +00:00
whitlocktech deleted branch feat/asset-bridge-p3 2026-09-10 23:57:35 +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#30
No description provided.