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
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""Check what files the game needs and whether they exist, then show more code context."""
|
|
import re, struct, os
|
|
|
|
print("=== Files present in game directory ===")
|
|
game_dir = "."
|
|
for f in sorted(os.listdir(game_dir)):
|
|
full = os.path.join(game_dir, f)
|
|
if os.path.isfile(full):
|
|
print(f" {f:30s} {os.path.getsize(full):>10d} bytes")
|
|
elif os.path.isdir(full):
|
|
print(f" {f}/ [dir]")
|
|
|
|
print("\n=== Checking expected files ===")
|
|
needed = ['MUSIC.FF','INTRFACE.FF','BIGFILE.DAT','STRDATA.DAT','HAVOC.INI','WORLDS']
|
|
for n in needed:
|
|
exists = os.path.exists(n)
|
|
info = ""
|
|
if exists:
|
|
if os.path.isdir(n):
|
|
contents = os.listdir(n)
|
|
info = f" [dir, {len(contents)} files]"
|
|
else:
|
|
info = f" [{os.path.getsize(n)} bytes]"
|
|
print(f" {'OK' if exists else 'MISSING':<6} {n}{info}")
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
pe_off = struct.unpack_from('<I', exe, 0x3c)[0]
|
|
img_base = struct.unpack_from('<I', exe, pe_off + 52)[0]
|
|
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
|
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
|
secs = []
|
|
for i in range(num_secs):
|
|
s = pe_off + 24 + opt_sz + i*40
|
|
vsz = struct.unpack_from('<I', exe, s+8)[0]
|
|
vrva = struct.unpack_from('<I', exe, s+12)[0]
|
|
rsz = struct.unpack_from('<I', exe, s+16)[0]
|
|
roff = struct.unpack_from('<I', exe, s+20)[0]
|
|
secs.append((roff, rsz, vrva, vsz))
|
|
|
|
# Search for ALL C:\ paths and Z:\ paths still in the binary
|
|
print("\n=== Remaining hardcoded paths (C: or Z:) in binary ===")
|
|
data = exe[0x000400:0x070000]
|
|
for m in re.finditer(rb'[CcDdZz]:\\[A-Za-z0-9_.\\]{3,40}', data):
|
|
fo = 0x000400 + m.start()
|
|
print(f" 0x{fo:06x}: {m.group().decode('latin1', errors='replace')!r}")
|
|
|
|
# Show more code around the flag check at 0x020270 - look at 0x020200-0x020280
|
|
# Disassemble-like dump with computed CALL targets
|
|
print("\n=== Call targets in 0x020200-0x020280 ===")
|
|
for fo in range(0x020200, 0x020280):
|
|
b = exe[fo]
|
|
if b == 0xe8: # CALL rel32
|
|
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
|
next_fo = fo + 5
|
|
# VA of next instruction
|
|
next_va = img_base + 0x001000 + (next_fo - 0x000400)
|
|
target_va = next_va + rel
|
|
target_fo = 0x000400 + (target_va - img_base - 0x001000)
|
|
ctx = exe[max(0,fo-8):fo+8]
|
|
print(f" CALL at 0x{fo:06x} -> fo=0x{target_fo:06x} (VA=0x{target_va:08x}): ...{exe[fo-3:fo+6].hex()}...")
|
|
|
|
# Also look for ALL the path strings still in the binary
|
|
print("\n=== All path/filename strings in data section (0x069400-0x06EC00) ===")
|
|
data_sec = exe[0x069400:0x06ec00]
|
|
for m in re.finditer(rb'[A-Za-z0-9][A-Za-z0-9_. \\:]{2,39}(?=\x00)', data_sec):
|
|
s = m.group().decode('latin1', errors='replace')
|
|
if any(c in s for c in ['.', '\\', ':']):
|
|
fo = 0x069400 + m.start()
|
|
print(f" 0x{fo:06x}: {s!r}")
|