Diagnosed and fixed the crash that killed HAVOC_NOCD.EXE ~1s after launch
(dgVoodoo window appears black, then the process exits). It was not a clean
exit: an ACCESS_VIOLATION (null-pointer read at VA 0x444E6A) during init.
Root cause is a copy-protection check in the WORLDS\ data-file loader
(0x42a480). It opens each file (GRAFIX/LAND/MAP/STUF) from both a local
"WORLDS\<name>" path and a CD-drive path (HAVOC.INI [SETUP] DRIVE=D:\), and
returns a valid stream only when the local open fails and the CD-drive open
succeeds. With files present locally and no D: drive it returned NULL, and the
GRAFIX loader (0x444db0) dereferenced that NULL without checking.
Fix: 14-byte patch at FO 0x29a2b rewrites the return decision to hand back the
successfully-opened local object. The game now boots to the title screen
("HAVOC(tm) by Reality Bytes"), responsive, main loop running.
- docs/PATCHES.md: full patch table (28 patches) + crash write-up
- tools/: RE + patching scripts (r2pipe disasm, minidump parser, ctypes
debugger, PE/IAT/import analysis)
- run_probe.bat / run_and_log.bat: reliable native launch for repro
- .gitignore: exclude CD images, Ghidra install/project, dgVoodoo, *.ini
Diagnosed via WER minidumps (%LOCALAPPDATA%\CrashDumps) parsed in pure Python
(no cdb/windbg available).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
"""
|
|
Parse ISO 9660 from a MODE1/2352 raw BIN file.
|
|
Each sector = 2352 bytes; user data starts at byte 16 (after sync+header).
|
|
"""
|
|
import struct, sys, os
|
|
|
|
BIN = sys.argv[1] if len(sys.argv) > 1 else r"Z:\Development\devl\Havoc\tmp_cd\Havoc (USA) (Game Disk).bin"
|
|
SECTOR = 2352
|
|
DATA_OFF = 16 # within each sector: sync(12) + header(4)
|
|
DATA_LEN = 2048
|
|
|
|
def read_sector(f, lba):
|
|
f.seek(lba * SECTOR + DATA_OFF)
|
|
return f.read(DATA_LEN)
|
|
|
|
def read_u16_le(b, off): return struct.unpack_from('<H', b, off)[0]
|
|
def read_u32_le(b, off): return struct.unpack_from('<I', b, off)[0]
|
|
def read_u32_both(b, off): return struct.unpack_from('<I', b, off)[0] # LE half
|
|
|
|
def parse_dir(f, lba, size, indent=0, entries=None):
|
|
if entries is None:
|
|
entries = []
|
|
data = b''
|
|
remaining = size
|
|
cur_lba = lba
|
|
while remaining > 0:
|
|
chunk = read_sector(f, cur_lba)
|
|
data += chunk[:min(DATA_LEN, remaining)]
|
|
remaining -= DATA_LEN
|
|
cur_lba += 1
|
|
|
|
off = 0
|
|
while off < len(data):
|
|
reclen = data[off]
|
|
if reclen == 0:
|
|
off = (off + 1 + 1) & ~1 # skip padding; advance
|
|
if off >= len(data): break
|
|
continue
|
|
flags = data[off + 25]
|
|
name_len = data[off + 32]
|
|
name_raw = data[off+33:off+33+name_len]
|
|
# Strip version suffix ;1
|
|
name = name_raw.decode('ascii', errors='replace').split(';')[0].rstrip('\x00')
|
|
file_lba = read_u32_both(data, off + 2)
|
|
file_size = read_u32_both(data, off + 10)
|
|
is_dir = bool(flags & 0x02)
|
|
if name not in ('', '\x01'):
|
|
entries.append({'name': name, 'lba': file_lba, 'size': file_size,
|
|
'is_dir': is_dir, 'indent': indent})
|
|
if is_dir:
|
|
parse_dir(f, file_lba, file_size, indent+1, entries)
|
|
off += reclen
|
|
return entries
|
|
|
|
with open(BIN, 'rb') as f:
|
|
# Check for ISO 9660 PVD at sector 16
|
|
pvd = read_sector(f, 16)
|
|
if pvd[1:6] != b'CD001':
|
|
print(f"No ISO 9660 PVD at sector 16. Got: {pvd[0:8].hex(' ')}")
|
|
# Try sector 16 with different offset (some TOAST BINs have ISO 9660 elsewhere)
|
|
# Check volume descriptor sequence starting at 16
|
|
for lba in [16, 17, 18, 32, 64]:
|
|
s = read_sector(f, lba)
|
|
print(f" Sector {lba}: type={s[0]} sig={s[1:6]} system_id={s[8:40].rstrip()}")
|
|
sys.exit(1)
|
|
|
|
vol_type = pvd[0] # 1 = PVD
|
|
system_id = pvd[8:40].decode('ascii', errors='replace').strip()
|
|
vol_id = pvd[40:72].decode('ascii', errors='replace').strip()
|
|
vol_space = read_u32_both(pvd, 80)
|
|
print(f"ISO 9660 PVD found:")
|
|
print(f" System ID: {system_id!r}")
|
|
print(f" Volume ID: {vol_id!r}")
|
|
print(f" Vol sectors:{vol_space}")
|
|
|
|
root_dir_lba = read_u32_both(pvd, 156 + 2)
|
|
root_dir_size = read_u32_both(pvd, 156 + 10)
|
|
print(f" Root dir LBA={root_dir_lba} size={root_dir_size}")
|
|
print()
|
|
|
|
entries = parse_dir(f, root_dir_lba, root_dir_size)
|
|
|
|
print(f"Files ({len(entries)} entries):")
|
|
for e in entries:
|
|
prefix = ' ' * e['indent']
|
|
tag = '[DIR]' if e['is_dir'] else f"{e['size']:>10}"
|
|
print(f" {prefix}{e['name']:<30} {tag}")
|
|
|
|
print()
|
|
# Look for specific files we need
|
|
targets = ['HAVOCDAT.DAT', 'KEYSET.DAT', 'BIGFILE.DAT', 'GAMIMAGE.DAT']
|
|
print("Target file search:")
|
|
for e in entries:
|
|
if e['name'].upper() in targets:
|
|
print(f" FOUND: {e['name']} at LBA {e['lba']} size {e['size']}")
|