pi-simocracy · codex-pet-rendering
Simocracy's org.simocracy.sim lexicon picked up a spriteKind: "codexPet" branch with a 1536×1872 WebP petSheet from OpenAI's hatch-pet skill. Pi-simocracy@0.3.0 only knew the legacy 32×32 PNG sprite path, so codex pet sims loaded with no avatar at all. This run loads Einstein (the only codex pet on the indexer today) against the patched extension and confirms the WebP atlas decodes, the idle frame crops, and the half-block ANSI render fires inline.
Avatar shows. The 192×208 idle cell from row 0 col 0 of the codex pet atlas decodes through @jsquash/webp (lazy-init wasm, 138 KB), box-downscales to 32×35, and renders as 24-bit half-block ANSI — 16 lines tall, comparable footprint to a pipoya sprite. Persona injection still works alongside it: Einstein replied “Ah, I am Albert — a humble seeker of truth, with a fondness for thought experiments and a deep mistrust of war.” The full WebP → RGBA → crop → box-downscale → ANSI pipeline is asset-agnostic and back-compatible: pipoya sims (Mr Meow) still render unchanged.
renderSpriteAnsi() tried sim.sprite (absent), then sim.image (also absent for this sim) and silently returned null.
renderSpriteAnsi() sees spriteKind === "codexPet", fetches petSheet (image/webp), decodes through @jsquash/webp, crops the idle cell, box-downscales, renders inline.One vhs tape drives the whole flow against pi-simocracy at HEAD. Webm + tape are both committed under assets/ so the recording is reproducible.
▀. Note the white hair, blue shirt, white lab coat, brown pants — every visible detail comes from the WebP atlas, not the (absent) PNG fallback.
before_agent_start, which appends buildSimPrompt(loadedSim) (constitution + style) to pi's system prompt. Reply: “Ah, I am Albert — a humble seeker of truth…”src/simocracy.tsExtended SimRecord with spriteKind, petSheet, petManifest mirroring the lexicon update. Existing pipoya fields stay optional for back-compat.
src/png-to-ansi.tsAdded boxDownscaleRgba() — alpha-aware area-weighted downscaler that handles non-integer ratios. Used to shrink 192×208 codex pet cells (and 128×128 codex pet thumbnails) to a target close to a pipoya sprite's footprint.
src/webp-to-rgba.ts newThin wrapper around @jsquash/webp/decode. Loads the wasm module lazily from disk via createRequire().resolve() (the package's browser-shaped fetch() path doesn't work under Node Undici). Returns the same {width, height, data: Buffer} shape as decodePng().
src/index.tsrenderSpriteAnsi branches on spriteKind: pipoya uses the existing 32×32 walk-frame path; codexPet fetches petSheet, decodes via decodePng or decodeWebp based on mimeType, crops row-0/col-0 (192×208), box-downscales to 32 wide. Image fallback now also box-downscales any non-pixel-art whose long edge exceeds 40 px.
tsconfig.jsonAdded WebWorker to lib so WebAssembly globals type-check (the strict ES2022 baseline doesn't include them).
package.jsonAdded @jsquash/webp@^1.5.0 as a runtime dep. Wasm-only, no native bindings, no postinstall step. ~915 KB unpacked (decoder wasm itself is 138 KB).
Loading Einstein under HEAD shows the full sprite (white hair, lab coat, blue shirt) above the loaded-pi line. The published 0.3.0 build silently dropped the avatar because neither sim.sprite (pipoya-only) nor sim.image (best-effort, missing for Einstein) had a usable PNG. The new spriteKind === "codexPet" branch reads petSheet directly, which is the canonical asset for this sim system.
Mr Meow (the legacy 32×128 PNG walk sheet) still loads through the original branch — pipoya rendering is untouched. Confirmed both via the standalone node --experimental-strip-types /tmp/test-render-pet.mjs harness and an interactive /sim mr meow session.
Einstein's before_agent_start handler appends his constitution + style to pi's system prompt every turn. Reply to “who are you?”: “Ah, I am Albert — a humble seeker of truth, with a fondness for thought experiments and a deep mistrust of war.” No regression in the unload override either.
Per-stage timing under pi's interactive event loop, captured via temporary in-process logging:
resolveHandle(): 392 msfetchAgentsForSim() (PDS listRecords): 1948 msfetchStyleForSim() (PDS listRecords): 1950 msfetchBlob(petSheet, 1.95 MB): ~14.6 sWebAssembly.compile(webp_dec.wasm) + initWebpDecode(): 5 msdecode(webp) 1536×1872: 108 mscropRgba + boxDownscaleRgba + renderRgbaToAnsi: 8 msThe 14.6 s blob fetch dominates. satyam2.climateai.org's com.atproto.sync.getBlob is slower at serving the 2 MB atlas than the existing standalone-Node path (~1 s) suggests — likely Undici behaviour under pi's render loop. Worth a follow-up if loading codex pets becomes hot.
Out of 58 org.simocracy.sim records currently exposed by simocracy-indexer-production.up.railway.app, exactly one (Einstein, at://did:plc:qc42fmqqlsmdq7jiypiiigww/org.simocracy.sim/3mfo5txyqmo2f) has spriteKind: "codexPet". The remaining 57 are pipoya. Einstein also has no image PNG thumbnail, so it's the worst-case test — if the WebP path didn't work, there was nothing else to fall back on. That's exactly the failure mode shown in the “before” screenshot.
vhs assets/01-load-einstein.tape from the report root — produces both webm and gif.
Direct loadpi -e ~/Projects/pi-simocracy/src/index.ts -ne -ns then /sim einstein
Standalone rendernode --experimental-strip-types /tmp/test-render-pet.mjs (ad-hoc harness used during the fix; pulls Einstein from the indexer and prints the same ANSI).
Type-checkcd ~/Projects/pi-simocracy && npx tsc --noEmit — clean.
Packnpm pack --dry-run — 15 files, 34 kB tarball, unchanged surface area.
Video01-load-einstein.webm (853 KB) · .gif (577 KB)
Tape01-load-einstein.tape
Stillsassets/00-before-no-avatar.png, assets/02-einstein-loaded.png, assets/03-einstein-reply.png