feat(assets): creature artwork from the shard's own client (Phase 3) #37
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/asset-bridge-p3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.jsonnaming each one. Almost nobody did, soshard_spawn_creatures.artwas 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 byreplaceAtlason 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
applyAtlasre-derivesarton 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
a.asset_key = CONCAT('body/', b.body, '/a0'), nota.body = b.body. The simpler join is correct today and stops being correct the moment phase 6 addsbody/400/a2/f0, at which point one slug matches dozens of rows and whichever the engine returns last becomes the portrait.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.absent) and kept apart fromunsupported, which is a bug on our side.shard_creature_bodieskeeps the negative answers. Without them the next pass asks again, and each name costs a real constructor on the shard's Core thread.unknownis 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
arthas been on the API response since the atlas shipped and nothing consumed it, so filling the column would have been unobservable.CreaturePortraitrenders it on the bestiary list and the creature page,imageRendering: pixelated, and returnsnullwhen there is no picture — which stays the ordinary case, so every layout around it collapses to exactly what it was before.Testing
splitStatements— which is what proved theCONCATjoin ignores a deeper key on the same body, that afile IS NULLrow produces no art, and that the singletonCHECKonshard_asset_metaactually refuses a second row.routes.manifest.jsonregenerated 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
🤖 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. 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