Skip to content

Define Encoded Sprite (0x08): indexed, palette-swap, and deflate sprite payloads - #242

Draft
SolbiatiAlessandro wants to merge 3 commits into
Metta-AI:masterfrom
SolbiatiAlessandro:compressed-sprites
Draft

Define Encoded Sprite (0x08): indexed, palette-swap, and deflate sprite payloads#242
SolbiatiAlessandro wants to merge 3 commits into
Metta-AI:masterfrom
SolbiatiAlessandro:compressed-sprites

Conversation

@SolbiatiAlessandro

Copy link
Copy Markdown

Proposal + prototype: a compressed sprite definition message. Heartleaf's director view ships a 7.5MB init packet for 600KB of source art, because Define Sprite (0x01) carries raw RGBA under snappy and the game pre-composites 20 tinted copies of 4 base sprites. This branch adds Define Encoded Sprite (0x08) and takes that packet to 1.08MB with pixel-identical output.

Message. 0x08 = the Define Sprite fields plus a u8 encoding before the payload length. Encodings: 0x00 rgba-snappy (legacy payload), 0x01 rgba-deflate (zlib over RGBA), 0x02 indexed (≤256 colors: palette + zlib over one index byte per pixel), 0x03 palette-swap (u16 source sprite id + a palette; the client reuses the source's index plane). 0x07 was left alone because stag_hunt uses it.

Decoders. Browser: client/spritecodec.js (synchronous, pixel-exact; used by global_client.html and player_client.html). Native/wasm: client/global_client.nim parseMessage case 0x08. parseSpritePacket returns 0x08 as spkSprite with an encoding field (legacy 0x01 parses as encoding 0x00); spriteMessageBytes knows 0x08.

Why not PNG. Same inflate work in JS plus chunk/filter parsing, no way to express a palette swap, and browser-native decode is asynchronous and not byte-exact for translucent pixels (canvas premultiplies).

Results on heartleaf's init packet (tools/init_packet_report.nim on the heartleaf side): 7,487,421 → 1,083,170 bytes. Main map bottom 685KB → 190KB (indexed); its five dusk tints 5×680KB → 5×81 bytes (palette-swap); overhang 164KB → 14KB; forest underlay 1.7MB → 767KB (rgba-deflate; 1629 colors, so no palette). The remaining packet is 71% forest; quantizing it to 4 bits/channel would give ~306KB (a heartleaf art decision).

Compatibility. 0x01 is unchanged and still tested. Old clients close on 0x08 under the existing unknown-type rule; games ship their own clients, so a game opts in by emitting 0x08. No change to client→server messages, replays, or certification tools (they do not parse sprite packets).

Tests. test_spriteprotocol (existing) passes; new test_spriteencoding (round trips for every encoding, chained swaps, fallback, sizes, malformed input) passes; new test_spritecodec_js runs node against client/spritecodec.js on 11 sprites covering every encoding plus legacy: 0 failed; test_client passes; the native client compiles.

Full write-up with the measurement tables and open points (0x07 vs 0x08 numbering, whether to keep rgba-snappy inside 0x08, hand-written inflater vs DecompressionStream, a sprite scale factor for large backgrounds): docs/compressed_sprites_proposal.md on this branch.

Companion heartleaf branch that emits 0x08: SolbiatiAlessandro/coworld-heartleaf compressed-sprites (pins this branch's commit in nimby.lock).

🤖 Generated with Claude Code

Alessandro Solbiati and others added 3 commits September 2, 2026 17:38
…lette-swap payloads

Define Sprite (0x01) ships every sprite as raw RGBA under Snappy: 4 bytes per
pixel before compression, and Snappy finds little in flat 16-color art. A
748x941 map with 16 colors costs 685 KB per copy on the wire, and a game that
pre-composites five dusk tints of it pays that six times.

This adds one server-to-client message, Define Encoded Sprite (0x08). It has
the fields of Define Sprite plus an encoding byte before the payload length:

  0x00 rgba-snappy   the legacy payload, unchanged
  0x01 rgba-deflate  zlib over raw RGBA
  0x02 indexed       u8 count-1, count*4 RGBA palette, zlib over one index
                     byte per pixel (sprites with at most 256 colors)
  0x03 palette-swap  u16 source sprite id, u8 count-1, count*4 RGBA palette;
                     reuses the index plane of an indexed sprite the client
                     already holds

Define Sprite keeps working unchanged; a client that has not been updated only
breaks if a server sends 0x08 to it, which is the same rule as for any new
message type.

Server side: addEncodedSprite picks indexed or rgba-deflate per sprite;
addPaletteSwapSprite emits a swap when the new pixels are a per-color
recoloring of the source and falls back to a normal sprite otherwise.
parseSpritePacket returns 0x08 as spkSprite with the encoding and payload;
decodeSprite turns any definition (legacy included) into straight RGBA and
keeps the index plane for later swaps.

Clients: the shared browser clients load client/spritecodec.js (a small
zlib/deflate inflater plus the palette expansion) and parse 0x08 next to 0x01;
the native and wasm global client decode through spriteprotocol.decodeSprite
with zippy. client.nim serves spritecodec.js on the same routes as
snappyjs.min.js.

Tests: tests/test_spriteencoding.nim round-trips every encoding
pixel-identically and covers the swap fallback, message sizes, and malformed
payloads; tests/test_spritecodec_js.nim runs the browser decoder under node
against packets the Nim encoder wrote (skips when node is absent);
tests/test_client.nim pins the new routes and parser cases.

Measured on Heartleaf's director init packet (313 sprites): 7,487,421 bytes
with Define Sprite, 1,083,170 bytes with Define Encoded Sprite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant