Files
WolfensteinVB/graphics/extract_vb_doom.py
2026-02-19 23:28:57 +01:00

484 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
extract_vb_doom.py
Parse vb_doom.c arrays, extract graphics into separate C files:
- face_sprites.c/h (3 face expressions, 3x4 tiles each)
- fist_sprites.c/h (4 fist frames)
- pistol_sprites.c/h (pistol red + black, 2 frames each)
- vb_doom.c (HUD-only: UI bar, numbers, transitions)
Also computes the new VRAM layout and prints updated CHAR_START values.
"""
import re, os, sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
VB_DOOM_SOURCE = os.path.join(SCRIPT_DIR, "grit_conversions", "vb_doom.c")
VB_DOOM_FALLBACK = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images", "vb_doom.c")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images")
MAP_COLS = 48
BYTES_PER_ROW = MAP_COLS * 2 # 96
# ---------------------------------------------------------------------------
# Parsing
# ---------------------------------------------------------------------------
def parse_hex_array(filepath, array_name):
"""Parse a C hex array, return list of ints."""
with open(filepath, 'r') as f:
text = f.read()
pattern = rf'{re.escape(array_name)}\[\d+\].*?=\s*\{{(.*?)\}};'
m = re.search(pattern, text, re.DOTALL)
if not m:
raise ValueError(f"Array '{array_name}' not found in {filepath}")
return [int(x, 16) for x in re.findall(r'0x[0-9A-Fa-f]+', m.group(1))]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def char_of(entry):
"""Char index from a BGMap u16 entry (bits 0-10)."""
return entry & 0x07FF
def remap(entry, new_char):
"""Replace char bits of a BGMap u16 entry, keep flags/palette."""
return (entry & 0xF800) | (new_char & 0x07FF)
def tile_words(tiles_u32, char_idx):
"""Return the 4 u32 words for one 8x8 tile."""
o = char_idx * 4
return tiles_u32[o:o + 4]
def collect_chars(map_u16, regions):
"""Collect unique non-zero char indices from (byte_off, nbytes) regions."""
chars = set()
for boff, nbytes in regions:
for i in range(0, nbytes, 2):
idx = (boff + i) // 2
if idx < len(map_u16):
c = char_of(map_u16[idx])
if c != 0:
chars.add(c)
return chars
def collect_entries(map_u16, regions):
"""Collect map entries in order from (byte_off, nbytes) regions."""
entries = []
for boff, nbytes in regions:
for i in range(0, nbytes, 2):
idx = (boff + i) // 2
entries.append(map_u16[idx] if idx < len(map_u16) else 0)
return entries
# ---------------------------------------------------------------------------
# C code generation
# ---------------------------------------------------------------------------
def fmt_tiles(name, data_u32, comment=""):
"""Generate C tile array."""
lines = []
if comment:
lines.append(f"/* {comment} */")
lines.append(f"const unsigned int {name}[{len(data_u32)}]"
f" __attribute__((aligned(4))) =")
lines.append("{")
for i in range(0, len(data_u32), 8):
chunk = data_u32[i:i + 8]
s = ",".join(f"0x{v:08X}" for v in chunk)
comma = "," if i + 8 < len(data_u32) else ""
lines.append(f"\t{s}{comma}")
lines.append("};")
return "\n".join(lines)
def fmt_map(name, data_u16, comment=""):
"""Generate C map array."""
lines = []
if comment:
lines.append(f"/* {comment} */")
lines.append(f"const unsigned short {name}[{len(data_u16)}]"
f" __attribute__((aligned(4))) =")
lines.append("{")
for i in range(0, len(data_u16), 8):
chunk = data_u16[i:i + 8]
s = ",".join(f"0x{v:04X}" for v in chunk)
comma = "," if i + 8 < len(data_u16) else ""
lines.append(f"\t{s}{comma}")
lines.append("};")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
vb_doom_c = VB_DOOM_SOURCE if os.path.isfile(VB_DOOM_SOURCE) else VB_DOOM_FALLBACK
print(f"Using source: {vb_doom_c}")
tiles_u32 = parse_hex_array(vb_doom_c, "vb_doomTiles")
map_u16 = parse_hex_array(vb_doom_c, "vb_doomMap")
num_tiles = len(tiles_u32) // 4
print(f"Parsed {num_tiles} tiles, {len(map_u16)} map entries")
# -----------------------------------------------------------------------
# 1. Define regions (byte_offset, num_bytes) into vb_doomMap
# -----------------------------------------------------------------------
# -- HUD --
hud_regions = []
for r in range(4): # UI bar rows 0-3
hud_regions.append((r * 96, 96))
hud_regions.append((4 * 96, 2)) # black tile (row 4 col 0)
hud_regions.append((16 * 96, 46)) # large nums row 16 (cols 0-22, incl ":")
hud_regions.append((17 * 96, 46)) # large nums row 17 (cols 0-22, incl ":")
hud_regions.append((18 * 96, 22)) # small nums row 18 (normal digits 0-9)
hud_regions.append((19 * 96, 20)) # bright digits 0-9 (row 19)
hud_regions.append((20 * 96, 20)) # rectangled digits 0-9 (row 20)
for r in range(20, 28): # transition tiles rows 20-27
hud_regions.append((r * 96 + 31 * 2, 8)) # cols 31-34
hud_regions.append((25 * 96 + 39 * 2, 4)) # wall tiles row 25 cols 39-40
# -- Faces (3 faces × 4 rows × 3 entries) --
face_regions = []
for face_id in range(3):
wf = face_id * 384
for rb in [400, 496, 592, 688]:
face_regions.append((rb + wf, 6))
# -- Fists (4 frames) --
fist_fdefs = [
(96 * 22, 30, 6), # frame 0 idle
(96 * 22 + 30, 22, 6), # frame 1 attack 1
(96 * 13 + 48, 28, 7), # frame 2 attack 2
(96 * 28, 34, 8), # frame 3 attack 3
]
fist_per_frame = []
for sp, xt, nr in fist_fdefs:
fist_per_frame.append([(sp + r * 96, xt) for r in range(nr)])
# -- Pistol red (2 frames) --
pist_fdefs = [
(96 * 9, 16, 7), # idle
(96 * 5 + 22, 16, 11), # shooting
]
pist_per_frame = []
for sp, xt, nr in pist_fdefs:
pist_per_frame.append([(sp + r * 96, xt) for r in range(nr)])
# -- Pistol black (2 frames) --
pistb_fdefs = [
(96 * 28 + 34, 16, 8), # idle black
(96 * 28 + 50, 16, 8), # shooting black
]
pistb_per_frame = []
for sp, xt, nr in pistb_fdefs:
pistb_per_frame.append([(sp + r * 96, xt) for r in range(nr)])
# -----------------------------------------------------------------------
# 2. Collect unique chars per group
# -----------------------------------------------------------------------
hud_chars = collect_chars(map_u16, hud_regions) | {0}
face_chars = collect_chars(map_u16, face_regions)
fist_chars = set()
for fr in fist_per_frame:
fist_chars |= collect_chars(map_u16, fr)
pist_chars = set()
for fr in pist_per_frame:
pist_chars |= collect_chars(map_u16, fr)
pistb_chars = set()
for fr in pistb_per_frame:
pistb_chars |= collect_chars(map_u16, fr)
pistol_all = pist_chars | pistb_chars
weapon_max = max(len(fist_chars), len(pistol_all))
H = len(hud_chars)
F = len(face_chars)
W = weapon_max
# Use the ACTUAL game VRAM layout char starts.
# Faces use 12 dynamic chars (from sprites/faces/face_sprites.h).
# The large gap between faces and weapons is intentional.
FACE_CS = H # face chars immediately after HUD
WEAP_CS = 544 # game's WEAPON_CHAR_START (doomgfx.h)
ZOMBIE_CS = 983 # game's ZOMBIE_CHAR_START (doomgfx.h)
PICKUP_CS = ZOMBIE_CS + 5 * 64 # 1303 (5 enemy slots x 64 chars each)
WALL_CS = 1327 # game's WALL_TEX_CHAR_START (wall_textures.h)
print(f"\nChars per group: HUD={H} Face={F} Weapon={W}"
f" (fists={len(fist_chars)}, pistol={len(pistol_all)})")
print(f"\nNew VRAM layout:")
print(f" HUD 0 - {H-1}")
print(f" Faces {FACE_CS} - {FACE_CS+F-1}")
print(f" Weapon {WEAP_CS} - {WEAP_CS+W-1}")
print(f" Enemies {ZOMBIE_CS} - {ZOMBIE_CS+191}")
print(f" Pickups {PICKUP_CS} - {PICKUP_CS+35}")
print(f" Wall textures {WALL_CS} - {WALL_CS+511}")
after_walls = WALL_CS + 512
print(f" After walls {after_walls} (free: {2048-after_walls})")
# -----------------------------------------------------------------------
# 3. Build remappings old_char -> new_char
# -----------------------------------------------------------------------
hud_sorted = sorted(hud_chars)
hud_rm = {old: new for new, old in enumerate(hud_sorted)}
face_sorted = sorted(face_chars)
face_rm = {0: 0}
for i, c in enumerate(face_sorted):
face_rm[c] = FACE_CS + i
fist_sorted = sorted(fist_chars)
fist_rm = {0: 0}
for i, c in enumerate(fist_sorted):
fist_rm[c] = WEAP_CS + i
pistol_sorted = sorted(pistol_all)
pistol_rm = {0: 0}
for i, c in enumerate(pistol_sorted):
pistol_rm[c] = WEAP_CS + i
# -----------------------------------------------------------------------
# 4. Generate HUD-only vb_doom.c
# -----------------------------------------------------------------------
hud_tile_data = []
for c in hud_sorted:
hud_tile_data.extend(tile_words(tiles_u32, c))
hud_map_out = [0] * len(map_u16)
for boff, nbytes in hud_regions:
for i in range(0, nbytes, 2):
idx = (boff + i) // 2
if idx < len(map_u16):
e = map_u16[idx]
c = char_of(e)
if c in hud_rm:
hud_map_out[idx] = remap(e, hud_rm[c])
# Zero weapon slot positions (cols 17-19, rows 1-2) - drawn dynamically by drawWeaponSlotNumbers
for row in (1, 2):
for col in (17, 18, 19):
idx = row * MAP_COLS + col
hud_map_out[idx] = 0
# Zero dynamic ammo positions (cols 40-42, rows 0-3) so static placeholders
# don't show before drawUpdatedAmmo writes the real digits there
for row in range(4):
for col in (40, 41, 42):
hud_map_out[row * MAP_COLS + col] = 0
hud_c_path = os.path.join(OUTPUT_DIR, "vb_doom.c")
with open(hud_c_path, 'w') as f:
f.write("/*\n")
f.write(" * vb_doom -- HUD-only tileset (auto-generated by extract_vb_doom.py)\n")
f.write(f" * {H} tiles, {H*16} bytes tile data\n")
f.write(f" * {len(hud_map_out)} map entries (48x46), non-HUD entries zeroed\n")
f.write(" */\n\n")
f.write(fmt_tiles("vb_doomTiles", hud_tile_data,
f"{H} HUD tiles"))
f.write("\n\n")
f.write(fmt_map("vb_doomMap", hud_map_out,
"48x46 map -- HUD entries only"))
f.write("\n")
print(f"\nWrote {hud_c_path} ({H} tiles, {H*16} bytes)")
# -----------------------------------------------------------------------
# 5. Generate face_sprites.c/h
# -----------------------------------------------------------------------
face_tile_data = []
for c in face_sorted:
face_tile_data.extend(tile_words(tiles_u32, c))
# Compact face map: 3 faces × 4 rows × 3 entries = 36 entries
face_map_out = []
for face_id in range(3):
wf = face_id * 384
for rb in [400, 496, 592, 688]:
for i in range(0, 6, 2):
idx = (rb + wf + i) // 2
e = map_u16[idx]
c = char_of(e)
face_map_out.append(remap(e, face_rm.get(c, 0)) if c in face_rm else 0)
face_c = os.path.join(OUTPUT_DIR, "face_sprites.c")
with open(face_c, 'w') as f:
f.write("/* Face sprite tiles -- auto-generated by extract_vb_doom.py */\n\n")
f.write(fmt_tiles("faceTiles", face_tile_data, f"{F} face tiles"))
f.write("\n\n")
f.write(fmt_map("faceMap", face_map_out,
"3 faces x 4 rows x 3 cols = 36 entries"))
f.write("\n")
face_h = os.path.join(OUTPUT_DIR, "face_sprites.h")
with open(face_h, 'w') as f:
f.write("#ifndef __FACE_SPRITES_H__\n#define __FACE_SPRITES_H__\n\n")
f.write(f"#define FACE_CHAR_START {FACE_CS}\n")
f.write(f"#define FACE_TILE_COUNT {F}\n")
f.write(f"#define FACE_TILE_BYTES {F*16}\n")
f.write(f"#define FACE_MAP_ENTRIES 36 /* 3 faces x 12 tiles */\n\n")
f.write(f"extern const unsigned int faceTiles[{len(face_tile_data)}];\n")
f.write(f"extern const unsigned short faceMap[{len(face_map_out)}];\n\n")
f.write("#endif\n")
print(f"Wrote {face_c} ({F} tiles)")
# -----------------------------------------------------------------------
# 6. Generate fist_sprites.c/h
# -----------------------------------------------------------------------
fist_tile_data = []
for c in fist_sorted:
fist_tile_data.extend(tile_words(tiles_u32, c))
fist_frame_maps = []
fist_frame_sizes = [] # (width_entries, rows)
for fi, (sp, xt, nr) in enumerate(fist_fdefs):
w_entries = xt // 2
fmap = []
for r in range(nr):
for i in range(0, xt, 2):
idx = (sp + r * 96 + i) // 2
e = map_u16[idx]
c = char_of(e)
fmap.append(remap(e, fist_rm.get(c, 0)) if c in fist_rm else 0)
fist_frame_maps.append(fmap)
fist_frame_sizes.append((w_entries, nr))
fist_c = os.path.join(OUTPUT_DIR, "fist_sprites.c")
with open(fist_c, 'w') as f:
f.write("/* Fist sprite tiles -- auto-generated by extract_vb_doom.py */\n\n")
f.write(fmt_tiles("fistTiles", fist_tile_data,
f"{len(fist_sorted)} fist tiles"))
for fi, fm in enumerate(fist_frame_maps):
f.write("\n\n")
we, nr = fist_frame_sizes[fi]
f.write(fmt_map(f"fistFrame{fi}Map", fm,
f"frame {fi}: {we} cols x {nr} rows"))
f.write("\n")
fist_h = os.path.join(OUTPUT_DIR, "fist_sprites.h")
with open(fist_h, 'w') as f:
f.write("#ifndef __FIST_SPRITES_H__\n#define __FIST_SPRITES_H__\n\n")
f.write(f"#define FIST_TILE_COUNT {len(fist_sorted)}\n")
f.write(f"#define FIST_TILE_BYTES {len(fist_sorted)*16}\n\n")
f.write(f"extern const unsigned int fistTiles[{len(fist_tile_data)}];\n\n")
f.write(f"#define FIST_FRAME_COUNT 4\n")
for fi, (we, nr) in enumerate(fist_frame_sizes):
xt = we * 2
f.write(f"#define FIST_F{fi}_XTILES {xt}\n")
f.write(f"#define FIST_F{fi}_ROWS {nr}\n")
f.write(f"extern const unsigned short fistFrame{fi}Map[{len(fist_frame_maps[fi])}];\n")
f.write("\n#endif\n")
print(f"Wrote {fist_c} ({len(fist_sorted)} tiles)")
# -----------------------------------------------------------------------
# 7. Generate pistol_sprites.c/h (red + black combined tile data)
# -----------------------------------------------------------------------
pistol_tile_data = []
for c in pistol_sorted:
pistol_tile_data.extend(tile_words(tiles_u32, c))
# Red frames
pist_frame_maps = []
pist_frame_sizes = []
for fi, (sp, xt, nr) in enumerate(pist_fdefs):
we = xt // 2
fmap = []
for r in range(nr):
for i in range(0, xt, 2):
idx = (sp + r * 96 + i) // 2
e = map_u16[idx]
c = char_of(e)
fmap.append(remap(e, pistol_rm.get(c, 0)) if c in pistol_rm else 0)
pist_frame_maps.append(fmap)
pist_frame_sizes.append((we, nr))
# Black frames
pistb_frame_maps = []
pistb_frame_sizes = []
for fi, (sp, xt, nr) in enumerate(pistb_fdefs):
we = xt // 2
fmap = []
for r in range(nr):
for i in range(0, xt, 2):
idx = (sp + r * 96 + i) // 2
e = map_u16[idx]
c = char_of(e)
fmap.append(remap(e, pistol_rm.get(c, 0)) if c in pistol_rm else 0)
pistb_frame_maps.append(fmap)
pistb_frame_sizes.append((we, nr))
pist_c = os.path.join(OUTPUT_DIR, "pistol_sprites.c")
with open(pist_c, 'w') as f:
f.write("/* Pistol sprite tiles (red + black) -- auto-generated */\n\n")
f.write(fmt_tiles("pistolTiles", pistol_tile_data,
f"{len(pistol_sorted)} pistol tiles (red+black)"))
# Red frame maps
for fi, fm in enumerate(pist_frame_maps):
f.write("\n\n")
we, nr = pist_frame_sizes[fi]
f.write(fmt_map(f"pistolRedFrame{fi}Map", fm,
f"red frame {fi}: {we} cols x {nr} rows"))
# Black frame maps
for fi, fm in enumerate(pistb_frame_maps):
f.write("\n\n")
we, nr = pistb_frame_sizes[fi]
f.write(fmt_map(f"pistolBlackFrame{fi}Map", fm,
f"black frame {fi}: {we} cols x {nr} rows"))
f.write("\n")
pist_h = os.path.join(OUTPUT_DIR, "pistol_sprites.h")
with open(pist_h, 'w') as f:
f.write("#ifndef __PISTOL_SPRITES_H__\n#define __PISTOL_SPRITES_H__\n\n")
f.write(f"#define PISTOL_TILE_COUNT {len(pistol_sorted)}\n")
f.write(f"#define PISTOL_TILE_BYTES {len(pistol_sorted)*16}\n\n")
f.write(f"extern const unsigned int pistolTiles[{len(pistol_tile_data)}];\n\n")
f.write("/* Red layer frames */\n")
for fi, (we, nr) in enumerate(pist_frame_sizes):
xt = we * 2
f.write(f"#define PISTOL_RED_F{fi}_XTILES {xt}\n")
f.write(f"#define PISTOL_RED_F{fi}_ROWS {nr}\n")
f.write(f"extern const unsigned short pistolRedFrame{fi}Map"
f"[{len(pist_frame_maps[fi])}];\n")
f.write("\n/* Black layer frames */\n")
for fi, (we, nr) in enumerate(pistb_frame_sizes):
xt = we * 2
f.write(f"#define PISTOL_BLK_F{fi}_XTILES {xt}\n")
f.write(f"#define PISTOL_BLK_F{fi}_ROWS {nr}\n")
f.write(f"extern const unsigned short pistolBlackFrame{fi}Map"
f"[{len(pistb_frame_maps[fi])}];\n")
f.write("\n#endif\n")
print(f"Wrote {pist_c} ({len(pistol_sorted)} tiles)")
# -----------------------------------------------------------------------
# 8. Print summary of defines to update in C headers
# -----------------------------------------------------------------------
print("\n" + "=" * 60)
print("UPDATE THESE DEFINES IN C HEADERS:")
print("=" * 60)
print(f" doomgfx.h:")
print(f" #define ZOMBIE_CHAR_START {ZOMBIE_CS}")
print(f" #define WEAPON_CHAR_START {WEAP_CS}")
print(f" pickup.h (or doomgfx.h):")
print(f" #define PICKUP_CHAR_START {PICKUP_CS}")
print(f" wall_textures.h:")
print(f" #define WALL_TEX_CHAR_START {WALL_CS}")
print(f" doomgfx.c loadDoomGfxToMem:")
print(f" copymem size = {H * 16} (was 14224)")
print("=" * 60)
if __name__ == "__main__":
main()