From 55be676c0c7027b47321f462376020a403248f71 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 29 Sep 2026 17:23:29 -0500 Subject: [PATCH] Tile generation tests. --- assets/extractFlame.py | 103 -------- assets/font.png | 3 - assets/genPlaceholderArt.py | 301 --------------------- assets/sprites/sprites.png | 3 - assets/tiles/tbank0.png | 3 - assets/tiles/tbank1.png | 3 - assets/tiles/tbank2.png | 3 - assets/tiles/tilebank0.png | 3 - extractFromDump.py | 496 ----------------------------------- spacetaxi.c | 1 - tools/staxiharness/harness.c | 1 - 11 files changed, 920 deletions(-) delete mode 100644 assets/extractFlame.py delete mode 100644 assets/font.png delete mode 100644 assets/genPlaceholderArt.py delete mode 100644 assets/sprites/sprites.png delete mode 100644 assets/tiles/tbank0.png delete mode 100644 assets/tiles/tbank1.png delete mode 100644 assets/tiles/tbank2.png delete mode 100644 assets/tiles/tilebank0.png delete mode 100644 extractFromDump.py diff --git a/assets/extractFlame.py b/assets/extractFlame.py deleted file mode 100644 index 8fc7d21..0000000 --- a/assets/extractFlame.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/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/.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() diff --git a/assets/font.png b/assets/font.png deleted file mode 100644 index 3bad103..0000000 --- a/assets/font.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74c85ec34cf69de066faabfa9ddab61768e254c5aeabf48f73dbf5315dccc469 -size 1379 diff --git a/assets/genPlaceholderArt.py b/assets/genPlaceholderArt.py deleted file mode 100644 index 6b7e84a..0000000 --- a/assets/genPlaceholderArt.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/env python3 -# genPlaceholderArt.py -- generate generic geometric placeholder art -# for the Space Taxi port. Output is 320x200 indexed PNG, 16-color. -# Replace these with hand-authored art any time; the build pipeline -# picks up whichever PNG sits in assets/. - -import os -from PIL import Image, ImageDraw - -ROOT = os.path.dirname(os.path.abspath(__file__)) - -# 16-color palette matching the engine's kDefaultPalette (see stRender.c). -# Each entry is (R, G, B) in 0..255. The .jas format will store the -# 4-bit-per-channel quantization automatically via joeyasset. -PALETTE_RGB = [ - (0x00, 0x00, 0x00), # 0 black - (0x00, 0x00, 0x77), # 1 dark blue - (0xCC, 0x00, 0x00), # 2 red - (0x00, 0x77, 0x00), # 3 dark green - (0x66, 0x44, 0x00), # 4 brown - (0xEE, 0xEE, 0x00), # 5 yellow - (0x88, 0x88, 0x88), # 6 mid gray - (0xBB, 0xBB, 0xBB), # 7 light gray - (0x44, 0x66, 0xFF), # 8 sky blue - (0x77, 0x88, 0xFF), # 9 light blue - (0xFF, 0x88, 0x88), # 10 light red - (0x88, 0xFF, 0x44), # 11 light green - (0xFF, 0x88, 0x00), # 12 orange - (0xFF, 0x00, 0xFF), # 13 magenta - (0x00, 0xFF, 0xFF), # 14 cyan - (0xFF, 0xFF, 0xFF), # 15 white -] - - -def new_canvas(w=320, h=200): - """Returns a fresh w x h indexed PIL image with the palette installed.""" - img = Image.new('P', (w, h), 0) - flat = [] - for r, g, b in PALETTE_RGB: - flat.extend([r, g, b]) - flat.extend([0] * (768 - len(flat))) # pad to 256 entries - img.putpalette(flat) - return img - - -def write_indexed_png(img, path): - """Save with mode='P' so PNG output is indexed (joeyasset wants this - when converted through ppm; ImageMagick's `convert` preserves - indexed mode when writing PPM).""" - img.save(path, 'PNG', optimize=True) - print(f" wrote {path}") - - -def gen_tilebank(): - """tilebank0.png: 320x200, 40x25 cells of 8x8 each (= 1000 tile - slots). The engine convention is: - index 0 = empty (non-solid) - index 1..63 = solid walls - index 64..127 = landing-pad surfaces - index 128..255 = decorative non-solid - We fill a few representative slots; the rest stay black so they - show as "empty" if a level references them. - """ - img = new_canvas() - px = img.load() - - def draw_tile(idx, painter): - bx = (idx % 40) * 8 - by = (idx // 40) * 8 - painter(bx, by) - - def block(color): - return lambda x, y: [px.__setitem__((x+dx, y+dy), color) - for dx in range(8) for dy in range(8)] - - def bricks(color_a, color_b): - def paint(x, y): - for dy in range(8): - row_offset = 4 if (y // 8 + dy // 4) % 2 == 0 else 0 - for dx in range(8): - is_grout = (dx == (row_offset % 8)) or (dy == 3 or dy == 7) - px[x + dx, y + dy] = color_b if is_grout else color_a - return paint - - def stripes(color_a, color_b): - def paint(x, y): - for dy in range(8): - c = color_a if (dy + (x // 8)) % 2 == 0 else color_b - for dx in range(8): - px[x + dx, y + dy] = c - return paint - - def landing_pad(color_a, color_b): - # diagonal stripes signal a landing-pad surface - def paint(x, y): - for dy in range(8): - for dx in range(8): - px[x + dx, y + dy] = color_a if ((dx + dy) // 2) % 2 == 0 else color_b - return paint - - # tile 0 -> already black (empty); leave as-is - - # walls 1..7: a variety of brick/stripe patterns - draw_tile(1, bricks(6, 0)) # gray brick - draw_tile(2, bricks(4, 0)) # brown brick - draw_tile(3, block(6)) # solid gray - draw_tile(4, block(4)) # solid brown - draw_tile(5, stripes(6, 7)) # gray stripes (ceiling) - draw_tile(6, stripes(1, 8)) # blue stripes (sky deco) - draw_tile(7, block(7)) # solid light gray - - # landing pad tiles 64..67 - draw_tile(64, landing_pad(5, 12)) # yellow/orange pad - draw_tile(65, landing_pad(11, 3)) # green pad - draw_tile(66, landing_pad(9, 8)) # blue pad - draw_tile(67, landing_pad(15, 7)) # white pad - - write_indexed_png(img, os.path.join(ROOT, 'tiles', 'tilebank0.png')) - - -def gen_sprites(): - """RETIRED 2026-07-20: sprites.png is now fully extracted from the - C64 dump by stuff/spacetaxi/extractSprites.py (taxi $C1/$C0/$DC/ - $DD, passenger $C4-$CB/$D9, flame $6DB0 table, warp $E2-$E8 -- - 216x96, 4 rows). Regenerating placeholder art here would clobber - the real sheet, the same trap gen_font has with the hand-edited - font.png. Refuses to run; use extractSprites.py instead.""" - print("gen_sprites: REFUSING -- sprites.png is real C64 art now.") - print(" regenerate via: python3 stuff/spacetaxi/extractSprites.py " - "stuff/spacetaxi/raw.bin examples/spacetaxi/assets/sprites/sprites.png") - return - - -def gen_sprites_retired(): - img = new_canvas(9 * 24, 3 * 24) # 216x72: 9 cols x 3 rows of 24x24 - d = ImageDraw.Draw(img) - - # ---- Taxi cels: simple side-view "pod with thruster" ---- - # Cel 0 = idle (thruster off) - # Cel 1..3 = thrust-on, alternating flame frames - for cel in range(4): - x = cel * 24 - # Pod body: rounded rectangle in gray - d.rectangle([x+2, 4, x+21, 16], fill=7, outline=15) - # Cockpit window in blue - d.rectangle([x+13, 6, x+19, 10], fill=8, outline=15) - # Landing skid - d.line([(x+3, 17), (x+20, 17)], fill=6) - d.line([(x+3, 17), (x+3, 19)], fill=6) - d.line([(x+20,17), (x+20, 19)], fill=6) - # Thruster flame (cels 1..3 only, alternating shape) - if cel >= 1: - flame_color = [2, 12, 5][(cel - 1) % 3] - flame_top = 18 - flame_bot = 22 if cel != 2 else 23 - d.polygon([(x+8, flame_top), - (x+15, flame_top), - (x+12, flame_bot)], fill=flame_color) - - # ---- Taxi shrink cels (transporter warp): the cab receding into the - # distance, 5 progressively smaller sizes, centered in the 24x24 cell. - # Placed in the free row-0 slots (cels 4..8). No flame -- it is warping - # away, not thrusting. The renderer draws one per warp step ($E2..$E9 - # on the C64 = 8 shrink cels; the port approximates with 5 sizes). - for i in range(5): - cx = (4 + i) * 24 + 12 - scale = 1.0 - i * 0.19 # 1.0, 0.81, 0.62, 0.43, 0.24 - bw = max(3, int(round(19 * scale))) - bh = max(2, int(round(12 * scale))) - x0 = cx - bw // 2 - y0 = 12 - bh // 2 - # Pod body (gray, white outline) - d.rectangle([x0, y0, x0 + bw, y0 + bh], fill=7, outline=15) - # Cockpit window (blue) while big enough to show - if bw >= 8: - d.rectangle([x0 + bw - 5, y0 + 1, x0 + bw - 1, y0 + 3], fill=8) - - # ---- Passenger cels: small stick figure, walk cycle ---- - # Centered in 16x16, walking right. - for cel in range(4): - x = cel * 16 - y = 24 - # Head - d.ellipse([x+5, y+1, x+10, y+6], fill=10, outline=15) - # Body - d.line([(x+7, y+7), (x+7, y+12)], fill=15) - # Arms (swing alternately based on cel) - if cel == 0 or cel == 2: - d.line([(x+4, y+10), (x+10, y+10)], fill=15) - else: - d.line([(x+3, y+9), (x+11, y+11)], fill=15) - # Legs (alternating stride) - if cel == 0 or cel == 2: - d.line([(x+7, y+12), (x+4, y+15)], fill=15) - d.line([(x+7, y+12), (x+10,y+15)], fill=15) - else: - d.line([(x+7, y+12), (x+5, y+15)], fill=15) - d.line([(x+7, y+12), (x+9, y+15)], fill=15) - - write_indexed_png(img, os.path.join(ROOT, 'sprites', 'sprites.png')) - - -def gen_font(): - """font.png: 320x200 (40x25 grid of 8x8 ASCII glyphs). - Authored manually for the printable subset (0x20..0x7E). - Each glyph cell is at (ascii%40, ascii/40) when ascii<128. - Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime). - - WARNING: this generator is UPPERCASE-ONLY (lowercase maps to the - uppercase glyphs). The COMMITTED font.png has been hand-extended with - real lowercase glyphs, so running this clobbers it -- the HUD reverts - to all-caps. Regenerate SPRITES only (gen_sprites) unless you also add - the lowercase glyphs here first; then `git checkout` font.png. - """ - # A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px - # padding on all sides (so 5x7 active inside an 8x8 cell, baseline - # at row 6). The font supports digits, uppercase letters, space, - # and basic punctuation -- enough for HUD text. Lowercase letters - # map to the same glyphs as uppercase (room to add later). - GLYPHS = { - ' ': [], - '!': ['..#..', '..#..', '..#..', '..#..', '.....', '..#..', '.....'], - '"': ['.#.#.', '.#.#.', '.....', '.....', '.....', '.....', '.....'], - '0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'], - '1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'], - '2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'], - '3': ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'], - '4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'], - '5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'], - '6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'], - '7': ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'], - '8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'], - '9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'], - ':': ['.....', '..#..', '.....', '.....', '.....', '..#..', '.....'], - '.': ['.....', '.....', '.....', '.....', '.....', '..#..', '.....'], - '-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'], - '/': ['....#', '...#.', '...#.', '..#..', '.#...', '.#...', '#....'], - 'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], - 'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'], - 'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'], - 'D': ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'], - 'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'], - 'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'], - 'G': ['.###.', '#...#', '#....', '#..##', '#...#', '#...#', '.###.'], - 'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], - 'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'], - 'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'], - 'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'], - 'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'], - 'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'], - 'N': ['#...#', '##..#', '#.#.#', '#.#.#', '#..##', '#...#', '#...#'], - 'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], - 'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'], - 'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'], - 'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'], - 'S': ['.###.', '#...#', '#....', '.###.', '....#', '#...#', '.###.'], - 'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'], - 'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], - 'V': ['#...#', '#...#', '#...#', '#...#', '.#.#.', '.#.#.', '..#..'], - 'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '##.##', '#...#'], - 'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'], - 'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'], - 'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'], - } - - img = new_canvas() - px = img.load() - - def paint_glyph(ascii_code, glyph_rows): - col = ascii_code % 40 - row = ascii_code // 40 - bx = col * 8 - by = row * 8 - # active 5x7 area: cell-x 1..5, cell-y 0..6 (top row, 1px left pad) - for ry, line in enumerate(glyph_rows): - for rx, ch in enumerate(line): - if ch == '#': - px[bx + 1 + rx, by + ry] = 15 # white - - for ascii_code in range(32, 127): - c = chr(ascii_code) - if c in GLYPHS: - paint_glyph(ascii_code, GLYPHS[c]) - elif 'a' <= c <= 'z': - paint_glyph(ascii_code, GLYPHS[c.upper()]) - - write_indexed_png(img, os.path.join(ROOT, 'font.png')) - - -def main(): - os.makedirs(os.path.join(ROOT, 'tiles'), exist_ok=True) - os.makedirs(os.path.join(ROOT, 'sprites'), exist_ok=True) - print("genPlaceholderArt: emitting 320x200 indexed PNGs...") - gen_tilebank() - gen_sprites() - gen_font() - print("done.") - - -if __name__ == '__main__': - main() diff --git a/assets/sprites/sprites.png b/assets/sprites/sprites.png deleted file mode 100644 index 85b7bb7..0000000 --- a/assets/sprites/sprites.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6c0257eff1f87dc285fb951c460ae91c546a79883be959ebf5ee445979ab59a -size 843 diff --git a/assets/tiles/tbank0.png b/assets/tiles/tbank0.png deleted file mode 100644 index fc5ee47..0000000 --- a/assets/tiles/tbank0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc2c7c8b5e7e76db87264621c938e9328059949ffa097199c428b83c51fce9ee -size 1879 diff --git a/assets/tiles/tbank1.png b/assets/tiles/tbank1.png deleted file mode 100644 index f83877b..0000000 --- a/assets/tiles/tbank1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6fee43fab89a4a9784e0900fd4d58c4b0e0b2ed1872bdf746fcfaa14f59eca9f -size 1201 diff --git a/assets/tiles/tbank2.png b/assets/tiles/tbank2.png deleted file mode 100644 index 1cb395a..0000000 --- a/assets/tiles/tbank2.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a056acda9a6cb50b2a7941830750e820898087880995d65c514d98d19d0a56ea -size 1237 diff --git a/assets/tiles/tilebank0.png b/assets/tiles/tilebank0.png deleted file mode 100644 index 8dedcfd..0000000 --- a/assets/tiles/tilebank0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a60d804d43b02325963da525328818da646f44cb7fcb9cdcf95686a83325c98 -size 1039 diff --git a/extractFromDump.py b/extractFromDump.py deleted file mode 100644 index e7b792d..0000000 --- a/extractFromDump.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -# extractFromDump.py -- convert a VICE C64 memory dump into JoeyLib -# Space Taxi assets. -# -# Reads: /tmp/spacetaxi/mem0000-.bin (VICE `save` output; -# 2-byte LE start-addr header + 64 KB RAM). -# Writes: examples/spacetaxi/assets/tiles/tilebank.png -# examples/spacetaxi/assets/levels/level.txt -# examples/spacetaxi/assets/sprites/sprites.png (level1 only) -# examples/spacetaxi/assets/font.png (level1 only) -# -# Per-level: -# - Custom charset at $2800-$2FFF (2 KB, 256 chars * 8 bytes mono) -# becomes a tile bank PNG: 40 cols x 25 rows of 8x8 cells, with -# tile index N at cell (N%40, N/40). The PNG uses the EGA-ish -# 16-color palette we already install on stage. -# - Screen RAM at $0400-$07E7 (1000 bytes = 40x25 char codes) and -# color RAM at $D800-$DBE7 (1000 bytes = 40x25 palette nybbles) -# become the tilemap+colormap in the level .txt source. -# -# Sprite data at $3000-$37FF + sprite pointer table at $07F8-$07FF -# is decoded once (it's the same across levels for the active set): -# - sprite 0 / 7: the taxi cab body & shadow -# - sprite 1..6: passenger frames / animation cels -# 64 bytes per sprite, 24x21 1-bit, MSB-leftmost. Color comes from -# $D027-$D02E (one color per sprite). -# -# Usage: -# python3 extractFromDump.py stuff/spacetaxi/mem0000-level1.bin level01 1 -# python3 extractFromDump.py stuff/spacetaxi/mem0000-level2.bin level02 2 -# args: -# -# Dumps live in stuff/spacetaxi/ (committed-ish, repo-local) rather -# than /tmp so they survive reboots and stay with the project. - -import os -import struct -import sys -from collections import Counter - -from PIL import Image - - -# C64 standard color palette (EGA-ish 16-color JoeyLib mapping). -# Indices 0..15 match the C64 VIC-II color register order so a value -# read from $D8xx or $D02x maps directly to a JoeyLib palette slot. -# Colors below approximate Pepto's standard PAL palette in $RGB form. -C64_PALETTE_RGB = [ - (0x00, 0x00, 0x00), # 0 black - (0xFF, 0xFF, 0xFF), # 1 white - (0x88, 0x39, 0x32), # 2 red - (0x67, 0xB6, 0xBD), # 3 cyan - (0x8B, 0x3F, 0x96), # 4 purple - (0x55, 0xA0, 0x49), # 5 green - (0x40, 0x31, 0x8D), # 6 blue - (0xBF, 0xCE, 0x72), # 7 yellow - (0x8B, 0x54, 0x29), # 8 orange - (0x57, 0x42, 0x00), # 9 brown - (0xB8, 0x69, 0x62), # 10 light red - (0x50, 0x50, 0x50), # 11 dark gray - (0x78, 0x78, 0x78), # 12 mid gray - (0x94, 0xE0, 0x89), # 13 light green - (0x78, 0x69, 0xC4), # 14 light blue - (0x9F, 0x9F, 0x9F), # 15 light gray -] - - -ROOT = os.path.dirname(os.path.abspath(__file__)) - - -def load_dump(path): - with open(path, "rb") as f: - raw = f.read() - start = raw[0] | (raw[1] << 8) - if start != 0: - raise SystemExit(f"{path}: unexpected start ${start:04X}, want $0000") - if len(raw) - 2 != 0x10000: - raise SystemExit(f"{path}: not a 64K dump (got {len(raw) - 2} bytes)") - return raw[2:] - - -def new_canvas(w, h): - img = Image.new("P", (w, h), 0) - flat = bytearray() - for r, g, b in C64_PALETTE_RGB: - flat.extend((r, g, b)) - flat.extend(b"\x00" * (768 - len(flat))) - img.putpalette(bytes(flat)) - return img - - -def paint_charset_glyph(px, mem, src_code, bx, by): - """Plot the 8x8 glyph for C64 screen code `src_code` at pixel - (bx, by) of the indexed-palette PIL image. Foreground = palette - index 1, background untouched (already index 0 from new_canvas). - """ - src = 0x2800 + src_code * 8 - for r in range(8): - byte = mem[src + r] - for c in range(8): - if byte & (0x80 >> c): - px[bx + c, by + r] = 1 - - -def extract_tilebank(mem, out_path, code_to_slot=None): - """Render the custom charset at $2800-$2FFF as a 320x200 PNG. - When `code_to_slot` is None, lays out all 256 chars at their raw - C64 positions (40 cols x 25 rows). When `code_to_slot` is given, - writes each charset glyph at the slot the level loader expects - -- i.e. the bank tile at position N is the glyph for screen-code - `code_for_slot[N]`. That keeps level data and tilebank aligned - after the level extractor's frequency-sorted code remapping. - """ - img = new_canvas(320, 200) - px = img.load() - if code_to_slot is None: - for ch in range(256): - paint_charset_glyph(px, mem, ch, (ch % 40) * 8, (ch // 40) * 8) - else: - # Invert: slot -> code. Multiple codes can collide into slot 0 - # (background); only the first one we see "owns" that slot for - # painting purposes -- the rest just fall through to the same - # blank tile. Highest priority is the explicit space mapping. - slot_to_code = {} - for code, slot in code_to_slot.items(): - slot_to_code.setdefault(slot, code) - for slot, code in slot_to_code.items(): - if slot >= 256: - continue - paint_charset_glyph(px, mem, code, (slot % 40) * 8, (slot // 40) * 8) - img.save(out_path, "PNG", optimize=True) - - -def build_code_to_slot(mem): - """Build the screen-code -> alphabet-slot map shared between the - tilebank emission and the level emission. The slot index is what - the runtime sees in level.tilemap[]; the tilebank must mirror this - ordering so a level-data slot-N cell renders the charset glyph - that was actually used in the original screen RAM at that code. - """ - screen = mem[0x0400:0x0400 + 1000] - codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0])) - code_to_slot = {} - next_slot = 0 - for code, _count in codes: - if code == 0x20: # space -> always index 0 - code_to_slot[code] = 0 - continue - if code in code_to_slot: - continue - if next_slot >= len(ALPHABET): - code_to_slot[code] = 0 - continue - # Reserve slot 0 for background (space). The very first - # non-space code claims slot 1, etc. - if next_slot == 0: - next_slot = 1 - code_to_slot[code] = next_slot - next_slot += 1 - return code_to_slot - - -def screencode_to_char(code): - """Convert a C64 screen code into a printable tilemap character - using the encoding the .txt format already understands: - 0 -> '.' (treated as 0) - 1..9 -> '1'..'9' (digits encode the lower 10 indices) - but we shift to use up most of the index space below. - For arbitrary 0..255 we need a 256-symbol alphabet -- not feasible - in a single character per cell. So we encode as TWO chars per cell - using base-16 hex would still only cover 0..255. Decision: keep - .txt cells one-char and map screen codes via a custom 64-char - alphabet plus an @char directive table emitted into the file so - the loader knows exactly which screen code each char represents. - """ - # This function is unused as a one-char encoder; we encode via the - # generic alphabet below instead. - return code - - -# Generic 1-char alphabet (62 codes); the level loader's index map -# was: '.' or ' '=0, '0'..'9'=0..9, 'A'..'Z'=10..35, 'a'..'z'=36..61. -# That's only 62 codes, not 256. The original C64 levels use ~30 -# distinct screen codes per level so most levels fit. We map the N -# most-frequent screen codes to those alphabet slots. -ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - - -def extract_level(mem, level_name, tilebank_id, out_path, code_to_alpha): - """Render screen RAM + color RAM as an STL1-format .txt source - using the prebuilt screen-code -> alphabet-slot mapping. Caller - must pass the same mapping that was used to reorder the tilebank - PNG; otherwise the level tilemap and tilebank diverge. - """ - screen = mem[0x0400:0x0400 + 1000] # 40x25 - color = mem[0xD800:0xD800 + 1000] # 40x25, low nybble = palette slot - codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0])) - - # Full 25-row C64 screen. The title screen uses all 25 rows - # (PUBLISHED ... SOFTWARE credit wraps across rows 21, 22, 24 - # with frame chars interleaved). Gameplay HUD draws over the - # bottom 3 rows at runtime. - playfield_rows = 25 - - lines = [] - lines.append(f"# Extracted from VICE memory dump. Edit at your own risk;") - lines.append(f"# regenerating will overwrite. Keep the @tile mappings in") - lines.append(f"# sync with the tilebank PNG (extracted from $2800).") - lines.append(f"") - lines.append(f"@name {level_name.upper()}") - lines.append(f"@tilebank {tilebank_id}") - lines.append(f"@music 0") - bg = mem[0xD021] & 0x0F - border = mem[0xD020] & 0x0F - lines.append(f"@bgColor {bg}") - lines.append(f"@borderColor {border}") - - # Best-effort taxi spawn: middle-top of playfield. The original - # script-driven spawn position would need to be lifted out of - # game state at dump time; for now hand-edit if you want it - # to match the cracker's preferred spawn. - lines.append(f"@taxiSpawn 18 2") - lines.append(f"") - - # Pads: scan the playfield for "landable surfaces" -- runs of - # >= 3 identical non-background tile cells with two or more - # empty rows directly above (so the taxi can approach from the - # top). Per-row scanning runs top-to-bottom; for the same - # surface tile across rows we only take the topmost contiguous - # platform edge. - bg_code = codes[0][0] if codes else 0x20 - def is_bg(code): - return code == bg_code or code == 0x20 - - pads = [] - seen_used = [False] * (playfield_rows * 40) - for row in range(2, playfield_rows): - line = screen[row * 40:(row + 1) * 40] - prev = screen[(row - 1) * 40:row * 40] - prev2 = screen[(row - 2) * 40:(row - 1) * 40] - col = 0 - while col < 40 and len(pads) < 8: - if is_bg(line[col]): - col += 1 - continue - j = col - while j < 40 and line[j] == line[col] and not seen_used[row * 40 + j]: - j += 1 - width = j - col - # Approachable from above? At least 2/3 of cells in the - # 2 rows above this stretch must be background. - above_clear = 0 - for k in range(col, j): - if is_bg(prev[k]): - above_clear += 1 - if is_bg(prev2[k]): - above_clear += 1 - if width >= 3 and above_clear >= width: - pads.append((row, col, width)) - for k in range(col, j): - seen_used[row * 40 + k] = True - col = j - - lines.append("# Pads auto-detected from landable surfaces (>= 3 wide,") - lines.append("# approachable from above). Verify and adjust as needed.") - pad_letter = ord("A") - for (row, col, width) in pads: - if pad_letter > ord("H"): - break - lines.append(f"@pad {chr(pad_letter)} {col} {row} {width}") - pad_letter += 1 - if pad_letter == ord("A"): - lines.append("# (No landable surfaces auto-detected; emitting placeholders.)") - lines.append("@pad A 4 10 4") - lines.append("@pad B 32 10 4") - pad_letter = ord("C") - - lines.append("") - # Fare list: simple round-robin between detected pads. - pad_count = pad_letter - ord("A") - if pad_count >= 2: - for k in range(pad_count): - src = chr(ord("A") + k) - dst = chr(ord("A") + ((k + 1) % pad_count)) - lines.append(f"@fare {src} {dst} 45") - else: - lines.append("@fare A B 45") - lines.append("") - - # Emit the @tile mapping for round-trip clarity (and so the - # author can read a level .txt and tell which screen code maps - # to which char). Format: @tile -- the - # mkstlevel parser ignores these today but they're useful - # documentation, and a future loader could honor them. - lines.append("# Screen-code -> alphabet-char mapping (one per unique code).") - inv = {v: k for k, v in code_to_alpha.items()} - for idx in sorted(inv): - ch = '.' if idx == 0 else ALPHABET[idx] - code = inv[idx] - lines.append(f"# @tile '{ch}' = $C64_screencode_{code:02X}") - lines.append("") - - # Tilemap (22 rows of 40 chars each). - lines.append("@tilemap") - for r in range(playfield_rows): - row = screen[r * 40:(r + 1) * 40] - s = "" - for code in row: - idx = code_to_alpha.get(code, 0) - s += '.' if idx == 0 else ALPHABET[idx] - lines.append(s) - - lines.append("") - lines.append("@colormap") - for r in range(playfield_rows): - row = color[r * 40:(r + 1) * 40] - s = "".join( - ('0' if (c & 0x0F) == 0 - else ALPHABET[(c & 0x0F)] - if (c & 0x0F) < len(ALPHABET) else '0') - for c in row - ) - lines.append(s) - - with open(out_path, "w") as fp: - fp.write("\n".join(lines) + "\n") - - -def extract_sprites(mem, out_path): - """Render C64 hardware sprites into the JoeyLib-expected sprite - sheet layout (320x200, 16-color indexed PNG): - y= 0..23 : 1 taxi cel (24x24, x = 0) - sprite 0 ptr (in level1 dump: $DC -> $3700) - The adjacent slot $DD is the landing-gear-down - pose, not a thrust frame -- ignored for now. - y= 24..47 : 4 passenger cels (16x16 each, x = 0, 16, 32, 48) - Sprite 3 ptr ($07FB) is the passenger data; sprites 3-6 share it. - """ - img = new_canvas(320, 200) - px = img.load() - - # Shared multicolor sprite colors from VIC registers. - mcm_color0 = mem[0xD025] & 0x0F # 2-bit value 01 - mcm_color1 = mem[0xD026] & 0x0F # 2-bit value 11 - mcm_mask = mem[0xD01C] # bit per sprite: 1 = multicolor - - def paint_sprite_at(data_addr, sprite_color, dst_x, dst_y, dst_w, dst_h, multicolor=False): - # Mono C64 sprite is 24x21, 1 bit per pixel, 3 bytes per row. - # Multicolor C64 sprite is 12 "double-wide pixels" x 21 rows, - # 2 bits per pixel, still 3 bytes per row. Color mapping: - # 00 -> transparent - # 01 -> mcm_color0 ($D025) - # 10 -> sprite_color (sprite's own $D027+sp) - # 11 -> mcm_color1 ($D026) - if sprite_color == 0: - sprite_color = 1 - sx_max = min(24, dst_w) - sy_max = min(21, dst_h) - for r in range(sy_max): - if multicolor: - for byte_idx in range(3): - byte = mem[data_addr + r * 3 + byte_idx] - # 4 multicolor pixels per byte; each spans 2 - # hardware pixels wide. - for pp in range(4): - bits = (byte >> ((3 - pp) * 2)) & 0x03 - if bits == 0: - continue - if bits == 1: - col = mcm_color0 - elif bits == 2: - col = sprite_color - else: - col = mcm_color1 - sx0 = byte_idx * 8 + pp * 2 - if sx0 + 1 >= sx_max: - break - px[dst_x + sx0, dst_y + r] = col - px[dst_x + sx0 + 1, dst_y + r] = col - else: - for byte_idx in range(3): - byte = mem[data_addr + r * 3 + byte_idx] - for bit in range(8): - sx = byte_idx * 8 + bit - if sx >= sx_max: - break - if byte & (0x80 >> bit): - px[dst_x + sx, dst_y + r] = sprite_color - - def paint_c64_sprite(sp_index, dst_x, dst_y, dst_w, dst_h): - ptr = mem[0x07F8 + sp_index] - if ptr == 0: - return False - sprite_color = mem[0xD027 + sp_index] & 0x0F - is_multicolor = bool(mcm_mask & (1 << sp_index)) - paint_sprite_at(ptr * 64, sprite_color, dst_x, dst_y, dst_w, dst_h, - multicolor=is_multicolor) - return True - - # Single taxi cel from sprite 0's actual pointer. - paint_c64_sprite(0, 0, 0, 24, 24) - - # 4 passenger cels = the shared C64 sprite 3 (sprites 3..6 all - # point to the same data in the level-1 dump). - for cel in range(4): - paint_c64_sprite(3, cel * 16, 24, 16, 16) - - img.save(out_path, "PNG", optimize=True) - - -def extract_font(mem, out_path): - """Render the custom charset (which IS the in-game font in C64 - text-mode games) as the 320x200 font.png. Same layout as - tilebank, but here we want each glyph at cell (ascii%40, - ascii/40) for jlDrawText's ASCII map. - - For Space Taxi specifically, the charset doubles as the level - tile bank AND any in-screen text Sierra-style. We map the C64 - screen codes used for ASCII display (codes 1..26 = A..Z, - 32..63 = punctuation/digits) directly to their ASCII positions - in the JoeyLib font sheet. - """ - img = new_canvas(320, 200) - px = img.load() - - def render_at(ascii_code, charset_idx): - if ascii_code < 32 or ascii_code >= 128: - return - src = 0x2800 + (charset_idx & 0xFF) * 8 - col = ascii_code % 40 - row = ascii_code // 40 - bx = col * 8 - by = row * 8 - for r in range(8): - byte = mem[src + r] - for c in range(8): - if byte & (0x80 >> c): - px[bx + c, by + r] = 1 - - # C64 upper-only screen codes: 1..26 = A..Z, 27=[ 28=£ 29=] 30=↑ 31=← - # 32 = space, 33..63 = !"# ... ?, etc. We map ASCII directly. - for c in range(32, 64): - render_at(c, c) # symbols + digits 1:1 - for c, code in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ"): - render_at(ord(code), 1 + c) # screen code 1..26 -> A..Z - - img.save(out_path, "PNG", optimize=True) - - -def main(): - if len(sys.argv) != 4: - print("usage: extractFromDump.py ", file=sys.stderr) - sys.exit(2) - dump_path = sys.argv[1] - level_name = sys.argv[2] - tilebank_id = int(sys.argv[3]) - - mem = load_dump(dump_path) - print(f"loaded {dump_path}: 64 KB OK") - - tiles_dir = os.path.join(ROOT, "assets", "tiles") - sprites_dir = os.path.join(ROOT, "assets", "sprites") - levels_dir = os.path.join(ROOT, "assets", "levels") - os.makedirs(tiles_dir, exist_ok=True) - os.makedirs(sprites_dir, exist_ok=True) - os.makedirs(levels_dir, exist_ok=True) - - # Build the screen-code -> bank-slot mapping once and feed it to - # both extractors so the tilemap and tilebank stay aligned. - code_to_slot = build_code_to_slot(mem) - - # 8.3 DOS naming -- "tilebank0" is 9 chars and fopen fails under - # DOSBox strict 8.3 mode. Keep the short form across the pipeline. - tilebank_path = os.path.join(tiles_dir, f"tbank{tilebank_id}.png") - extract_tilebank(mem, tilebank_path, code_to_slot) - print(f" wrote {tilebank_path}") - - level_path = os.path.join(levels_dir, f"{level_name}.txt") - extract_level(mem, level_name, tilebank_id, level_path, code_to_slot) - print(f" wrote {level_path}") - - # Sprites + font come from level 1 only (sprite pointer table is - # script-driven so it differs by scene; we use the first dump's - # set as the canonical sheet, plus its charset as the font). - if tilebank_id == 1: - sprite_path = os.path.join(sprites_dir, "sprites.png") - extract_sprites(mem, sprite_path) - print(f" wrote {sprite_path}") - - font_path = os.path.join(ROOT, "assets", "font.png") - extract_font(mem, font_path) - print(f" wrote {font_path}") - - -if __name__ == "__main__": - main() diff --git a/spacetaxi.c b/spacetaxi.c index b8b5571..10232d6 100644 --- a/spacetaxi.c +++ b/spacetaxi.c @@ -983,7 +983,6 @@ int main(void) { #else config.codegenBytes = 160UL * 1024; #endif - config.audioBytes = 32UL * 1024; if (!jlInit(&config)) { return 1; } diff --git a/tools/staxiharness/harness.c b/tools/staxiharness/harness.c index aab0a57..c368693 100644 --- a/tools/staxiharness/harness.c +++ b/tools/staxiharness/harness.c @@ -95,7 +95,6 @@ int main(void) { uint8_t k; config.codegenBytes = 160UL * 1024; - config.audioBytes = 32UL * 1024; if (!jlInit(&config)) { printf("jlInit failed\n"); return 1;