havoc-remaster/tools/trace_init.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

48 lines
1.5 KiB
Python

import re, struct
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]
print(f"ImageBase: 0x{img_base:08x}")
# The global byte tested before 'PUSH 6 -> corrupt error'
# Instruction at 0x020270: a0 25 54 47 00 = MOV AL, [0x00475425]
flag_va = 0x00475425
flag_needle = struct.pack('<I', flag_va)
print(f"\n=== All accesses to flag byte at VA=0x{flag_va:08x} ===")
for m in re.finditer(re.escape(flag_needle), exe[0x400:0x067000]):
off = 0x400 + m.start()
ctx = exe[max(0x400, off-4):off+6]
print(f" 0x{off-4:06x}: {ctx.hex(' ')}")
# String VAs with correct ImageBase
rdata_vRVA = 0x079000
rdata_raw = 0x067000
def rdata_va(fo):
return img_base + rdata_vRVA + (fo - rdata_raw)
string_fos = {
'MUSIC.FF': 0x06b27c,
'INTRFACE.FF': 0x06b288,
'OBJECTS.FF': 0x06b298,
'SOUND.FF': 0x06b2a4,
'BIGFILE.DAT': 0x06b3b8,
'STRDATA.DAT': 0x06bee8,
'WORLDS_rel': 0x06b2b0,
'WORLDS_abs': 0x06b2b8,
'HAVOCDAT': 0x06b3a8,
'GAMIMAGE': 0x06b980,
}
print(f"\n=== String VAs and code references ===")
for label, fo in string_fos.items():
va = rdata_va(fo)
needle = struct.pack('<I', va)
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
print(f" {label:<15} VA=0x{va:08x} refs={len(refs)}")
for r in refs[:3]:
ctx = exe[max(0x400,r-6):r+8]
print(f" 0x{r:06x}: {ctx.hex(' ')}")