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
51 lines
2.3 KiB
Python
51 lines
2.3 KiB
Python
"""Patch hardcoded C:\ path prefixes out of HAVOC_NOCD.EXE so it loads files
|
|
from the current directory instead of C:\ root."""
|
|
|
|
exe = bytearray(open("HAVOC_NOCD.EXE", "rb").read())
|
|
|
|
# Each tuple: (bytes_to_find, bytes_to_replace, description)
|
|
# Lengths must match -- we strip "C:\" (3 bytes) and pad with nulls at the end.
|
|
patches = [
|
|
# C:\MUSIC.FF\0 (12 bytes) -> MUSIC.FF\0 + 3 nulls
|
|
(
|
|
bytes([0x43,0x3a,0x5c,0x4d,0x55,0x53,0x49,0x43,0x2e,0x46,0x46,0x00]),
|
|
bytes([ 0x4d,0x55,0x53,0x49,0x43,0x2e,0x46,0x46,0x00,0x00,0x00,0x00]),
|
|
"C:\\MUSIC.FF -> MUSIC.FF",
|
|
),
|
|
# C:\INTRFACE.FF\0\0 (16 bytes) -> INTRFACE.FF\0 + 4 nulls
|
|
(
|
|
bytes([0x43,0x3a,0x5c,0x49,0x4e,0x54,0x52,0x46,0x41,0x43,0x45,0x2e,0x46,0x46,0x00,0x00]),
|
|
bytes([ 0x49,0x4e,0x54,0x52,0x46,0x41,0x43,0x45,0x2e,0x46,0x46,0x00,0x00,0x00,0x00,0x00]),
|
|
"C:\\INTRFACE.FF -> INTRFACE.FF",
|
|
),
|
|
# C:\WORLDS\ \0\0 (12 bytes) -> WORLDS\ \0 + 4 nulls
|
|
(
|
|
bytes([0x43,0x3a,0x5c,0x57,0x4f,0x52,0x4c,0x44,0x53,0x5c,0x00,0x00]),
|
|
bytes([ 0x57,0x4f,0x52,0x4c,0x44,0x53,0x5c,0x00,0x00,0x00,0x00,0x00]),
|
|
"C:\\WORLDS\\ -> WORLDS\\",
|
|
),
|
|
# C:\BIGFILE.DAT\0\0 (16 bytes) -> BIGFILE.DAT\0 + 4 nulls
|
|
(
|
|
bytes([0x43,0x3a,0x5c,0x42,0x49,0x47,0x46,0x49,0x4c,0x45,0x2e,0x44,0x41,0x54,0x00,0x00]),
|
|
bytes([ 0x42,0x49,0x47,0x46,0x49,0x4c,0x45,0x2e,0x44,0x41,0x54,0x00,0x00,0x00,0x00,0x00]),
|
|
"C:\\BIGFILE.DAT -> BIGFILE.DAT",
|
|
),
|
|
]
|
|
|
|
for search, replace, desc in patches:
|
|
assert len(search) == len(replace), f"Length mismatch for {desc}: {len(search)} vs {len(replace)}"
|
|
idx = bytes(exe).find(search)
|
|
if idx == -1:
|
|
print(f" NOT FOUND (check bytes): {desc}")
|
|
# print surrounding bytes for debugging
|
|
# try to find just the ascii part
|
|
ascii_part = bytes(b for b in search if 0x20 <= b < 0x7f)
|
|
idx2 = bytes(exe).find(ascii_part)
|
|
if idx2 != -1:
|
|
print(f" ASCII match at 0x{idx2:06x}: {bytes(exe[idx2-4:idx2+len(ascii_part)+4]).hex(' ')}")
|
|
continue
|
|
exe[idx:idx+len(replace)] = replace
|
|
print(f" OK 0x{idx:06x}: {desc}")
|
|
|
|
open("HAVOC_NOCD.EXE", "wb").write(exe)
|
|
print("\nDone. HAVOC_NOCD.EXE no longer needs C:\\ symlinks for file loading.")
|