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
39 lines
1.4 KiB
Python
39 lines
1.4 KiB
Python
import struct, sys, io, r2pipe
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
|
|
data = open('HAVOC_NOCD.EXE','rb').read()
|
|
FO2VA = lambda fo: fo + 0x400C00
|
|
VA2FO = lambda va: va - 0x400C00
|
|
|
|
# The string at STRDATA offset 0x8C is string index ~7 (counting from 0)
|
|
# Parse STRDATA.DAT to find the index of the "corrupt or unavailable" string
|
|
sd = open('STRDATA.DAT','rb').read()
|
|
idx = 0
|
|
str_idx = 0
|
|
while idx < len(sd):
|
|
length = sd[idx]
|
|
if length == 0:
|
|
idx += 1
|
|
continue
|
|
s = sd[idx+1:idx+1+length]
|
|
printable = ''.join(chr(b) if 32 <= b < 127 else '?' for b in s)
|
|
print("String[%d] len=%d: %s" % (str_idx, length, printable[:80]))
|
|
if b'corrupt' in s or b'unavail' in s:
|
|
print(" ^^^ THIS IS THE TARGET string index %d" % str_idx)
|
|
str_idx += 1
|
|
idx += 1 + length
|
|
|
|
print()
|
|
print("=== All hardcoded paths/filenames in binary ===")
|
|
# Search for drive letters and path patterns
|
|
for needle in [b'C:\\', b'D:\\', b'.FF', b'.DAT', b'WORLD', b'BIGFILE',
|
|
b'OBJECTS', b'SOUND.FF', b'CD_DATA', b'cd_data']:
|
|
fo = 0
|
|
while True:
|
|
pos = data.find(needle, fo)
|
|
if pos == -1: break
|
|
start = max(0, pos-8)
|
|
end = min(len(data), pos+40)
|
|
ctx = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[start:end])
|
|
print("FO 0x%05x VA 0x%08x: %s" % (pos, FO2VA(pos), ctx))
|
|
fo = pos + 1
|