From b84a73d0e75b335c4f54fa5f2f60f7ab805b2d86 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 14 Sep 2026 01:10:01 -0500 Subject: [PATCH] docs(link): the phase the measurement cancelled, and the 452 wrong pictures it found on the way (Phase 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §16 planned deep animation keys and a bulk-fill switch. Measuring first changed the phase: a complete one-direction animation set is 174,453 frames / 281.5 MB (not the ~119,000 / ~117 MB §11 estimated), every frame carries its own centre and 23,818 of 26,274 actions have frames of differing size — and the site displays still pictures. So none of it was built (§11.2, org lead 2026-09-11); `body//a/f` stays named in §5 and refused in practice. §4.10 is what the walk found instead. A body's actions are a contiguous band and the next body's begin immediately after, so one action past the ceiling is a real record of another creature: 643 of 795 legacy bodies pass every check there and 452 are byte-identical to body+1's action 0. `Animations.GetAnimLength` is not the ceiling — it disagrees with the index arithmetic on one body of this client, by nine actions. What shipped: the 73 bodies with no art at action 0 (a horse at `body/820/a23`), the catalogue key carrying its action, the atlas join that reads it instead of hardcoding `a0`, and the ceiling that makes the fallback walk safe. Catalogue 1,022 -> 1,095; EXTRACTOR_VERSION 2 -> 3; protocol stays 8. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 --- link/v8.md | 214 ++++++++++++++++++++++++++++++++++++----- modules/uo/SCHEMA.md | 2 +- website/SPAWN_ATLAS.md | 8 +- 3 files changed, 195 insertions(+), 29 deletions(-) diff --git a/link/v8.md b/link/v8.md index 6c27db6..193464b 100644 --- a/link/v8.md +++ b/link/v8.md @@ -635,6 +635,45 @@ needed its own lookup, because `Ultima.Files`' table of known client files preda and answers null for every `AnimationFrame*.uop` however present they are. The replacement matches **case-insensitively by enumeration**, which is a Linux-host concern rather than a tidiness one. +### 4.10 What phase 6 measured: the ceiling, and 452 validated pictures of the wrong body + +§4.8 found the library returning the previously-decoded body's bitmap for an index entry that +reads `length 0`. Phase 6 went looking one axis over — along **actions** rather than bodies — and +found the same class of failure with none of the tells. + +An index entry for an animation is `bodyBase + action * 5 + direction`, and `bodyBase` comes from a +**band**: 110 slots for a high-detail body (22 actions), 65 for a low-detail one (13), 175 for a +people body (35). The bands are contiguous, so the slots immediately after a body's band are the +**next body's**. Ask for one action past the ceiling and the arithmetic lands on a real entry, at a +real offset, holding a real animation record — of a different creature. + +Measured on this machine's stock client, over the 795 bodies the legacy path serves: + +| Asking one action past the band | Bodies | +|---|---| +| Refused by §4.5's validator (the entry is absent or unreadable) | 152 | +| **Passes `CheckEntry` and `AnimationSane` and decodes** | **643** | +| ...and the picture is **byte-identical to body+1's action 0** | **452** | + +Body 1 action 22 is an ettin. Body 3 action 22 is an imp. Both confirmed by rendering them beside +the body they belong to, because a count would have said the walk was fine — phase 0's validator +cannot catch this and is not wrong to miss it: there is nothing defective about the record. The +only defence is to **refuse the address**, so the ceiling lives inside +`BridgeAssetValidator.ResolveAnimation`, where every caller already goes and no caller can skip it. + +**The ceiling is the banding, not the library's own `GetAnimLength`.** That function exists, looks +authoritative and disagrees with the index arithmetic on exactly one body of this client: a body +reaching file type 5 as id 34 is excluded from the first band by `Animations.GetFileIndex`'s own +"looks strange, though it works" special case, so it owns **13** actions while `GetAnimLength` +answers **22**. Taking the larger number is nine actions of somebody else's art, reached on this +client by translation from body 276. So the count is derived from the same switch that produces the +offset, in the same file, where the two cannot drift apart. + +This is the §4.3 never-sweep rule again in a third disguise. Sweeping file types puts a spider on +the gargoyle page; trusting a `length 0` entry puts the last creature decoded on this one; walking +past a band puts the *next* creature on it. All three decode cleanly, all three report success, and +all three are caught by refusing to ask rather than by checking the answer. + --- ## 5. Addressing: one key for every asset @@ -647,11 +686,22 @@ static/3922 one item graphic static/3922/h33 the same graphic, hue 33 applied land/3 one land tile body/34/a0 creature body 34, action 0, first frame -body/400/a0/f0..f9 human male, action 0, all ten frames +body/820/a23 body 820, whose action 0 is empty — a horse (§11.2) +body/400/a0/f0..f9 human male, action 0, all ten frames — NOT SERVED (§11.2) cliloc/enu the whole converted string table (not an image) tree/Spawns/Trammel.xml a ServUO tree file (§10) ``` +**The catalogue's key names whichever action its picture came from.** One row per body either way, +and for all but 73 of this client's bodies that action is 0 — but a body with no art there is +catalogued at the first action that has any, and the key says which (§11.2). Calling it `a0` +regardless would have been fewer changes downstream and a key that lies about its content, which +is the failure §4.5, §4.8 and §4.10 each describe from a different direction. + +**The frame depth is defined and not served.** `body/400/a0/f0..f9` is well-formed under this +scheme and every request for it is refused `unsupported` — see §11.2 for the decision and what it +would cost to change. + Three properties this shape buys: - **Hue is part of the key, not a transform.** `itemId` and `hue` are already on the wire together @@ -659,8 +709,10 @@ Three properties this shape buys: key for its own picture. Applying hues website-side would mean shipping `Hues.mul` semantics into Node for no gain. - **Depth is expressible without being mandatory.** `body/400/a0` and `body/400/a0/f0..f9` are the - same addressing scheme at two depths, which is what lets §11 bulk-import thumbnails and fetch full - animations on demand without a second protocol. + same addressing scheme at two depths, which is what lets §11 bulk-import thumbnails and, if + anything ever wants them, fetch full animations on demand without a second protocol. Phase 6 + measured what that second depth costs and deferred it (§11.2); the point of the scheme is that + deferring it is a decision about *what to serve*, not about what can be named. - **Nothing in the key is client-version-specific**, so a client patch changes an asset's *bytes*, not its name — which is what makes §7's delta work. @@ -1019,9 +1071,9 @@ project. §1.1's measurements make the sizing question concrete: |---|---|---| | Item statics | **49,152** addressable, **39,189** with art (§11.1, phase 5) | No — on demand, cached, keyed by `itemId` (+ hue) | | Land tiles | **16,384** addressable, **4,244** with art | No — on demand | -| Creature/player bodies, first frame | **1,022** — 787 legacy (§4.8, *not* the 1,144 the library reports) + 235 UOP (§4.9) | **Yes** — this is the catalogue | -| One body, every action, one direction | **210 frames** (body 400); 96–210 measured across six bodies | No — on demand, per body | -| All bodies, every action, one direction | **~119,000 frames**, ~117 MB | No — but no longer unthinkable | +| Creature/player bodies, first frame | **1,095** — 787 legacy (§4.8, *not* the 1,144 the library reports) + 235 UOP (§4.9) + 73 at a later action (§11.2) | **Yes** — this is the catalogue | +| One body, every action, one direction | **210 frames** (body 400); median 118 KB per body, max 9.6 MB (§11.2) | **Not served** (§11.2) | +| All bodies, every action, one direction | **174,453 frames, 281.5 MB** — measured, not the ~119,000 estimated here | **Not served** (§11.2) | | The same at five directions | ~865,000 frames | Not built (§5.1) | | Cliloc table | 123,490 entries → 67,496 rows | **Yes** — whole-table replace | | ServUO tree files (§10) | ~21 files, ~10.6 MB | **Yes** | @@ -1040,23 +1092,26 @@ its opposite: the 235 it added are bodies with real art in a file the vendored d open (§4.9), each one validated at every bound and identified by a name that carries its body id. Both numbers moved because something was measured rather than reported. -Everything deeper is the *same protocol at a deeper key* (§5), fetched on demand and cached. That -is what serves the future project without exporting 3.5 GB of someone else's copyrighted client -into a database: a viewer that wants body 400's full walk cycle asks for `body/400/a2/f0..f9` and -gets it, once, and it is cached from then on. +Everything deeper is the *same protocol at a deeper key* (§5) — a viewer that wants body 400's full +walk cycle would ask for `body/400/a2/f0..f9`. **That depth is not implemented** (§11.2): the site +displays still pictures, so the frames have no consumer, and the design's value here is that a +future one costs a reader and a store rather than a protocol. Because §5.1 dropped four of the five directions, a **complete** one-direction animation set for -every body is now ~119,000 frames rather than ~865,000 — around 117 MB (~173,000 before §4.8 cut -the body count to the ones that have art; both figures carry the same measured ~151-frame average -per body). That is still not the -default and still not something to import before anything asks for it, but it has moved from -"never" to "a thing an operator could reasonably choose", and phase 6 should leave room for a -bulk-fill-everything switch rather than assuming on-demand is the only mode. +every body looked like ~119,000 frames rather than ~865,000. Phase 6 measured it at **174,453 +frames and 281.5 MB** (§11.2) and, more to the point, established that nothing on this site would +ever read them: the site shows still pictures. So neither the deep keys nor the +bulk-fill-everything switch this paragraph anticipated was built, and the numbers above are what a +future consumer would be choosing to pay. **Hued variants are on demand, always.** `static/3922/h33` is generated when something on the wire actually carries hue 33. The cross product of 49,152 statics and 3,000 hues is not a set anyone enumerates. +**Everything deeper than the catalogue is the same protocol at a deeper key, and is refused rather +than served** — §11.2. That sentence was written expecting phase 6 to serve it; what phase 6 +actually found was that no consumer exists, so the depth stays named and unserved. + ### 11.1 What phase 5 measured, and the two traps it found The sizing above was an estimate taken off `art.mul`'s length. Measured through the reader itself, @@ -1085,7 +1140,7 @@ than throwing, so the over-run counted silently as "empty slots".) **81 MB is small enough to reopen the bulk question, and the answer is still no.** Not on size — on what the transfer buys. Base64 puts it at 108 MB through a 512 KB single-slot channel, roughly 210 round trips, to store 43,433 pictures of which a live shard displays a few hundred. On-demand stays -right; phase 6's bulk-fill switch is where an operator who wants the lot says so. +right, and phase 6 did not build the bulk-fill switch this sentence promised — see §11.2. #### The library's cache poisons a hued sprite @@ -1147,6 +1202,65 @@ call** — §4.1. It is the only decoder that reaches the `hasExtra: true` branc corrupts the process on the second id. Adding gump art later means fixing that path first, deliberately; it is additive under the same key scheme (`gump/`), which is the point of §5. +### 11.2 What phase 6 measured, and why the deep keys are not built + +Everything above about full animations was an estimate — ~119,000 frames, ~117 MB, from a +151-frame average over six bodies. Phase 6 decoded and PNG-encoded **every action of every body at +the catalogue's direction**, which is the set a complete one-direction animation store would hold: + +| | Estimated (§11) | Measured | +|---|---|---| +| Bodies with art at *some* action | 1,022 | **1,096** | +| Action keys (`body//a`) with frames | — | **26,274** (mean 25.4 per body, max 35) | +| Frames | ~119,000 | **174,453** | +| As PNG | ~117 MB | **281.5 MB** (mean 1,692 B, largest single frame 71.5 KB) | +| Per body | — | median **118 KB** / 210 frames; p90 536 KB; max **9.6 MB** (body 826) | +| Decode + encode the lot | — | **104 s** | + +Two facts fall out of that walk that any frame-serving design has to answer. + +**Frames are not a sequence of pictures; they are a sequence of *placed* pictures.** Every frame +carries its own centre offset and its own dimensions, and **23,818 of the 26,274 actions have +frames that differ in size** (centres span x −51..270, y −217..184). A key that returned only a PNG +would produce an animation that jitters, and nothing downstream could tell — the same shape of +silent wrongness as every other trap in this document. + +**At 281.5 MB the transfer is ~375 MB of base64 through a 512 KiB single-slot channel** (§3.2), +which is upward of 750 round trips for a store of 174,453 files. + +### The decision: the site shows still pictures, so frames wait for a consumer (org lead, 2026-09-11) + +§11 justified the deep keys with "the future project", not with anything on this site — and the +site does not display animation anywhere, nor is it planned to. So phase 6 builds **none** of it: +no frame keys, no per-body frames manifest, no bulk-fill switch, no store sized for 281 MB, and no +admin surface for any of that. What would have been the consumer-side machinery is exactly the part +that would have been guessed at, and when a consumer does exist it can say what shape it wants. + +`body//a/f` stays defined in §5 and refused in practice. Reviving it costs the frames +manifest, the alignment fields and a store — not a protocol change, which is the whole point of +§5's key scheme. + +### What phase 6 built instead: the 73 bodies nobody could see + +The measurement did turn up something the still-picture site wants. **73 bodies have no art at +action 0 and real art at a later one** — 66 reached through the UOP packages, 7 through the legacy +files. Body 820's first drawn action is 23, and it is a horse. Until this phase they were absent +from the catalogue and rendered as text on the bestiary. + +So the catalogue now falls back to **the first action that has art**, and the key names it +(§5). The count goes **1,022 → 1,095**, a full cold scan of all 2,047 bodies goes 1,409 ms → 2,090 ms, +and `EXTRACTOR_VERSION` goes **2 → 3** — unchanged input, a different answer, which is what that +number is for. + +Two honesties about those 73. The seven legacy ones and roughly a dozen of the UOP ones are +recognisable creatures — a horse, a spider, a dragon, a phoenix, a turkey. **Most of the rest are +thin flat sprites** with entries at actions 1 and 3 only, all anchored at centre x = 63: object or +equipment art rather than creatures. They are correct by construction (a UOP entry is addressed by +the hash of a name carrying body *and* action, and the payload re-declares the body), they cost +about 200 bytes of row each, and nothing surfaces them unless a spawn file names a creature class +that resolves to one of those ids. And the fallback is the one walk in this protocol that moves +along the action axis, which is why §4.10's ceiling had to ship with it rather than after it. + --- ## 12. Where it lands on the website @@ -1201,13 +1315,29 @@ past (org lead, 2026-09-10): is actually enforced — one spread, in one place, applied on every rebuild rather than only at import time. -**Two details worth not rediscovering.** The derivation joins on the catalogue key -(`a.asset_key = CONCAT('body/', b.body, '/a0')`), not on `a.body = b.body`: today one body has -exactly one asset and the simpler join is correct, and it 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 -returned last becomes the portrait. And the stored filename is **content-addressed** -(`uo-body-34-a0-.png`), because a stable name overwritten in place leaves every browser and -CDN serving last month's client's sprite from cache with the database row perfectly correct. +**Two details worth not rediscovering.** The derivation joins on the catalogue **key**, not on +`a.body = b.body`: today one body has exactly one asset and the simpler join is correct, and it +stops being correct the day a deeper key (`body/400/a2/f0`) is stored, at which point one slug +matches dozens of rows and whichever the engine returned last becomes the portrait. And the stored +filename is **content-addressed** (`uo-body-34-a0-.png`), because a stable name overwritten +in place leaves every browser and CDN serving last month's client's sprite from cache with the +database row perfectly correct. + +Phase 6 changed that join, and the way it changed is the point. It read +`a.asset_key = CONCAT('body/', b.body, '/a0')`, which stopped being right the moment a body could +be catalogued at another action — it would have dropped exactly the 73 creatures §11.2 added, a +horse among them, silently. It now reads the row's own action: + +```sql +JOIN shard_assets a ON a.body = b.body AND a.family = 'body' + AND a.asset_key = CONCAT('body/', b.body, '/a', COALESCE(a.action, 0)) +``` + +`COALESCE` because a row written before the column existed has NULL there, and a NULL inside +`CONCAT` makes the whole comparison NULL — which would have taken every portrait off the site on +upgrade, with the database perfectly correct and nothing to see in a log. The `action` is stored +rather than parsed back out of the key because this join needs it in SQL, and re-deriving it there +would put a second, weaker parser of §5's key scheme in the schema. `shard_creature_bodies` also answers §8 without a schema change on the atlas side: `shard_spawn_creatures.name` already holds the ServUO **class name** — the atlas build picks the @@ -1279,6 +1409,14 @@ discovers the gap as a refusal *per key, per pass, forever*, with no picture eve warning in the log every few minutes. With it, that is one reported state carrying a sentence naming the fix. +**Phase 6 added one field and no command.** Manifest and fetch rows carry `action` — which action +of the body the thumbnail came from (§11.2). Additive, so **the protocol stays 8**; a consumer that +ignores it sees the catalogue it always saw plus 73 rows, and one that reads it can build the right +URL for a body catalogued at `a23`. `EXTRACTOR_VERSION` goes **2 → 3**, which is the change every +consumer does see. A fetch for a key naming an action the catalogue did not choose is answered +`unsupported` with the chosen action alongside it — never by decoding the asked-for action, which +is §4.10's wrong picture reached politely. + **Phase 4 added one field and no command.** `source` on a manifest or fetch row is `legacy` or `uop` — which reader produced the bytes (§4.9). It is additive, so **the protocol stays 8**: a consumer that does not read it is unaffected, and one that does can say which half of the extractor @@ -1326,7 +1464,7 @@ disagree, so a split bump means the next bundle silently fails to compose. | 3 | **DONE 2026-09-10.** Body resolution (§8) + the **787**-body catalogue (§4.8), `assets.manifest` / `assets.fetch` / `assets.bodies` and their REST mirrors, `shard_spawn_creatures.art` filled and rendered (§8.1, §12.1). **787 rows in one 734 ms page; 455 types resolved at ~190 ms per 100 on the Core thread; zero mobiles leaked.** `UOFIDDLER.md` deleted, two phases early | servuo-plugins, **link**, module-uo | | 4 | **DONE 2026-09-11.** The UOP animation decoder (§4.3, §4.9): `BridgeUop` + a PNG encoder that never touches `System.Drawing`, wired in beneath the legacy reader. **Two of the eight player bodies turned out to exist** (gargoyles 666/667); the other six are in no client file, and ghost ids left the player-body set (§5.2, §17.9). The same fallback added **233 other bodies**: the catalogue is **1,022 rows, 1,409 ms cold**, and all six player bodies have art for the first time. `EXTRACTOR_VERSION` 1 → 2 | servuo-plugins | | 5 | **DONE 2026-09-11.** Item statics and land on demand (§11.1): the `static` and `land` families, hue applied on the shard from `tiledata.mul`, the byte-bounded art cache, `assets.fetch` made family-aware, `families` on `assets.sources`. Website side: the warm pass, per-row `catalog` staleness, and pictures on the marketplace and the character sheet. **39,189 statics and 4,244 land tiles served; the only refusals are the 9,963 + 12,140 empty index slots §4.5 predicted.** Two traps found — the library's bitmap cache poisons a hued sprite, and `PartialHue` decides the picture from a file only the shard has. Protocol stays 8; `EXTRACTOR_VERSION` stays 2 | servuo-plugins, module-uo | -| 6 | Deep animation keys (`body//a/f`) for the future project, plus the bulk-fill switch | servuo-plugins, module-uo | +| 6 | **DONE 2026-09-11, and not what this row said.** The measurement came first and changed the phase: a complete one-direction animation set is **174,453 frames / 281.5 MB**, not the ~119,000 estimated, and **the site displays still pictures** — so the deep keys and the bulk-fill switch were **not built** (§11.2, org lead 2026-09-11). What shipped is what the still-picture site was missing: the **73 bodies with no art at action 0 and real art deeper** (a horse at `body/820/a23`), the catalogue key carrying its action, the atlas join that reads it, and §4.10's per-body **action ceiling** — without which the fallback walk itself would serve **452 validated pictures of the next body**. Catalogue **1,022 → 1,095**; `EXTRACTOR_VERSION` 2 → 3; protocol stays 8 | servuo-plugins, module-uo | | 7 | The atlas over the sidecar (§10); shared-filesystem requirement retired | module-uo | | 8 | Admin surface, Import/Update, approve/reject, activity log | module-uo | | 9 | Docs pass across five repos; live walk on the real rig | docs | @@ -1337,6 +1475,11 @@ a client that has been patched — *before* building eight phases on top of it. §1.1 were run from PowerShell against a stock client; neither of those is the environment this will actually run in. +**Phase 6's deep keys are not deferred to a later phase; they are out of the plan** until something +wants them. §11.2 has the measurement a future consumer would be choosing to pay and the two facts +it would have to answer (per-frame centres, and 750 round trips through a single-slot channel). +Nothing about reviving them needs a protocol change — that is what §5's key scheme bought. + Phase 4 sat **after** the catalogue rather than inside it on purpose. The catalogue was useful with 783 of its 787 bodies, the UOP reader is the one piece of genuinely new format work in this protocol, and putting it on the critical path would have held up every website-side phase behind @@ -1453,3 +1596,24 @@ in the document. from a complete one. The door §4.4 wanted open stays open; phase 4 simply does not walk through it. +11. **§11.2: phase 6 stopped being the phase it was planned as — settled 2026-09-11.** Put to the + org lead with the measurement in hand, and the answer removed most of the phase: + + - **The deep animation keys and the bulk-fill switch are not built.** The org lead's own scope + note — "I hadn't planned to use the animations on the site, just static images" — is the + whole argument: §11 justified them with a future project rather than with anything on this + site, and 281.5 MB of store, a warm pass and an admin surface for a consumer that does not + exist yet would all have been guesses. `body//a/f` stays named in §5 and refused in + practice; reviving it is a reader and a store, not a protocol change. + - **The catalogue falls back to the first action that has art, and the key names that action.** + 73 bodies on a stock client have nothing at action 0 and real art deeper — body 820's action + 23 is a horse — and they rendered as text on the bestiary. Keeping the `a0` spelling for + them was rejected for the reason §5 gives: a key that lies about its content is this + protocol's recurring failure, not a shortcut. + - **Only that one exception**, over a per-body pose override that would let an operator pick a + better-looking action. One rule with one mechanical exception; the setting can come the day + somebody wants a specific creature to look different. + - **§4.10's action ceiling ships in the same phase**, because the fallback is the walk that + would otherwise hit it: one action past a body's band is 643 validated pictures and 452 + byte-identical copies of the next body. + diff --git a/modules/uo/SCHEMA.md b/modules/uo/SCHEMA.md index 198a244..2cd7d37 100644 --- a/modules/uo/SCHEMA.md +++ b/modules/uo/SCHEMA.md @@ -202,7 +202,7 @@ Creature artwork read from the shard's own UO client ([`link/v8.md`](../../link/ | Table | Shape | |---|---| -| `shard_assets` | `asset_key` VARCHAR PK (§5's key, e.g. `body/34/a0`, `static/3922/h33`, `land/3`), `family`, `sha256`, `bytes`, `width`, `height`, `body`, `direction`, `file`, `catalog`, `imported_at` | +| `shard_assets` | `asset_key` VARCHAR PK (§5's key, e.g. `body/34/a0`, `body/820/a23`, `static/3922/h33`, `land/3`), `family`, `sha256`, `bytes`, `width`, `height`, `body`, `action`, `direction`, `file`, `catalog`, `imported_at` | | `shard_creature_bodies` | `slug` PK, `type_name` (the ServUO class name asked), `body` nullable, `status`, `resolved_at` | | `shard_asset_meta` | Singleton (`id = 1`), `payload` JSON (catalogue id, extractor version, source fingerprint, counts), `imported_at` | diff --git a/website/SPAWN_ATLAS.md b/website/SPAWN_ATLAS.md index 91babc3..e7e02ef 100644 --- a/website/SPAWN_ATLAS.md +++ b/website/SPAWN_ATLAS.md @@ -233,10 +233,12 @@ 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. -On this machine's stock client that is **1,022 creature portraits**, about a -megabyte in total — 787 out of the legacy `anim*.mul` files and 235 more out of +On this machine's stock client that is **1,095 creature portraits**, about a +megabyte in total — 787 out of the legacy `anim*.mul` files, 235 more out of `AnimationFrame*.uop`, which ServUO's own decoder never opens -([`../link/v8.md`](../link/v8.md) §4.9). +([`../link/v8.md`](../link/v8.md) §4.9), and 73 more that have no art at the +walk's first action and real art at a later one, which the import now falls back +to (§11.2). Body 820 is one of them, and it is a horse. **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