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
192 lines
7.9 KiB
Markdown
192 lines
7.9 KiB
Markdown
# 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 |
|