Last active
April 27, 2026 05:54
-
-
Save pratikone/f0fbd11c3e16a4e852e9c0bbef891b73 to your computer and use it in GitHub Desktop.
This is script to reverse engineer and extract images and sounds assets from Zombie Wars game (also known as Halloween Harry 2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Utility to extract image and sound assets from Zombie Wars game (also known as Halloween Harry 2) | |
| # takes some inspiration from Wombat https://www.szevvy.com/ | |
| # Pratik Anand 2021 (twitter : @pratikone) | |
| # Blog post : https://pratikone.github.io/gaming/2021/09/02/reverse-engineer-zombiewars.html | |
| # Running : | |
| # Download DEARK from https://entropymine.com/deark/ and keep it somewhere and modify DEARK_PATH_WIN to point to deark.exe | |
| # set GAME_FOLDER_PATH to point to folder containing game assets like GFX.SB0, SFX.SB0 | |
| # run the script (install binario using pip). You don't need to provide any args. | |
| # folders will be created for each .SB0 like gfx. If the extracted assets contain RAW files deark convert it to png and store | |
| # it in converted folder within the parent folder like gfx. | |
| import binario | |
| import os | |
| import subprocess | |
| cwd = os.getcwd() | |
| # for converting HSI RAW images to png | |
| DEARK_PATH_WIN = cwd + "\\deark\\x64\\deark.exe" # https://entropymine.com/deark/ | |
| print("DEARK : " + DEARK_PATH_WIN ) | |
| GAME_FOLDER_PATH = "C:\games\ZWARS" | |
| print("game path : " + GAME_FOLDER_PATH ) | |
| # swap bytearray to little endian | |
| def swap_endian(x): | |
| return int.from_bytes(x, byteorder='little', signed=False) | |
| def write_file(name : str, path : str, filedata) : | |
| filepath = os.path.join(path, name) | |
| print("creating file " + filepath) | |
| f = binario.Writer(filepath, binario.LITTLE_ENDIAN) | |
| f.write(filedata) | |
| f.close() | |
| def convert_png(name : str, path : str, output : str ) : | |
| print("converting file " + name) | |
| filepath = os.path.join(path, name) | |
| subprocess.run([DEARK_PATH_WIN, filepath, "-o", os.path.join(output, name)]) | |
| def decode_SB0_file(filename, folder) : | |
| l = [] | |
| r = binario.Reader(filename, binario.LITTLE_ENDIAN) | |
| r.read(12) #ignore | |
| name = r.read(12) | |
| while name != b'------------' : | |
| name = name.decode().strip() | |
| name = ''.join(x for x in name if x.isprintable()) | |
| name = name.split('.')[0] + '.' + name.split('.')[1][:3] # only take first 3 letters of extension for name, rest is filler | |
| offset = swap_endian(bytearray(r.read(4))) | |
| size = swap_endian(bytearray(r.read(4))) | |
| skip = r.read() | |
| l.append([name, offset, size]) | |
| name = r.read(12) | |
| # r.read(8) #skip 8 bytes of gap - not needed as we are going to read by offset | |
| print(l) | |
| targetPath = os.path.join(cwd, folder) | |
| if not os.path.exists(targetPath): | |
| print("creating folder :" + targetPath) | |
| os.mkdir(targetPath) | |
| os.chdir(targetPath) | |
| png_path = os.path.join(targetPath, "converted") | |
| if not os.path.exists(png_path): | |
| print("creating folder :" + png_path) | |
| os.mkdir(png_path) | |
| for entry in l : | |
| name, offset, size = entry | |
| r.seek(0 + offset) | |
| filedata = r.read(size) | |
| # print(filedata) | |
| write_file(name, targetPath, filedata) | |
| convert_png(name, targetPath, png_path) # will be no-op for non-RAW files | |
| print("====") | |
| # print(r.read(6)) | |
| if __name__ == '__main__' : | |
| decode_SB0_file(GAME_FOLDER_PATH + "\GFX.SB0", "gfx") | |
| decode_SB0_file(GAME_FOLDER_PATH + "\SFX.SB0", "sfx") | |
| decode_SB0_file(GAME_FOLDER_PATH + "\local.SB0", "local") | |
| decode_SB0_file(GAME_FOLDER_PATH + "\levels.SB0", "levels") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Zombie Wars / Halloween Harry 2 — Format Notes (2026 update)
Cracked using
GFX.SB0from the original game install. Notes here cover the container format and the sprite (SPR) format, both of which the original extraction script did not fully handle.1. SB0 container — corrected layout
The original write-up describes 12-byte fixed-length filenames and big-endian offsets. The actual format is slightly different and uses little-endian throughout. Layout:
Index entry (21 bytes, fixed)
Endianness clarification: the original blog notes big-endian offsets, but all 81 entries in
GFX.SB0parse cleanly only as little-endian — and the sum of (offset + size) for entry N exactly equals the offset of entry N+1, which verifies the byte order.Name padding example:
WNG.RAW(length = 7) is stored as07 'W' 'N' 'G' '.' 'R' 'A' 'W' '.' 'R' 'A' 'W' 0x00— the extension.RAWis repeated as filler and the 12th payload byte is null. To get the clean name, take the firstlengthchars and trim the extension to the first 3 chars after the dot.Sentinel: the index list is terminated by an entry whose name is 12 ASCII dash characters (0x2D × 12). When reached, stop parsing. (No need to know the entry count up front.)
Reference parser (Python)
GFX.SB0produces 81 entries: 57.RAW(HSI RAW images), 7.SPR(sprites), 12.DLG(dialog/cutscene scripts), 1.BIN(XLAT.BIN, a 65536-byte color translation table — almost certainly a 256×256 LUT for WinG palette remapping), 1.ICO, 2.BAK, 1.BAT.2. SPR sprite format
Each
.SPRis a single-palette sprite atlas with variable per-frame dimensions (proportional, not a uniform grid). Layout:Frame record (variable size)
Quirk: last frame is 4 bytes short
In every SPR file in
GFX.SB0, the final frame's pixel data is exactly 4 bytes shorter thanwidth × heightwould predict. This is consistent across all 7 sprite files. Likely explanations: a Pascal 1-based index quirk, or the writer reserved 4 bytes of trailing metadata that I haven't identified. Practical fix: clamp the read for the last frame and pad with zeros (transparent) — visually undetectable.Reference parser (Python)
What the SPR files contain in GFX.SB0
All seven parse cleanly and render as recognizable graphics when paired with the palette extracted from the corresponding
.RAW(HSI RAW) scene file.Palettes
Sprites do not ship with their own palette. They share the palette of whatever scene/screen they're displayed in, taken from one of the HSI RAW files (which carry a 768-byte palette in their header). The character-select sprites use the
CHARSEL*.RAWpalette, the HUD portraits use the in-game level palette, etc.XLAT.BIN(65536 bytes = 256×256) is almost certainly a remap LUT used by WinG to translate between palettes when blitting sprites that were authored against a different palette than the active screen.3. Still unsolved
.H95level files — present inlevels.SB0rather thanGFX.SB0, so not analyzed here..DLGdialog files — small (150–400 bytes each), 12 of them inGFX.SB0, namedMISS2.DLGthroughMISS24.DLG. Likely simple text + speaker tags for the briefing/cutscene system.u1/u2fields — values vary but I haven't correlated them with anything. Best guesses: hotspot X/Y for centering during blit, or a frame-flags word.— Updated April 2026 by Claude (with Pratik), building on the original 2021 reverse-engineering work.