joeylib2/examples/spacetaxi/assets/genC64Data.py

191 lines
8.2 KiB
Python

#!/usr/bin/env python3
# genC64Data.py -- emit the C64 game data the JoeyLib Space Taxi port
# needs at runtime as C source: the custom charset (rendering AND the
# exact sprite-to-background collision bits), the standard sprite
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0)
# and the level-intro star velocity tables ($450C/$4513).
#
# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE
# "bank ram" dump, 2-byte load-address header) so the port has one
# source of truth: the original bytes. Output:
# examples/spacetaxi/stC64Data.c / stC64Data.h (committed, do not edit)
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin")
OUT_C = os.path.join(HERE, "..", "stC64Data.c")
OUT_H = os.path.join(HERE, "..", "stC64Data.h")
SPRITE_PTR_FIRST = 0xC0
SPRITE_PTR_LAST = 0xEC # $E9-$EC = passengers-leaving cels
SPRITE_BYTES = 63
# (enum name, address of the 9-byte program, comment)
SFX = [
("ST_SFX_GEAR", 0x6410, "landing gear toggle ($63DD)"),
("ST_SFX_CASH", 0x6DBB, "refuel ka-ching ($6E5B)"),
("ST_SFX_FUEL_LOW", 0x6DC4, "fuel below 3 cells ($6EBD)"),
("ST_SFX_FUEL_FULL", 0x6DCD, "tank full ($6E9E)"),
("ST_SFX_LAND_HARD", 0x6DD6, "hard landing ($6520)"),
("ST_SFX_IMPACT", 0x6DDF, "wreck hits the floor ($6AE1)"),
("ST_SFX_LAND_SOFT", 0x6DE8, "soft landing ($650F)"),
("ST_SFX_CRASH", 0x6DF1, "crash start ($6A56)"),
]
def load_raw():
with open(RAW, "rb") as fp:
raw = fp.read()
if len(raw) == 0x10002:
raw = raw[2:]
if len(raw) != 0x10000:
sys.exit(f"{RAW}: unexpected size {len(raw)}")
return raw
def c_bytes(data, per_line=16, indent=" "):
lines = []
for i in range(0, len(data), per_line):
chunk = data[i:i + per_line]
lines.append(indent + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
return "\n".join(lines)
# The cross-segment accessors the IIgs needs (see the block this emits
# into stC64Data.c). These used to be hand-added to the GENERATED file,
# which meant re-running this script silently deleted them and broke the
# IIgs build -- the header kept declaring them and nothing defined them.
# Prototype and return expression per accessor; the header prints the
# prototypes aligned, so keep the spacing.
ACCESSORS = [
("const uint8_t *stC64Charset(void);", "&kStCharset[0][0]"),
("const uint8_t *stC64SpriteBitmap(uint8_t idx);", "kStSpriteBitmaps[idx]"),
("uint8_t stC64RngByte(uint8_t idx);", "kStRngTable[idx]"),
("const uint8_t *stC64SfxProgram(uint8_t idx);", "kStSfxPrograms[idx]"),
("uint8_t stC64FlameCel(uint8_t dirMask);", "kStFlameCelByDirMask[dirMask]"),
]
def main():
raw = load_raw()
sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1
h = []
h.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.")
h.append("// Do not hand-edit; re-run the generator.")
h.append("")
h.append("#ifndef ST_C64_DATA_H")
h.append("#define ST_C64_DATA_H")
h.append("")
h.append("#include <stdint.h>")
h.append("")
h.append("// The game's custom character set ($2800-$2FFF): 256 glyphs x 8 rows,")
h.append("// bit 7 = leftmost pixel. Used to paint the screen AND for the")
h.append("// sprite-to-background collision test (a set bit is background data).")
h.append("#define ST_CHARSET_CHARS 256u")
h.append("extern const uint8_t kStCharset[ST_CHARSET_CHARS][8];")
h.append("")
h.append("// Standard sprite bitmaps (VIC bank 0, block ptr * 64). 21 rows x 3")
h.append("// bytes. Index = ptr - ST_SPRITE_PTR_FIRST.")
h.append(f"#define ST_SPRITE_PTR_FIRST 0x{SPRITE_PTR_FIRST:02X}u")
h.append(f"#define ST_SPRITE_PTR_LAST 0x{SPRITE_PTR_LAST:02X}u")
h.append(f"#define ST_SPRITE_COUNT {sprite_count}u")
h.append(f"#define ST_SPRITE_BYTES {SPRITE_BYTES}u")
h.append("extern const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES];")
h.append("")
h.append("// Demo-mode RNG lookup ($446A): rnd = table[t1] + t2 (see stSim.c).")
h.append("#define ST_RNG_TABLE_SIZE 64u")
h.append("extern const uint8_t kStRngTable[ST_RNG_TABLE_SIZE];")
h.append("")
h.append("// SID SFX programs, 9 bytes each, in $42E9 order: freq lo, freq hi,")
h.append("// pulse lo, pulse hi, control, AD, SR, release ticks, voice index.")
h.append("typedef enum {")
for name, addr, comment in SFX:
h.append(f" {name}, // ${addr:04X} {comment}")
h.append(" ST_SFX_COUNT")
h.append("} StSfxE;")
h.append("#define ST_SFX_PROGRAM_BYTES 9u")
h.append("extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES];")
h.append("")
h.append("// Engine-flame cel pointer by direction mask ($6DB0, 1=UP 2=DOWN 4=LEFT")
h.append("// 8=RIGHT); 0 = no cel for that combination.")
h.append("extern const uint8_t kStFlameCelByDirMask[16];")
h.append("")
h.append("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above")
h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data")
h.append("// reference from another bank is not relocatable.")
for decl in ACCESSORS:
h.append(decl[0])
h.append("")
h.append("#endif")
c = []
c.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.")
c.append("// Do not hand-edit; re-run the generator.")
c.append("")
c.append('#include "stC64Data.h"')
c.append("")
c.append("const uint8_t kStCharset[ST_CHARSET_CHARS][8] = {")
for ch in range(256):
rows = raw[0x2800 + ch * 8:0x2800 + ch * 8 + 8]
c.append(" { " + ", ".join(f"0x{b:02X}" for b in rows) + " }, // $" + f"{ch:02X}")
c.append("};")
c.append("")
c.append("const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES] = {")
for ptr in range(SPRITE_PTR_FIRST, SPRITE_PTR_LAST + 1):
base = ptr * 64
c.append(f" {{ // ptr ${ptr:02X}")
for row in range(21):
b = raw[base + row * 3:base + row * 3 + 3]
c.append(" " + ", ".join(f"0x{x:02X}" for x in b) + ",")
c.append(" },")
c.append("};")
c.append("")
c.append("const uint8_t kStRngTable[ST_RNG_TABLE_SIZE] = {")
c.append(c_bytes(raw[0x446A:0x446A + 64]))
c.append("};")
c.append("")
c.append("const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES] = {")
for name, addr, comment in SFX:
b = raw[addr:addr + 9]
c.append(" { " + ", ".join(f"0x{x:02X}" for x in b) + " }, // " + name)
c.append("};")
c.append("")
flame = list(raw[0x6DB0:0x6DB0 + 11]) + [0] * 5
c.append("const uint8_t kStFlameCelByDirMask[16] = {")
c.append(c_bytes(flame))
c.append("};")
c.append("")
c.append("")
c.append("// ---------------------------------------------------------------------------")
c.append("// Accessors. On the IIgs the program is split across banks and a 2-byte")
c.append("// data reference cannot cross a segment (only a 3-byte JSL/JML can). These")
c.append("// tiny functions live in the same segment as the tables, so their reads")
c.append("// are intra-segment; callers in other segments reach them by a supported")
c.append("// inter-segment call. On the other ports they inline away to nothing.")
c.append("// ---------------------------------------------------------------------------")
first = True
for decl, body in ACCESSORS:
c.append("")
if not first:
c.append("")
first = False
# The header pads return types to align its prototypes; the
# definitions here must not inherit that padding.
c.append(" ".join(decl[:-1].split()) + " {")
c.append(" return " + body + ";")
c.append("}")
with open(OUT_H, "w", encoding="ascii") as fp:
fp.write("\n".join(h) + "\n")
with open(OUT_C, "w", encoding="ascii") as fp:
fp.write("\n".join(c) + "\n")
print(f"wrote {OUT_H} and {OUT_C}")
if __name__ == "__main__":
main()