feat(assets): creature artwork from the shard's own client (Phase 3) #37

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

Until now the only way a creature got a picture on this site was for an operator to open UOFiddler on a desktop, export sprites by hand, copy them to the web host and write a spawnAtlas.art.json naming each one. Almost nobody did, so shard_spawn_creatures.art was NULL on every install and the atlas rendered as text.

Admin → Shard → Import now walks the shard's asset manifest, fetches only the sprites whose hash changed, writes them under uploads/atlas/, asks the shard for a body id per atlas creature and points each creature at its picture. On a stock client that is 787 portraits, about a megabyte.


The one thing v8.md §12 got wrong, and it is not cosmetic

§12 says shard_spawn_creatures.art "starts being filled by the import". That table is emptied and refilled by replaceAtlas on every atlas refresh, and a refresh runs on every boot. A filename stored there would be destroyed by an ordinary re-parse of the ServUO tree — and the next asset Update would find the client files unchanged, report nothing to do, and never restore it. Nothing would report a fault; the pictures would just be gone.

So the assets and the body map live in their own tables outside that blast radius, and applyAtlas re-derives art on the way past as { ...derived, ...operatorMap } — which is also the single place "the operator's own artwork wins" is enforced, on every rebuild rather than only at import time.

Approved by the org lead before any code was written, along with PNG-on-the-shard, the atlas-only body pass, and shipping the minimal admin pair now rather than waiting for phase 8.

Smaller decisions worth a reviewer's eye

  • The derivation joins on the catalogue key, a.asset_key = CONCAT('body/', b.body, '/a0'), not a.body = b.body. The simpler join is correct today and stops being correct the moment phase 6 adds body/400/a2/f0, at which point one slug matches dozens of rows and whichever the engine returns last becomes the portrait.
  • Filenames are content-addressed (uo-body-34-a0-<sha8>.png). A stable name overwritten in place leaves every browser and CDN serving the previous client's sprite, with the database row perfectly correct.
  • An unchanged key whose file is missing is fetched again. The row and the disk can disagree — a wiped uploads volume, a restore from a dump — and a broken image on a creature page is worse than one re-fetched sprite.
  • A key the shard cannot render is not a failure. Two thirds of the playable ghost and gargoyle bodies have no art on a stock client; an import reporting eight failures every time would teach an operator to ignore the panel. It is counted (absent) and kept apart from unsupported, which is a bug on our side.
  • A key that vanished from the manifest needs review before anything changes. An unmounted client volume and a deliberate downgrade look identical from here, and the wrong guess deletes artwork.
  • shard_creature_bodies keeps the negative answers. Without them the next pass asks again, and each name costs a real constructor on the shard's Core thread. unknown is also real drift an operator wants to see: the spawn files naming a type this shard's scripts do not define.

Also here: the portrait actually renders

art has been on the API response since the atlas shipped and nothing consumed it, so filling the column would have been unobservable. CreaturePortrait renders it on the bestiary list and the creature page, imageRendering: pixelated, and returns null when there is no picture — which stays the ordinary case, so every layout around it collapses to exactly what it was before.

Testing

  • 23 new tests (674 server, 42 client, all passing) covering the envelope failures, the diff, the vanished-key path, absence-is-not-failure, and operator-map precedence.
  • The SQL was run against a real MariaDB, using core's own splitStatements — which is what proved the CONCAT join ignores a deeper key on the same body, that a file IS NULL row produces no art, and that the singleton CHECK on shard_asset_meta actually refuses a second row.
  • routes.manifest.json regenerated against the pinned core: +2 routes, no core route moved. Swagger fragment current.

The phase-2 SCHEMA.md debt — taken, in the docs PR

I originally flagged docs/modules/uo/SCHEMA.md's "the conversion step is not avoidable" as out of scope. On the org lead's instruction it is now fixed in docs#238's second commit, where it turned out three sentences carried the same falsehood rather than one.

Companion PRs: servuo-plugins, link, docs.

AI disclosure

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

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Until now the only way a creature got a picture on this site was for an operator to open UOFiddler on a desktop, export sprites by hand, copy them to the web host and write a `spawnAtlas.art.json` naming each one. Almost nobody did, so `shard_spawn_creatures.art` was NULL on every install and the atlas rendered as text. **Admin → Shard → Import** now walks the shard's asset manifest, fetches only the sprites whose hash changed, writes them under `uploads/atlas/`, asks the shard for a body id per atlas creature and points each creature at its picture. On a stock client that is **787 portraits, about a megabyte**. --- ### The one thing `v8.md` §12 got wrong, and it is not cosmetic §12 says `shard_spawn_creatures.art` "starts being filled by the import". That table is **emptied and refilled by `replaceAtlas` on every atlas refresh**, and a refresh runs on every boot. A filename stored there would be destroyed by an ordinary re-parse of the ServUO tree — and the next asset Update would find the client files unchanged, report *nothing to do*, and never restore it. Nothing would report a fault; the pictures would just be gone. So the assets and the body map live in **their own tables outside that blast radius**, and `applyAtlas` re-derives `art` on the way past as `{ ...derived, ...operatorMap }` — which is also the single place "the operator's own artwork wins" is enforced, on every rebuild rather than only at import time. Approved by the org lead before any code was written, along with PNG-on-the-shard, the atlas-only body pass, and shipping the minimal admin pair now rather than waiting for phase 8. ### Smaller decisions worth a reviewer's eye - **The derivation joins on the catalogue key**, `a.asset_key = CONCAT('body/', b.body, '/a0')`, not `a.body = b.body`. The simpler join is correct *today* and stops being correct the moment phase 6 adds `body/400/a2/f0`, at which point one slug matches dozens of rows and whichever the engine returns last becomes the portrait. - **Filenames are content-addressed** (`uo-body-34-a0-<sha8>.png`). A stable name overwritten in place leaves every browser and CDN serving the previous client's sprite, with the database row perfectly correct. - **An unchanged key whose *file* is missing is fetched again.** The row and the disk can disagree — a wiped uploads volume, a restore from a dump — and a broken image on a creature page is worse than one re-fetched sprite. - **A key the shard cannot render is not a failure.** Two thirds of the playable ghost and gargoyle bodies have no art on a stock client; an import reporting eight failures every time would teach an operator to ignore the panel. It is counted (`absent`) and kept apart from `unsupported`, which *is* a bug on our side. - **A key that vanished from the manifest needs review before anything changes.** An unmounted client volume and a deliberate downgrade look identical from here, and the wrong guess deletes artwork. - **`shard_creature_bodies` keeps the negative answers.** Without them the next pass asks again, and each name costs a real constructor on the shard's Core thread. `unknown` is also real drift an operator wants to see: the spawn files naming a type this shard's scripts do not define. ### Also here: the portrait actually renders `art` has been on the API response since the atlas shipped and nothing consumed it, so filling the column would have been unobservable. `CreaturePortrait` renders it on the bestiary list and the creature page, `imageRendering: pixelated`, and returns `null` when there is no picture — which stays the ordinary case, so every layout around it collapses to exactly what it was before. ### Testing - **23 new tests** (674 server, 42 client, all passing) covering the envelope failures, the diff, the vanished-key path, absence-is-not-failure, and operator-map precedence. - **The SQL was run against a real MariaDB**, using core's own `splitStatements` — which is what proved the `CONCAT` join ignores a deeper key on the same body, that a `file IS NULL` row produces no art, and that the singleton `CHECK` on `shard_asset_meta` actually refuses a second row. - `routes.manifest.json` regenerated against the pinned core: **+2 routes, no core route moved**. Swagger fragment current. ### The phase-2 SCHEMA.md debt — taken, in the docs PR I originally flagged `docs/modules/uo/SCHEMA.md`'s *"the conversion step is not avoidable"* as out of scope. On the org lead's instruction it is now fixed in **docs#238's second commit**, where it turned out three sentences carried the same falsehood rather than one. Companion PRs: `servuo-plugins`, `link`, `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:54 +00:00
feat(assets): creature artwork from the shard's own client (Phase 3)
All checks were successful
PR Checks / client-build (pull_request) Successful in 18s
PR Checks / server-tests (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 49s
a194ec68e0
Until now the only way a creature got a picture on this site was for an
operator to open UOFiddler on a desktop, export sprites by hand, copy them to
the web host and write a spawnAtlas.art.json naming each one. Almost nobody
did, so shard_spawn_creatures.art was NULL on every install.

The shard has had those files the whole time. Admin -> Shard -> Import now
walks its asset manifest, fetches only the sprites whose hash changed, writes
them under uploads/atlas/, asks the shard for a body id per atlas creature
(§8: it CONSTRUCTS the creature and reads Body.BodyID, which is the only thing
that is right for a shard's own custom creatures) and points each creature at
its picture. On a stock client that is 787 portraits, about a megabyte.

**The one thing v8.md §12 got wrong, and it is not cosmetic.** It says
`shard_spawn_creatures.art` "starts being filled by the import". That table is
emptied and refilled by replaceAtlas on EVERY atlas refresh, and a refresh runs
on every boot -- so a filename stored there would be destroyed by an ordinary
re-parse of the ServUO tree, with the next Update finding the client files
unchanged, reporting "nothing to do", and never restoring it. Nothing would
report a fault; the pictures would just be gone.

So the assets and the body map live in their own tables outside that blast
radius, and applyAtlas re-derives `art` on the way past as
`{ ...derived, ...operatorMap }` -- which is also the one place "the operator's
own artwork wins" is enforced, on every rebuild rather than only at import.

Smaller decisions worth not rediscovering:

- The derivation joins on the catalogue KEY, not on the body id. The simpler
  join is correct today and stops being correct the moment phase 6 adds
  body/400/a2/f0, at which point one slug matches dozens of rows.
- Filenames are content-addressed. A stable name overwritten in place leaves
  every browser and CDN serving the previous client's sprite, with the database
  row perfectly correct.
- An unchanged key whose FILE is missing is fetched again. The row and the disk
  can disagree (a wiped uploads volume, a restore from a dump), and a broken
  image on a creature page is worse than one re-fetched sprite.
- A key the shard cannot render is not a failure. Two thirds of the playable
  ghost and gargoyle bodies have no art on a stock client, and an import that
  reported eight failures every time would teach an operator to ignore the panel.
- A key that VANISHED from the manifest needs review before anything changes:
  an unmounted client volume and a deliberate downgrade look identical here.

23 new tests; 674 server and 42 client tests pass. The SQL was also run against
a real MariaDB, which is what proved the CONCAT join and the singleton CHECK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 6ece48f7d3 into edge 2026-09-10 23:58:13 +00:00
whitlocktech deleted branch feat/asset-bridge-p3 2026-09-10 23:58:14 +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/Module-uo#37
No description provided.