havoc-remaster/docs/PATCHES.md
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

5 KiB

HAVOC_NOCD.EXE — Binary Patches (run on modern Windows without CD)

HAVOC_NOCD.EXE is a patched copy of the original HAVOC.EXE (485,376 bytes, 32-bit PE i386, DirectDraw + DirectSound). The patches let it run from a hard-drive install on Windows 11 via dgVoodoo2 (DirectDraw wrapper, windowed mode), with no CD mounted and no admin rights.

Do not commit HAVOC_NOCD.EXE or the original HAVOC.EXE (copyright).

Section layout / file-offset (FO) to virtual-address (VA) mapping

Section Raw FO start RVA FO → VA formula
.text 0x00400 0x01000 VA = FO + 0x400C00
.data 0x69400 0x7C000 VA = FO + 0x412C00
.rdata 0x67000 0x79000 VA = FO - 0x67000 + 0x79000 + 0x400000
.idata 0x6EC00 0x82000 VA = FO + 0x413400

.text VirtualSize was extended from 0x66bbd to 0x66c00 (PE header FO 0x180) so the Patch-V code cave at VA 0x467bce is mapped into memory.

Patch table

FO VA Bytes (→) Purpose
0x00180 (PE hdr) bd6b006c Extend .text VirtualSize to cover Patch-V cave
0x094fd 0x40a0fd 66 b8 01 0066 33 c0 90 Patch S: CreateSoundBuffer fail returns AX=0 (continue w/o sound)
0x20277 0x420e77 75eb Force a Jcc branch (skip)
0x29613 0x42a213 8844242c90909090 Patch D: NOP drive-letter overwrite (INTRFACE)
0x29623 0x42a223 8844241090909090 Patch D: NOP drive-letter overwrite (MUSIC)
0x296cc 0x42a2cc 7ceb Patch Q: skip check
0x29b02 0x42a702 18 bytes → NOP Remove drive-type compare/branches
0x29b38 0x42a738 75eb Drive-check skip
0x29b80 0x42a780 74eb CD-check skip
0x29bb0 0x42a7b0 81ec6c010000b001c39090 90 NOCD stub = MOV AL,1; RET
0x29d63 0x42a963 75eb Extra CD skip
0x29e10 0x42aa10 74eb CD skip 2
0x29a2b 0x42a62b 8b442414 85c0 8b442410 752a 85c08b442410 85c0 754e 8b442414 eb48 Copy-protection / GRAFIX fix (see below)
0x2cd70 0x42d970 81c3 Patch N3: show_error_dialog returns immediately (silence popups)
0x2cf2e 0x42db2e 74eb 16 Corrupt/unavailable bypass
0x30393 0x430f93 e888c9ffff→NOP NOP a validation CALL
0x42514 0x443114 b8010000e9b54a02 Patch V: JMP to vtable-setter code cave
0x66fce 0x467bce cave Patch V cave: sets vtable=1, returns EAX=1
0x6b27c 0x47de7c C:\MUSIC.FFMUSIC.FF Strip drive prefix
0x6b288 0x47de88 C:\INTRFACE.FFINTRFACE.FF Strip drive prefix
0x6b2b8 0x47deb8 C:\WORLDS\WORLDS\ Strip drive prefix
0x6b390 0x47df90 C:\WORLDS\LAND0101.DATWORLDS\... Strip drive prefix
0x6b3a8 0x47dfa8 C:\HAVOCDAT.DATHAVOCDAT.DAT Strip drive prefix
0x6b3b8 0x47dfb8 C:\BIGFILE.DATBIGFILE.DAT Strip drive prefix

The copy-protection / GRAFIX crash (the "black window then closes" bug)

Symptom: dgVoodoo window appears for ~1 second, all black, then the process disappears.

Actual cause: an ACCESS_VIOLATION (NULL-pointer read) at VA 0x444E6A during init — found by parsing the WER minidump in %LOCALAPPDATA%\CrashDumps (the crash call stack shows 0x4340730x407CCB, i.e. inside WinMain → 0x434040 init).

The WORLDS\ data-file loader 0x42a480 opens each file (GRAFIX00-03.DAT, LAND*, MAP*, STUF*) from two locations: the local WORLDS\<name> path and a path built from the CD drive letter in HAVOC.INI ([SETUP] DRIVE=D:\). Its return logic returns a valid stream only when the local open fails and the CD-drive open succeeds — a copy-protection check that assumes data lives on the CD. With the files present locally and no D: drive, it returns NULL; the caller 0x444db0 (GRAFIX loader) dereferences that NULL without checking → crash.

Fix (FO 0x29a2b): rewrite the return decision to prefer the successfully-opened local object:

0x42a62b: mov eax,[esp+0x10]   ; local WORLDS\ stream object
0x42a62f: test eax,eax
0x42a631: jnz 0x42a681         ; if local open OK, return it
0x42a633: mov eax,[esp+0x14]   ; else return the drive object
0x42a637: jmp 0x42a681

After this patch the game runs (window title "HAVOC(tm) by Reality Bytes", responsive, main loop executing) instead of crashing.

Reproducing / testing

The crash is launch-environment dependent (it reproduces when launched normally but not under a debugger). Reliable native run:

:: run_probe.bat
cd /d Z:\Development\devl\Havoc
.\HAVOC_NOCD.EXE

Notes:

  • NoDefaultCurrentDirectoryInExePath may be set — invoke the exe with a .\ prefix.
  • Launching via MSYS/Git-Bash segfaults (exit 139) and does not produce a WER dump; use cmd.
  • Minidumps can be parsed with plain Python (no cdb/windbg needed) — see the exception stream (type 6) for the faulting EIP and the memory streams (5/9) to walk the stack.