""" 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()