docs(link): the catalogue is real, and UOFiddler's last job is gone (Phase 3)

Phase 3 is built and walked on a live shard. What the walk measured, and the
two places the design of record needed correcting:

§8.1, new: the catalogue is 787 exactly as §4.8 predicted, and the whole scan
of bodies 1-2047 takes 734 ms cold -- so the wall-clock paging §11 designed
never fires on this client. Every §4.8/§5.2 prediction held when the bytes were
rendered and LOOKED at: 320, 607, 608 and 666 come back absent rather than as
another creature's picture, and the direction split is 783 at index 1 against 4
at index 0 -- four player bodies, not six.

44 of the 787 hashes are shared by two or three bodies, which is the exact
signature of the wrong-picture bug, so it was chased rather than assumed. It is
the client's own Body.def aliasing (83 {1}, 84 {1}, 106 {12, 59}), and the check
that settles it is at the source: Translate(ref body, ref hue) rewrites `body`
only when bit 31 is set, unlike the one-argument overload -- and ResolveAnimation
calls that same two-argument overload, so validator and decoder resolve the
identical record.

§12.1, new: **§12 is right about the outcome and wrong about the mechanism.**
`shard_spawn_creatures` is emptied and refilled by every atlas refresh, and a
refresh runs on every boot -- so an imported filename written to that row is
destroyed by an ordinary re-parse of the ServUO tree, and the next Update finds
the client files unchanged and never restores it. Three tables outside that
blast radius, and the atlas import re-derives `art` on the way past.

§14: **§16 listed phase 3 as servuo-plugins + module-uo and that was wrong.**
web.rs routes every command explicitly, so `link` is in the phase. Corrected in
both places.

UOFIDDLER.md is DELETED, two phases earlier than §9.1 predicted -- creature art
was the only thing still on it. SPAWN_ATLAS.md §Artwork is rewritten around the
import, keeping the operator's own map as the thing that wins; the module's
SCHEMA.md gains the three tables and API.md the two admin routes.

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:41:08 -05:00
parent 4c6b0c566a
commit 1a7481e9f4
7 changed files with 195 additions and 149 deletions

View File

@@ -213,28 +213,59 @@ implicitly commit, defeating the all-or-nothing guarantee. Point ids are assigne
explicitly rather than left to `AUTO_INCREMENT`, because the join rows need them
and `conn.batch()` reports no usable `insertId`.
## Artwork — operator-supplied, never shipped
## Artwork — the shard extracts it now (Protocol 8)
**This project ships no creature art and no extraction tooling, and never will.**
UO sprites live in the operator's own client `.mul`/`.uop` files. They are the
operator's, not ours to redistribute.
The atlas is fully functional as text. `shard_spawn_creatures.art` is nullable
and is NULL on every fresh import; pages render without images, which is the
normal and supported state, not a degraded one.
What changed in protocol 8 is not that rule — it is who does the extracting. The
shard already has those files (a ServUO server cannot boot without a UO client),
so as of [`v8.md`](../link/v8.md) phase 3 it decodes them itself and hands the
pictures to the website over the bridge. Nobody installs UOFiddler and nobody
copies images to a web host.
An operator who wants art — step-by-step, with the UOFiddler side spelled out, in
[`UOFIDDLER.md`](UOFIDDLER.md) §Part 2:
**Admin → Shard → Import.** The import 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 creature (§8 — the shard 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 `shard_spawn_creatures.art` at its picture.
Boot never calls the shard for this: the files change when an operator patches
their client, which is an event they know about and the site does not.
1. Extracts it from **their own** client files (UOFiddler, ClassicUO tooling, or
any art extractor).
2. Drops the images under `server/uploads/atlas/`.
3. Copies `server/db/data/spawnAtlas.art.example.json` to `spawnAtlas.art.json`
and maps creature slugs to file names.
4. Restarts, or runs `npm run atlas:import -- --force`.
On this machine's stock client that is **787 creature portraits**, about a
megabyte in total.
Both `spawnAtlas.art.json` and `server/uploads/` are gitignored, so neither the
map nor the images can be committed by accident.
**NULL stays a first-class state, and always will be.** An install with no shard
link has never imported one; a Linux shard host without `libgdiplus` cannot
render a sprite at all (a named `NO_IMAGING` status, not an error); and even
after a complete import, two thirds of the playable ghost and gargoyle bodies
have no art in the client. Pages render without images, which is normal and
supported, not degraded.
### The operator's own artwork still wins
An operator who has drawn their own portraits keeps them. The map is unchanged:
1. Drop the images under `server/uploads/atlas/`.
2. Copy `server/db/data/spawnAtlas.art.example.json` to `spawnAtlas.art.json` and
map creature slugs to file names.
3. Restart, or run the import.
`spawnAtlas.art.json` is applied **over** anything imported, per slug, so a sprite
rip never replaces a hand-drawn portrait on the next Update. Both it and
`server/uploads/` are gitignored, so neither the map nor the images can be
committed by accident.
### Why the imported art is not stored on the creature row
`shard_spawn_creatures` is emptied and refilled by every atlas refresh, and a
refresh happens on every boot. So the body ids and the imported files live in
`shard_creature_bodies` and `shard_assets`, outside that blast radius, and the
atlas import re-derives `art` from them on the way past. Storing it on the row
would mean an ordinary re-parse of the ServUO tree silently deleting every
portrait — with the next asset Update finding the client files unchanged,
reporting "nothing to do", and never putting them back.
## Code layout