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

158 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Havoc file formats (reverse-engineered)
All offsets/sizes verified against the retail data set (Reality Bytes, Inc., 1995).
## Binaries
| File | Type | Notes |
|---|---|---|
| `HAVOC.EXE` | PE32 i386, GUI | Imports `DDRAW`, `DSOUND`, `WINMM`, `WSOCK32`, `GDI32`, `USER32`, `KERNEL32`. **No Direct3D** — 3D is a software rasterizer drawing into a DirectDraw surface. Contains a CD-presence check ("...Extra Player CD..."). |
| `LAUNCHER.EXE` | PE32 i386, GUI | Front-end menu (see `havoc_9` screenshot). |
| `SETUP.EXE` | PE32 i386, GUI | Config/setup. |
| `DIRECTX/` | DX5-era | Ships 16-bit `DSETUP16.DLL`/`DDRAW16.DLL` — the 16-bit installer path is what breaks on 64-bit Windows, not the game itself. |
## `.FF` — "FlashFile" archive ✅ implemented in `tools/ff_extract.py`
Used by `MUSIC.FF`, `SOUND.FF`, `OBJECTS.FF`, `INTRFACE.FF`. Built by the bundled DOS tool
`FFCREATE.EXE` (found inside `MUSIC.FF`/`SOUND.FF`).
```
uint32 file_count (little-endian)
repeat file_count:
uint32 absolute_offset (little-endian, from start of .FF)
char[] name (NUL-terminated, variable length)
<concatenated payloads> entry size = next_offset - this_offset
```
Notes:
- The table may contain a trailing empty-name entry — skip it.
- Entry payloads are raw and self-describing (e.g. `RIFF`/`WAVE`, `MZ`, bitmaps).
### Archive contents
| Archive | Count | Entry kinds |
|---|---|---|
| `MUSIC.FF` | 116 | `WAVE<id>.DAT` PCM segments, `PLST<id>.DAT` playlists, `FFCREATE.EXE` |
| `SOUND.FF` | 65 | `WAVE<id>.DAT` sound effects, `FFCREATE.EXE` |
| `OBJECTS.FF` | 526 | `ASND<id>.DAT` (enemy/pickup models + object sounds — format TBD) |
| `INTRFACE.FF` | 547 | `BitB<id>.dat` headers, `BitR<id>.dat` rasters, `XPAL<id>.dat` palettes, `STR#<id>.dat` string tables, `RTbl<id>.dat` remap tables, `xpXl<id>.dat` palette variants |
## Music: `WAVE` segments + `PLST` playlists ✅
- `WAVE<id>.DAT` — standard `RIFF`/`WAVE`, PCM **mono, 22050 Hz, 8-bit**.
- `PLST<id>.DAT` — a song = an ordered sequence of WAVE segments. **Payload is big-endian**
(note: the `.FF` table around it is little-endian — mixed by design):
```
uint16 segment_count (big-endian; how many WAVE segments belong to this group)
uint16 step_count (big-endian)
uint16[step_count] sequence (big-endian; 1-based segment index)
```
Segment resolution: a playlist named `PLST<id>` has base id = `int(id, 16)`; **step value N
maps to wave id `base + N - 1`** (e.g. `PLST07D1` step 2 → `WAVE07D2`). Concatenating the
referenced segments' PCM reproduces the song exactly (verified by ear).
## `INTRFACE.FF` bitmap system ✅ implemented in `tools/ff_extract.py`
All entries share a 4-digit hex ID that links related assets (e.g. `797C` = main menu):
`BitB797C.dat` (dimensions) + `BitR797C.dat` (pixels) + `xpal797c.dat` (palette) + `STR#797C.dat` (strings).
### `BitB<id>.dat` — bitmap header (4 bytes)
```
uint16 width (big-endian)
uint16 height (big-endian)
```
### `BitR<id>.dat` — raw bitmap raster
Raw 8-bit palette-indexed pixels, row-major, `width * height` bytes total. No header.
### `XPAL<id>.dat` / `xpal<id>.dat` — palette (768 bytes)
256 RGB triplets, 8-bit per channel (NOT 6-bit VGA). One palette per screen/sprite group.
**Critical runtime patch:** the game engine always forces palette index 2 to `(0, 0, 0)` black
at runtime. The stored XPAL files may contain any value there (often `#e78484` salmon from the
development environment). Any renderer must override index 2 to black after loading. Index 0
and 1 are the transparency/key color (`#ff00ff` magenta in most palettes).
**Discovery method:** comparing `XPAL7A12` (correct black background) vs `XPAL7A1C` (wrong
salmon background) revealed only 3 differing entries out of 256 -- indices 0, 1, and 2.
### `STR#<id>.dat` — string table
Length-prefixed strings for the screen's UI labels. First byte = string length, then UTF-8
text. Example: `STR#797C` contains all main menu button labels and the copyright notice.
Two special-purpose string tables live in `INTRFACE.FF` at fixed IDs:
| ID | Content |
|---|---|
| `7D02` | Pickup banner text -- all item and weapon pickup notification strings |
| `7D03` | HUD active weapon name display strings |
See `docs/WEAPONS.md` for the complete weapon list derived from these tables.
### Identified screens (640x480 full-screen bitmaps)
| ID | Content (from STR# strings) |
|---|---|
| `797C` | Main menu |
| `79E0` | Network options |
| `79AE` | Load/save game |
| `7A44` | Settings menu |
| `7A4E` | Sound/music settings |
| `7A58` | Graphics settings |
| `7A62` | Keyboard settings |
| `7A6C` | Joystick settings |
| `7ADA` | Info/help |
| `7A12` | Network client/server status |
| `79F4` | Network client options |
| `79FE` | Network server options |
### Identified sprites
| Size | Count | Content |
|---|---|---|
| 640x480 | 32 | Full-screen menu/UI backgrounds |
| 512x236, 512x230, 512x218 | 3 | Cockpit windshield frames (color variants) |
| 428x196 | 12 | Vehicle selection screens with 3D-rendered thumbnails |
| 104x432 | 3 | Cockpit side panel strips |
| 420x46, 228x46 | 29 | Button/UI strip elements |
| 48x48 | 57 | Vehicle HUD icons (monochrome green, multiple vehicles/angles) |
| 18x18 | 13 | Small UI icons |
## World data (`WORLDS/`) — TODO (3D terrain)
Per-level sets `<NN>` = 01..06. Six worlds confirmed from EXE string table + player memory:
| File prefix | World name | Biome |
|---|---|---|
| `LAND01xx` | Badlands | Desert |
| `LAND02xx` | Fallout | Ice/Snow |
| `LAND03xx` | Tyrak | Alien |
| `LAND04xx` | Wasteland | Desert (harder) |
| `LAND05xx` | Malterra | Ice (harder) |
| `LAND06xx` | Overlord | Alien (harder) |
Level suffix codes within each world: `01`=Level 1, `02`=Level 2, `03`=Level 3, `04`=Boss, `0B`=Bonus.
Network variants use `0N`, `1N`, `2N` prefixes for the three biomes at Easy/Medium/Hard.
Music track ID is stored at **offset 18 as big-endian uint16** in each `LAND*.DAT` file.
| Pattern | Guess | Status |
|---|---|---|
| `MAP<lvl>.MAP` | tile/heightmap grid, 1 byte per cell | layout confirmed (byte grid); dims TBD |
| `LAND<lvl>.DAT` | terrain geometry/tile defs | TBD |
| `GRAFIX0N.DAT` | terrain/texture tilesets | TBD |
| `STUF<lvl>.DAT` | entity placement (contains `Item` records) | TBD |
## Other top-level data — TODO
| File | Notes |
|---|---|
| `BIGFILE.DAT` (64 MB) | starts `Copyright 1995, Reality Bytes, Inc.` then embedded `RIFF` data — bulk audio/stream pool |
| `STRDATA.DAT` | length-prefixed UI string table (`Out of memory.`, `Not enough disk space.`, …) |
| `HAVOC.ICO` | 32×32 4-bit icon |