Some very interesting info about GBA Doom 2 once again.
About nearly a decade ago I reverse engineered some of GBA Doom 2, but there were a couple mysteries I never quite figured out.
kippykip.com
* Maps (which was later cracked by Randy87)
* Compressed graphics
* Fonts
* Audio
Claude once again, was able to figure out a lot more details since it can easily reverse engineer code itself. Turns out there's some kind of Torus "DPAK" file, similar to a WAD.
It was able to convert the music format back into a tracker module.
Here's the notes:
A bit more info on this "DPAK" format:
Anyway, I've attached the converted modules.
They're compatible in OpenMPT and Milkytracker. Enjoy!
About nearly a decade ago I reverse engineered some of GBA Doom 2, but there were a couple mysteries I never quite figured out.
GBA Doom II Modding Tools! - 0.2 Beta
Well the wait is finally over! I present to you the GBA Doom II Modding Tools! The tools are programmed in Blitzmax (because it's the programming language I know best). I have to give credit to Magicteam for handing me their custom tool they used for making the translation patch. Despite that...
* Compressed graphics
* Fonts
* Audio
Claude once again, was able to figure out a lot more details since it can easily reverse engineer code itself. Turns out there's some kind of Torus "DPAK" file, similar to a WAD.
It was able to convert the music format back into a tracker module.
Here's the notes:
Code:
GBA Doom II - Music & Sound Format Notes
=========================================
Reverse engineered from DoomII_Recomp.gba (the player code lives at 0x0802F000-0x08031900).
All offsets below are file offsets into DoomII_Recomp.gba unless stated otherwise.
All values are little endian.
Short version
-------------
The music is ProTracker MOD music. Torus converted the original MODs into their own packed
container and plays them with a ProTracker-style replayer (same Amiga period table, same effect numbers, same speed/tick model).
That makes it a near 1:1 fit for an .XM module.
1. Where it lives: the "DPAK" archive
-------------------------------------
At 0x37CA4 there is a resource directory:
0x00 4 bytes "DPAK"
0x04 2 bytes 0x0040 (unknown, maybe version or max entries)
0x06 10 bytes "Torus" + zero padding
0x10 entries, 16 bytes each, 66 of them:
4 bytes type
4 bytes offset (relative to the start of "DPAK")
4 bytes size
4 bytes always 0
Types seen: 1 = small table, 2 = misc tables, 3/4 = game data, 5 = 56 level/graphic chunks,
6 = big graphics block (0xCE9938 bytes), 7 = MUSIC ("MUSC" chunk, 0x234DAC bytes).
The type 7 entry points at 0xDC1AB8 in this ROM. That is the music chunk.
2. The MUSC chunk (starts at 0xDC1AB8)
--------------------------------------
0x00 4 bytes "MUSC"
0x04 4 bytes offset to the SONG TABLE (0x0F27C4) - relative to "MUSC"
0x08 4 bytes offset to the MUSIC SAMPLE BANK (0x0F210C) - relative to "MUSC"
0x0C 4 bytes offset to the SFX SAMPLE BANK (0x16CFF8) - relative to "MUSC"
0x10 ... raw music sample data (8-bit signed PCM) up to the music sample bank.
The game checks the "MUSC" magic before playing (code at 0x0802FD52 / 0x0802FDD2).
3. Sample banks (music bank and SFX bank use the same layout)
-------------------------------------------------------------
4 bytes count (SFX bank: 98 = exact entry count. Music bank: says 120, but 143 entries
are actually present up to the song table and songs use instruments 1-143.)
then 12-byte entries, one per instrument:
0x00 int32 (offset << 8) | flags
- offset = signed (value >> 8), RELATIVE TO THIS ENTRY'S OWN ADDRESS,
points at 8-bit signed PCM.
- low byte: loop count. Music = 0 (normal), SFX = 1 (play once).
0x04 uint16 sample length in WORDS (x2 = bytes) <- same as MOD
0x06 uint8 finetune (0-15, MOD style: 8-15 = -8..-1) <- same as MOD
0x07 uint8 default volume (0-64) <- same as MOD
0x08 uint16 loop start in WORDS <- same as MOD
0x0A uint16 loop length in WORDS, 0 = no loop (MOD uses 1 for "no loop")
Music bank: 134 of the 143 slots are used, 9 are empty (all zero). Several instruments share
the same sample data. 46 instruments loop.
Sample rate: same as the original Amiga MOD. The player's pitch maths (see section 6)
plays period 428 (ProTracker C-2) at ~8312 Hz, Amiga PAL is ~8287 Hz. The old "12000hz?"
guess in Documentation.txt was the right ballpark; the real rate depends on the note.
4. Song table
-------------
4 bytes number of songs (19)
19 x 4 offset of each song, relative to the start of the song table (the count field)
Song header (8 bytes):
0x00 4 bytes 0x00145768 in every song (unknown, ignored by the player as far as I can see)
0x04 1 byte song length (number of order entries)
0x05 1 byte number of patterns
0x06 1 byte number of channels (4, 5 or 6)
0x07 1 byte 0x77 in every song (unknown)
0x08 order list: song length x uint16. Each entry is the BYTE OFFSET of that pattern,
relative to the start of the song header. (So pattern number =
(offset - (8 + 2 x length)) / (channels x 256).)
then the patterns, unpacked.
Each pattern: 64 rows x channels x 4 bytes. Row-major (row 0 ch0, row 0 ch1, ...).
Songs found (index: orders / patterns / channels / instruments used):
0: 42/34/6 ins 1-6 10: 19/15/5 ins 74-81
1: 13/12/6 ins 7-15 11: 8/ 8/4 ins 82-83
2: 61/46/4 ins 16-22 12: 26/26/4 ins 84-91
3: 24/11/4 ins 23-29 13: 13/13/5 ins 92-100
4: 16/10/4 ins 30-38 14: 37/36/5 ins 101-109
5: 24/20/5 ins 39-42 15: 68/49/5 ins 110-118
6: 40/28/4 ins 43-50 16: 20/19/4 ins 120-125
7: 30/30/4 ins 51-58 17: 20/19/4 ins 126-128
8: 46/22/5 ins 59-66 18: 2/ 2/6 ins 130-143
9: 35/26/4 ins 67-73
Which song plays on which map is not mapped yet.
5. Pattern cell (4 bytes)
-------------------------
byte 0 high nibble = effect command (0-F, ProTracker numbering)
low nibble = ignored by the player (leftover junk from conversion)
byte 1 instrument (1-143, index into the MUSIC bank; 0 = none)
byte 2 note (1-72; 0 = none). Index into the period table below:
note 1 = period 1712, note 13 = 856 (PT C-1), note 25 = 428 (PT C-2), ...
i.e. one octave lower than ProTracker's lowest, 6 octaves total.
byte 3 effect parameter
Effects actually implemented by the player:
0xy arpeggio 9xx sample offset (xx*256)
1xx porta up Axy volume slide
2xx porta down Bxx position jump
3xx tone portamento Cxx set volume
4xy vibrato Dxx pattern break (row is plain hex, NOT decimal like PT)
5xy tone porta + vol slide Fxx set speed (only 01-1F; tempo values 20+ are IGNORED)
6xy vibrato + vol slide
7xy tremolo
E1x/E2x fine porta, E4x vibrato waveform, E6x pattern loop, E7x tremolo waveform,
E9x retrigger, EAx/EBx fine volume slide, ECx note cut, EEx pattern delay.
Not implemented (no-ops): 8xx panning, E0x, E3x, E5x (set finetune), E8x, EDx (note delay), EFx.
6. Player / mixer facts
-----------------------
- Sound init: 0x0802FC70 (called from 0x0800C764 with mode 0)
- Hardware setup: 0x08030308. Direct Sound A + B, fed by DMA1/DMA2 from two 0x200-byte
double buffers. Timer 0 drives the sample rate.
- Mode 0 (what the game uses): Timer0 reload 0xFAE1 -> 16777216 / 1311 = ~12797 Hz mix rate.
(Mode 1 = 0xFBB6 -> ~15280 Hz, unused.)
- The mixer renders 256 samples per call and runs one replayer tick per call:
12797 / 256 = ~50 ticks/sec = ProTracker's 125 BPM. Tempo is fixed, only speed changes.
Default speed = 6.
- Pitch: step = (139 << 16) / period (139 = mode 0 constant at 0x0803037C)
The mixer (ARM, copied to IWRAM, ROM copy at 0x08FFB8F4) keeps the sample position as
17.15 fixed point and adds step once per output sample, nearest-neighbour, no interpolation.
-> playback Hz = 139 * 2 * 12797 / period = ~3,557,600 / period
(Amiga PAL is 3,546,895 / period, so same rate, 0.3% sharp.)
[Earlier revision of this file wrongly read the position as 16.16 and said "half rate".]
- Period table: 0x08030D08, 16 finetunes x 72 notes x uint16 (finetune * 0x90 bytes).
Vibrato sine table at 0x08030CE8, arpeggio tick table at 0x08030CC8.
- Row effects jump table 0x080309B0, tick effects 0x08030B50, Exx row effects 0x08030A60.
- Stereo: channels alternate between the two Direct Sound buffers (even channels -> buffer
A, odd -> buffer B), so it is hard-panned like an Amiga. Which side A is on is unverified.
7. Converting to a tracker module (gbad2_music2xm.py)
------------------------------------------------------
python3 gbad2_music2xm.py DoomII.gba out_folder [--format xm|mod|both] [--pan 0-100] [--sfx]
.MOD (closest to the GBA playback rules):
- the game's own period values and the samples go straight in, unchanged
- effects copied as-is apart from the no-ops being removed and Dxx hex -> decimal
- limitation: trackers force Amiga LRRL panning on MODs; the GBA alternates L/R per channel
.XM (correct panning, original samples untouched):
- note n -> XM note n+24, finetune nibble x16 minus 14 (folds in the ~11 cent offset)
- channel panning applied via the volume column whenever an instrument is triggered
- FastTracker 2 effect rules differ slightly from ProTracker's (effect memory, vibrato),
the converter strips 100/200/A00 etc. to cover the main differences
Both formats were checked in libopenmpt: every song loads, XM and MOD versions have identical
lengths (0:15 to 5:10) and render at the same pitch.
SFX (--sfx) are written as 8000 Hz WAVs; that rate is the old Documentation.txt guess and is
not verified against the SFX player code.
A bit more info on this "DPAK" format:
Code:
GBA Doom II - Torus "DPAK" Archive & "FPS_RAW_FILE" Notes
==========================================================
Reverse engineered from DoomII_Recomp.gba (game code A9DP52).
All values are little endian. Offsets are written as HEX/Decimal like the old Documentation.txt.
"ROM" offsets are file offsets into the .gba. "FRF+" offsets are relative to the start of FPS_RAW_FILE.
Confidence tags: [CONFIRMED] = checked against the data, [GUESS] = looks right but not proven.
Short version
-------------
Yep, the whole game is basically one big Torus .wad. There's a "DPAK" directory at 37CA4/228516,
and one of its entries is "FPS_RAW_FILE", a 13 MB blob that holds pretty much the entire 3D engine's data:
palettes, colourmaps, levels, thing tables, the graphic directory, ALL the wall/flat/sprite pixel data,
math tables and the English text.
And that magic number everyone had to add, 329CC0/3316928? It's not magic at all:
DPAK start 37CA4
+ DPAK entry 61 offset A04DC -> FPS_RAW_FILE starts at D8180/885120
+ FPS_RAW_FILE header field 0x48 = 251B40
= 329CC0 <- the "graphics data base"
So the old tools were extracting things correctly, they were just using a hardcoded result of
three lookups the game does at runtime. The game code holds one literal pointer, 08037CA4 (stored at
ROM 2CED4), and finds everything else through the DPAK and FPS_RAW_FILE headers.
This is the part that should carry over to other Torus engine GBA games: find "DPAK" + "Torus",
follow the type 6 entry, read the FPS_RAW_FILE header, and you get the base for free.
1. The DPAK directory (ROM 37CA4/228516) [CONFIRMED]
------------------------------------------------------
0x00 4 bytes "DPAK"
0x04 2 bytes 0x0040/64 = number of used entries (64 used, then 2 blank slots)
0x06 10 bytes "Torus" + zero padding
0x10 66 entries, 16 bytes each:
4 bytes type
4 bytes offset (relative to the "DPAK" text, NOT the start of the rom)
4 bytes size
4 bytes always 0
Type 0 = blank slot (entries 64 and 65).
# Type ROM offset Size What it is
0-2 2 380B4, 38F9C, EE8,1B08, Font/misc tables
3AAA4 1B40
3 3 3C5E4 31D98 2D tile graphics + 16 colour palettes (see section 5)
4 4 6E37C DEA4 "d2weapons": named tint palettes, "StructureEnglish"
5-60 5 7C220 - D7484 varies 56 full 2D images (title, intermission, BOSSBACK etc, section 4)
61 6 D8180 CE9938 FPS_RAW_FILE (section 2 & 3)
62 7 DC1AB8 234DAC MUSC music + sounds (see GBA_Doom2_Music_Format.txt)
63 1 FF6864 6D8 Small u32 table
To get a ROM offset from an entry: 37CA4 + entry offset.
2. FPS_RAW_FILE header (ROM D8180/885120)
-----------------------------------------
"RAW FILE" is a good name for it. It's laid out like a memory image of one big struct, with every pointer
stored as an offset from the start of FPS_RAW_FILE. It even ends with a footer that says "ENDOFRAW".
FRF+ Value Meaning
0x00 0000029A 666. Magic number. Of course it's 666. [CONFIRMED]
0x04 12 bytes "FPS_RAW_FILE" [CONFIRMED]
0x10 000001F5 501 unknown count
0x14 00000451 1105 unknown count
0x18 00000769 1897 count of u32s in the thing table below? (7588 bytes / 4 = 1897 exactly) [GUESS]
0x1C 00213390 -> Thing/sprite definition table (section 3.6)
0x20 00215134 -> 12 byte record table, probably states/frames (section 3.7)
0x24 0000000F 15 = count for the next table [CONFIRMED]
0x28 00217BAC -> 15 records of 44 bytes, probably weapon info (section 3.8)
0x2C 00000E73 3699 = last index of the graphic directory (3700 entries, 0 to 3699) [CONFIRMED by size]
0x30 00217E40 -> GRAPHIC DIRECTORY (the old "file listings") (section 3.9)
0x34 00000022 34 = number of levels [CONFIRMED]
0x38 00037D8C -> LEVEL DIRECTORY (section 3.5)
0x3C FFFFFFFF unused slot (FFFFFFFF = null pointer)
0x40 0000568C -> 96 RGB palettes (PLAYPAL) (section 3.3)
0x44 0001768C -> Colourmap / light tables (section 3.4)
0x48 00251B40 -> GRAPHICS DATA BASE. 329CC0 comes from here! [CONFIRMED]
0x4C 00CDB984 -> end of graphics data / free space after it (section 3.10)
0x50 FFFFFFFF unused
0x54 FFFFFFFF unused
0x58 00CDF350 -> u16 lookup table, 1024 entries (section 3.11)
0x5C 00CDFB50 -> u32 reciprocal table (section 3.11)
0x60 00000001 number of languages (1) [CONFIRMED]
0x64 00CE1B58 -> language list ("ENGLISH") (section 3.12)
0x68 00000176 374 = number of text strings [CONFIRMED]
0x6C 00CE1B70 -> string table (section 3.12)
0x70 FFFFFFFF unused
0x74 00CE4F0C -> "21-\" records (section 3.13)
0x78 00CE5418 -> mostly FF block (section 3.13)
0x7C 00000004 unknown
0x80 00CE9928 -> footer ("ENDOFRAW") [CONFIRMED]
0x84 0000000B unknown
0x88 00000000 unknown
0x8C FFFFFFFF unused
0x90 000000AC -> small table right after the header (section 3.1)
0x94 00000308 -> u32 reciprocal table (section 3.2)
0x98 00004508 -> 256 byte table
0x9C 00004608 -> 1024 x u32 table
0xA0 00005608 -> small 0x84 byte block
0xA4 FFFFFFFF unused
0xA8 FFFFFFFF unused
Every pointer above, as a ROM offset (D8180 + value):
AC->D822C 308->D8488 4508->DC688 4608->DC788 5608->DD788 568C->DD80C 1768C->EF80C
37D8C->10FF0C 213390->2EB510 215134->2ED2B4 217BAC->2EFD2C 217E40->2EFFC0 251B40->329CC0
CDB984->DB3B04 CDF350->DB74D0 CDFB50->DB7CD0 CE1B58->DB9CD8 CE1B70->DB9CF0 CE4F0C->DBD08C
CE5418->DBD598 CE9928->DC1AA8
3. What's inside FPS_RAW_FILE
-----------------------------
3.1 FRF+AC, ROM D822C (0x25C bytes) [GUESS]
Starts with 0x25A then ~150 u32s, mostly small even numbers with FFFFFFFF holes.
Looks like a hash/bucket table or an index remap. Not decoded.
3.2 Math tables, FRF+308 to FRF+568C [CONFIRMED that they're math tables]
FRF+308 u32 reciprocals: 0, 80000, 40000, 2AAAA, 20000, 19999... = (1/n) in 13.19 fixed point.
FRF+4508 256 bytes of slowly rising values (gamma/sqrt style curve).
FRF+4608 1024 x u32 reciprocal style table.
FRF+5608 0x84 byte block (small header then FF padding).
3.3 Palettes, FRF+568C, ROM DD80C/907276 [CONFIRMED]
96 x 768 byte RGB palettes, exactly 0x12000 bytes. These are the 96 PLAYPALs from the old docs.
3.4 Colourmaps, FRF+1768C, ROM EF80C/981004 [GUESS]
0x20700 bytes of palette index remaps that run up to the level directory.
Not the PC COLORMAP layout (row 0 isn't an identity map), so the light levels are probably
arranged differently. Needs a proper look.
3.5 Level directory, FRF+37D8C, ROM 10FF0C/1113868 [CONFIRMED layout, contents partly]
34 entries of 16 bytes (32 maps + 2 extra, probably the link-cable deathmatch maps):
4 bytes offset of a "ZW" compressed block (main level data)
4 bytes a count (MAP01 = 0x17B, MAP02 = 0x451 ...)
4 bytes offset of a bitfield block (reject style?)
4 bytes offset of a u16 list (rising values, looks like an index/blockmap list)
All offsets are FRF+ relative. The level data runs from FRF+37FAC up to FRF+213390.
Each level starts with a run of 16 byte records (FFFFFFFF 00000000 FFFFFFFF FFFFFFFF ...).
The main block starts with "ZW" + decompressed size, e.g. MAP01: "5A 57 5C 28 01 00" = 0x1285C bytes.
"ZW" is Torus' own compression (also used by the type 5 images below). Not cracked yet.
Cracking ZW is the key to editing levels.
3.6 Thing/sprite definitions, FRF+213390, ROM 2EB510 [GUESS on meaning]
0x1DA4 bytes. Records 52 (0x34) bytes apart, each one holds a 4 letter PC sprite name
(AMMO, BROK, SBOX, CELP, CELL, CLIP, ROCK, SHEL, ARM2 ... TELE, BSTG) at +0x28 from where I'd guess
the record starts. Lines up with Doom's mobj/sprite list.
3.7 FRF+215134, ROM 2ED2B4 [GUESS]
12 byte records: "01 00 01 00 00 00 00 00 00 00 00 00", "01 00 07 00 00 00 01 00 ..."
The 2nd number goes up by 6 each record. Probably the state/frame table.
3.8 FRF+217BAC, ROM 2EFD2C [CONFIRMED count/size]
15 records x 44 bytes (count is header field 0x24). Probably weapon definitions.
3.9 GRAPHIC DIRECTORY, FRF+217E40, ROM 2EFFC0/3080128 [CONFIRMED]
This is the "file listings" from the old Documentation.txt, but the old tools started it 4 bytes
too early, at 2EFFBC. 3700 records x 64 bytes, running up to exactly the graphics data base.
So the "4 bytes Unknown, maybe identifier?" at the start of the old .H header is really the
LAST field of the previous record. Correct layout:
0x00 4 bytes Height
0x04 4 bytes Width (number of columns, = number of entries in the .LOC offset table)
0x08 4 bytes Number of mip levels. 8 for everything, except skies which are 1.
0x0C 4 bytes X offset (sprite origin, the "X-Center" from the old docs). 0 on walls/flats.
0x10 4 bytes Y offset (sprite origin, top offset like the PC). 0 on walls/flats.
0x14 4 bytes X1 offset \
... > 8 offsets, add the graphics data base (329CC0) like before
0x30 4 bytes X8 offset /
0x34 8 bytes Name. PC style names for walls/flats, Torus names for sprites (BoxBull, bossd1e...).
Flats put a "*" in the 8th character, e.g. "FLOOR0_*", "GRASS1" + 00 + "*".
0x3C 4 bytes Flags/info, split into bytes:
byte 0 = type flags. Values seen:
46 = sprite (3136 of them) 08 = wall texture (360)
00 = flat (108) 02 = masked mid texture (MIDBARS, SW1EXIT...) (21)
18 = animated (FWATER, SLIME) (62) 09 = sky (3)
98 = SKINFACE, METAL, ZIMMER... (9) 92 = SKSPINE2 (1)
byte 1 = varies per graphic (9F, 8C, 66...). Maybe an average colour index for far away/
automap drawing [GUESS]
byte 2 = small signed number, mostly 0 (also 1, -1, -2, -3) [unknown]
byte 3 = 0
Example: BoxBull (AMMOA0) is record 0, height 32, width 32, 8 mips, X 16, Y 32, X1 offset 0.
329CC0 + 0 = the AMMOA0 .LOC table, which is exactly the one the old docs used as the sprite example.
1579 unique names across 3700 records, the rest are rotation duplicates.
3.10 Graphics data, FRF+251B40 to FRF+CDB984, ROM 329CC0 to DB3B04
All the .LOC tables and column pixels, same format as in the old docs.
After it is a mostly FF block up to FRF+CDF350 (free space, a few non-FF bytes at FRF+CDEA84).
3.11 FRF+CDF350 (u16 x 1024: 0,0,0,1,1,1,2,2,3...) and FRF+CDFB50 (u32: 0, 0, 0, 55555555, 0,
33333333, AAAAAAAA ...). Division/scaling tables for the renderer.
3.12 Text, FRF+CE1B58, ROM DB9CD8 [CONFIRMED]
Language list: 4 bytes 0, "ENGLISH" + 0, then 0DF0ADBA ("BAADF00D") filler.
String table at FRF+CE1B70: 374 records of 16 bytes:
4 bytes FFFFFFFC
4 bytes string ID (1015, 1008, 10A4 ...)
4 bytes 0
4 bytes FRF+ offset of the string
The strings start at FRF+CE32D0, ROM DBB450. Zero terminated, "\u" at the start = a message
that goes to the top of the screen ("\uPicked up the armor.", "Entryway", key door messages...).
This is where you'd change pickup messages and level names.
3.13 FRF+CE4F0C "21-\" records (0x50C bytes, FF padded), then FRF+CE5418 a 0x4510 byte block that's
nearly all FF. Not decoded.
3.14 Footer, FRF+CE9928, ROM DC1AA8: "C8 00 32 00 32 00 2C 01" + "ENDOFRAW". MUSC starts right after.
4. Type 5 images (DPAK entries 5-60) [CONFIRMED]
------------------------------------------------
0x00 2 bytes Width
0x02 2 bytes Height
0x04 2 bytes 0x000C, offset to the palette
0x06 2 bytes 0x020C, offset to the pixels
0x08 2 bytes Flags. 0x0008 = raw pixels, 0x0108 = "ZW" compressed pixels
0x0A 2 bytes 0x73DD on every image (key colour?)
0x0C 512 bytes 256 colour RGB555 palette
0x20C pixels, 8 bit, left to right
BOSSBACK from the old bin folder (713124/AE1A4) is entry 44's pixels: AE1A4 = ADF98 + 20C.
Compressed ones start with "ZW" + u32 decompressed size (00009600 = 240x160).
5. Type 3 block (ROM 3C5E4) [GUESS]
-----------------------------------
u16 3, u16 1, u16 0x0C, u16 0x3C, u16 0x12E0, u16 FFFF, then 12 u32 offsets, then 0x95/149
16 colour RGB555 palettes (colour 0 is 7FE0, the transparent key). That's GBA tile/OBJ graphics,
so this is most likely the HUD, status bar, menu and weapon sprites, and where the old docs'
SPRITE555 palettes ("only uses 1 of 16 colours per line") really come from.
It is NOT the level data, that's in FPS_RAW_FILE.
6. Notes for other Torus engine games
-------------------------------------
* Search the ROM for "DPAK" followed by "Torus". The game code holds one pointer to it (here 08037CA4).
* Entry types look like engine-wide resource types (6 = FPS_RAW_FILE, 7 = MUSC). Check if the
other games use the same numbers.
* FPS_RAW_FILE has magic 666 and ends in ENDOFRAW, so its size is easy to verify.
* The header layout is probably shared between games, but field order could shift if the engine
version differs. Check that header 0x48 lands right after the graphic directory
(directory start + count x 64) to confirm.
7. Still to do
--------------
* Crack "ZW" compression (needed for level data and the compressed full screen images).
* Decode the level block, thing table (3.6), states (3.7), weapon table (3.8).
* Colourmap layout (3.4) and the 0xAC table (3.1).
* Meaning of graphic directory byte 1/2 and flags 0x80/0x10.
Anyway, I've attached the converted modules.
They're compatible in OpenMPT and Milkytracker. Enjoy!