havoc-remaster/tools/check_files.py
pyr0ball 27174a1c79 feat: NOCD patch boots Havoc on modern Windows (defeats CD copy protection)
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
2026-07-03 09:29:59 -07:00

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