Tile generation tests.
This commit is contained in:
parent
dcddbbfe97
commit
55be676c0c
11 changed files with 0 additions and 920 deletions
|
|
@ -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/<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()
|
||||
BIN
assets/font.png
(Stored with Git LFS)
BIN
assets/font.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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()
|
||||
BIN
assets/sprites/sprites.png
(Stored with Git LFS)
BIN
assets/sprites/sprites.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/tiles/tbank0.png
(Stored with Git LFS)
BIN
assets/tiles/tbank0.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/tiles/tbank1.png
(Stored with Git LFS)
BIN
assets/tiles/tbank1.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/tiles/tbank2.png
(Stored with Git LFS)
BIN
assets/tiles/tbank2.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/tiles/tilebank0.png
(Stored with Git LFS)
BIN
assets/tiles/tilebank0.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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-<labelN>.bin (VICE `save` output;
|
||||
# 2-byte LE start-addr header + 64 KB RAM).
|
||||
# Writes: examples/spacetaxi/assets/tiles/tilebank<N>.png
|
||||
# examples/spacetaxi/assets/levels/level<NN>.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: <dump-path> <level-name-without-extension> <tilebank-id>
|
||||
#
|
||||
# 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 <char> <screencode> -- 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 <dump-path> <level-name> <tilebank-id>", 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()
|
||||
|
|
@ -983,7 +983,6 @@ int main(void) {
|
|||
#else
|
||||
config.codegenBytes = 160UL * 1024;
|
||||
#endif
|
||||
config.audioBytes = 32UL * 1024;
|
||||
if (!jlInit(&config)) {
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue