Both places this site already knew an item's (ItemID, hue) and could only print it as text now show the picture, hued the way the client would draw it. The shard does the hueing: whether a hue repaints every pixel or only the grey ones is a flag in `tiledata.mul`, which a browser has no way to read. **Ingest warms; the route only serves** (org lead, 2026-09-11). A page never waits on the shard and never causes a fetch -- it renders what is stored and leaves out what is not, which is the state every install was in before this phase. Fetching happens behind that, on a timer, from the keys the site's own rows name. The alternative, fetching on first request, was rejected on one number: the shard's asset plane serves ONE request at a time, so a URL that fetched would let any visitor walk 49,152 ids times 3,000 hues through that slot and park an operator's own import behind it. The wanted set is DERIVED (`SELECT DISTINCT item_id, hue`) rather than queued, so it is self-healing: a restart loses nothing, and a key stops being wanted the moment the vendor row naming it is deleted. The in-memory hint set on top is only for the character sheet, which is fetched live from the shard and stored nowhere -- nothing on disk would ever name those keys. Staleness without a manifest (§7): every row records the shard's `catalog` id, a hash of the files that decide its bytes. A client patch changes it and a restart does not, so "is this out of date?" is a per-row question -- and pictures nobody looks at any more are simply never re-fetched, which is why this is lazy rather than a sweep. `shard_asset_meta` is deliberately NOT written here: it is the body catalogue's singleton, and a warm pass touching it would tell the body import that a client it never looked at is unchanged. A key the shard has no art for writes no row at all. An empty row would make the key held and it would never be asked again -- including after the operator patches in the graphic that was missing. `assets.sources` now reports which families an overlay serves, so an overlay older than phase 5 is one reported state with a sentence naming the fix, instead of a refusal per pass forever with no picture ever appearing. 688 server tests pass (14 new); client builds; the frozen manifest regenerates with one added route, all documented, no core URL moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
48 lines
2.1 KiB
JavaScript
48 lines
2.1 KiB
JavaScript
// One item's picture, when this site holds one (docs/link/v8.md §5, §11 — phase 5).
|
||
//
|
||
// `art` is a FILENAME under uploads/items/, never a path or a URL — the same
|
||
// shape `CreaturePortrait` takes, so there is one place in this module that knows
|
||
// where uploads are mounted rather than one per surface.
|
||
//
|
||
// **NULL is ordinary and permanent for some items, and this renders nothing for
|
||
// it.** Three separate reasons an item has no picture, and none of them is a
|
||
// fault: the site has no shard link and never fetched one; the warm pass has not
|
||
// reached this key yet (pictures are fetched behind the page, never by it, so a
|
||
// new listing shows text first and gains its icon a few minutes later); or the
|
||
// operator's own client simply has no art at that id — 9,963 of a stock client's
|
||
// static ids have an empty index entry. Every layout using this is written to sit
|
||
// correctly with the icon absent, because that is the state all of them were
|
||
// built in.
|
||
//
|
||
// A hued item is a DIFFERENT picture, not a tinted one: the shard applies the hue
|
||
// out of `hues.mul` before it sends anything, because whether a hue repaints the
|
||
// whole sprite or only its grey pixels is decided by a flag in `tiledata.mul`
|
||
// that this browser has no way to read. So there is nothing to style here — the
|
||
// bytes already are the right colour.
|
||
//
|
||
// `imageRendering: 'pixelated'` for the same reason the creature portraits use
|
||
// it: UO art is pixel art, and a browser's default smoothing turns a 22×26
|
||
// item into a smear at any size above its own.
|
||
export default function ItemIcon({ art, name, size = 32 }) {
|
||
if (!art) return null
|
||
|
||
return (
|
||
<img
|
||
src={`/uploads/items/${encodeURIComponent(art)}`}
|
||
alt=""
|
||
// Decorative: the item's name is already beside it as text, and an alt
|
||
// repeating it would make a screen reader say it twice.
|
||
aria-hidden="true"
|
||
loading="lazy"
|
||
style={{
|
||
width: size,
|
||
height: size,
|
||
flex: 'none',
|
||
objectFit: 'contain',
|
||
imageRendering: 'pixelated',
|
||
}}
|
||
title={name}
|
||
/>
|
||
)
|
||
}
|