joeylib2/examples/spacetaxi/assets/extractFlame.py

103 lines
4.1 KiB
Python

#!/usr/bin/env python3
# extractFlame.py -- wire the engine-flame sprite cels from the C64 ROM
# image (stuff/spacetaxi/raw.bin) into the sprite sheet's flame row.
#
# The flame is C64 sprite 2, MULTICOLOR (verified: $D01C bit2 set). Its
# per-direction cel is chosen from the pointer table at $6DB0 (indexed by
# the direction mask $716A). Reading raw.bin at $6DB0 (2-byte load-addr
# header -> +2) gives, and the port's kFlameCelByDirMask mirrors exactly:
# mask 1 UP -> block $D8 mask 8 RIGHT -> block $D7
# mask 2 DOWN -> block $D4 mask 9 UP+RIGHT -> block $D3
# mask 4 LEFT -> block $D5 mask 10 DOWN+RIGHT-> block $D6
# mask 5 UP+LEFT -> block $D2
# mask 6 DOWN+LEFT -> block $D1
# flameCels[0..7] load from .spr cels 18..25 (sheet row 2, cols 0..7), so
# the cel order below is the kFlameCelByDirMask index order.
#
# Sprite data lives at block*64 in VIC bank 0 ($3000-$37FF). Each C64
# multicolor sprite is 12 MC-pixels x 21 rows (3 bytes/row, 2 bits/pixel);
# each MC pixel is 2 hi-res pixels, so it maps 1 MC pixel -> 2 of the
# port's 24-wide 4bpp cel pixels. Colors are the live-level flame palette
# ($7D05->$D025=$02 red = MC0, $7D06->$D026=$07 yellow = MC1; sprite-2
# colour = orange), mapped to sprites.png's indexed palette.
#
# Patches ONLY the flame row (row 2) of sprites.png -- taxi (row 0),
# passenger (row 1), and font.png are left untouched. Re-run the .spr
# bake afterwards (make/<port>.mk STAXI_SPR rule).
import os
import sys
try:
from PIL import Image
except ImportError:
sys.exit("extractFlame.py: needs Pillow (pip install pillow)")
HERE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin")
PNG = os.path.join(HERE, "sprites", "sprites.png")
CEL = 24 # cel is 24x24 px
LOAD_HDR = 2 # raw.bin 2-byte load-address header
# flameCels index -> C64 sprite block (from $6DB0 / kFlameCelByDirMask).
FLAME_BLOCKS = [0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6]
# Multicolor 2-bit pixel -> palette index. The engine renders sprite
# cels through kDefaultPalette (stRender.c), whose index IS the C64 VIC-II
# colour code -- the sprite's own baked palette is ignored (loadSpriteSheet
# passes NULL). So the C64 flame colours map DIRECTLY by code:
# 00 transparent(0), 01 MC0=$D025=$02 red(2), 11 MC1=$D026=$07 yellow(7),
# 10 sprite-2 colour -> orange(8) (barely used by these cels).
# (NOT the sprites.png preview-palette indices, which differ.)
MC_TO_INDEX = {0: 0, 1: 2, 2: 8, 3: 7}
FLAME_ROW_Y = 2 * CEL # sheet row 2 (flame row) top edge
def loadRaw():
with open(RAW, "rb") as f:
return f.read()
def celFromBlock(raw, block):
# Return a 24x24 list-of-rows of palette indices for one flame block.
base = LOAD_HDR + block * 64
cel = [[0] * CEL for _ in range(CEL)]
for row in range(21): # C64 sprite is 21 rows tall
for byteIdx in range(3): # 3 bytes/row
b = raw[base + row * 3 + byteIdx]
for pair in range(4): # 4 MC pixels per byte (MSB first)
mc = (b >> ((3 - pair) * 2)) & 3
idx = MC_TO_INDEX[mc]
px = (byteIdx * 4 + pair) * 2 # MC pixel -> 2 cel pixels
cel[row][px] = idx
cel[row][px + 1] = idx
return cel
def main():
raw = loadRaw()
im = Image.open(PNG)
if im.mode != "P":
sys.exit(f"extractFlame.py: {PNG} is {im.mode}, expected indexed 'P'")
px = im.load()
nonEmpty = 0
for celIdx, block in enumerate(FLAME_BLOCKS):
cel = celFromBlock(raw, block)
x0 = celIdx * CEL
opaque = 0
for y in range(CEL):
for x in range(CEL):
v = cel[y][x]
px[x0 + x, FLAME_ROW_Y + y] = v
if v != 0:
opaque += 1
nonEmpty += 1 if opaque else 0
print(f" flameCels[{celIdx}] block ${block:02X}: {opaque} opaque px -> cel {18 + celIdx}")
im.save(PNG)
print(f"extractFlame: patched {nonEmpty}/8 flame cels into {os.path.relpath(PNG, HERE)}")
if __name__ == "__main__":
main()