havoc-remaster/docs/FORMATS.md
pyr0ball 198977ed2b feat: crack GRAFIX0N.DAT texture format + extractor
WORLDS/GRAFIX0N.DAT is a big-endian (Mac-origin) tileset: an 8-byte header
(file size at 0x08, entry count at 0x0c), 16-byte entry records (BE offset +
id), and per-graphic blocks with a BE width/height/frames/id header followed by
w*h*frames 8-bit palette indices. Confirmed against the in-game loader at
0x444db0 (16-byte record stride, BE->LE dword swap).

GRAFIX00.DAT decodes to 29 graphics: explosion (id 901, 8 frames), smoke
(id 911, 8 frames), and 27 single-frame 64x64 structure/wall textures
(ids 8001-8028). tools/grafix_extract.py renders them to PNG with any
768-byte RGB palette (the XPAL palettes from INTRFACE.FF work).

- tools/grafix_extract.py: parser + PNG renderer
- docs/FORMATS.md: GRAFIX0N.DAT spec
- README: status update

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-03 09:36:41 -07:00

192 lines
7.9 KiB
Markdown
Raw Permalink 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` | 64x64 texture tilesets | ✅ cracked, see below (`tools/grafix_extract.py`) |
| `STUF<lvl>.DAT` | entity placement (contains `Item` records) | TBD |
### `GRAFIX0N.DAT` — texture tileset ✅ implemented in `tools/grafix_extract.py`
Big-endian (Mac-origin data, byte-swapped by the in-game loader at VA `0x444db0`).
`GRAFIX00.DAT` = 29 graphics: an explosion sprite (id 901, 8 frames), a smoke sprite
(id 911, 8 frames), and 27 single-frame 64x64 structure/wall textures (ids 8001-8028:
metal panels, hazard stripes, warning bars, arrows, circuit patterns).
```
Header:
0x00 8 bytes reserved (0)
0x08 u32 BE total file size
0x0c u16 BE entry count N
0x20 N*16 entry records
Entry record (16 bytes):
0x00 u32 BE absolute file offset of the graphic block
0x04 u16 BE id
0x06 10 bytes reserved
Graphic block (at record offset):
0x00 u16 BE width (64)
0x02 u16 BE height (64)
0x04 u16 BE frame count
0x06 u16 BE id
0x08 8 bytes reserved
0x10 w*h*frames bytes 8-bit palette indices
```
Pixels are 8-bit indices into a 256-colour palette (768-byte RGB, e.g. the `XPAL`
palettes in `INTRFACE.FF`). Index 1 (magenta `ff 00 ff`) is the transparency key.
The exact world palette per tileset is still TBD; UI `XPAL0000` renders them plausibly.
```sh
python tools/grafix_extract.py WORLDS/GRAFIX00.DAT -p tools/out_intrface/raw/XPAL0000.DAT
```
## 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 |