Every "Frame data" block in this disassembly is a byte stream in the game's own screen-drawing format - not a raw bitmap. It's built from the same control codes the ZX Spectrum ROM's own PRINT stream uses, and is fed to the screen the same way: byte by byte via RST $10, until a $FF terminator (draw_frame). Setting CHARS (set_chars) first, then replaying one of these streams, is really just "pick a tileset, then replay this PRINT sequence".
Byte(s) Meaning
$10,n INK n (0-7)
$11,n PAPER n (0-7)
$12,n FLASH n (0/1)
$13,n BRIGHT n (0/1)
$16,row,col AT row, col
$FF End of stream
anything else One character - see below
Any byte that isn't a control code prints one character at the cursor and advances it (wrapping at column 32). Which tile it draws from depends on the byte's range:
Byte range Tile source
$80-$8F Not a tile - a procedurally-generated 4-quadrant block fill (bit 0-3 = top-left/top-right/bottom-left/bottom-right quadrant on/off)
$90-$A4 A fixed UDG sprite tile at $FF58 + (byte-$90)*8 - always the hero's body parts, regardless of the active CHARS tileset
everything else A regular character tile from the active tileset: (CHARS value)+256+(byte-$20)*8. If CHARS is $3C00, the real ROM font is used instead (the ROM isn't part of the snapshot)
Two SkoolKit macros in paradisecafe.py render these streams: `decode_data` produces the actual picture (this is what's embedded throughout the disassembly); `decode_frame` produces an analysis table instead - one row per byte, with its address, meaning, screen position and a rendered tile where relevant. Most Frame data entries in this disassembly include both.