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

36 lines
1.2 KiB
Python

"""Patch J: bypass the init-flag error gate at 0x020277.
The instruction at 0x020277 is JNZ +0x2e (75 2e). When the init flag at
VA 0x475425 is zero the fall-through path shows the 'corrupt files' error.
The jump target (0x0202a7) is the success return stub:
b0 01 MOV AL, 1
5d 5f 5e 5b POP EBP/EDI/ESI/EBX
83 c4 44 ADD ESP, 0x44
c2 0c 00 RET 0x0c
Changing 75->eb turns JNZ into JMP so execution always takes the success path.
"""
import struct
with open("HAVOC_NOCD.EXE", "rb") as f:
exe = bytearray(f.read())
PATCH_FO = 0x020277
# Verify expected bytes
expected = bytes([0x75, 0x2e])
actual = bytes(exe[PATCH_FO:PATCH_FO+2])
if actual != expected:
print(f"ERROR: expected {expected.hex(' ')} at 0x{PATCH_FO:06x}, got {actual.hex(' ')}")
raise SystemExit(1)
exe[PATCH_FO] = 0xeb # JNZ -> JMP
# Verify target bytes at 0x0202a7
target_bytes = bytes(exe[0x0202a7:0x0202a7+12])
print(f"Patch J target bytes at 0x0202a7: {target_bytes.hex(' ')}")
# should be: b0 01 5d 5f 5e 5b 83 c4 44 c2 0c 00
with open("HAVOC_NOCD.EXE", "wb") as f:
f.write(exe)
print(f"Patch J applied: 0x{PATCH_FO:06x}: 75 2e -> eb 2e (JNZ -> JMP, bypass init-flag check)")