This document describes the portrait archive format used by the Wasteland 1 files ALLPICS1 and ALLPICS2.
It covers:
The compression and base-image post-processing used by this file are documented separately:
ALLPICS1 and ALLPICS2 use the same binary format. The only practical difference is which portraits each file contains.
All multi-byte integer fields are little-endian.
Portrait images are stored as packed 4-bit pixels:
96 pixels84 pixels48 bytes48 * 84 = 4032 bytes (0x0FC0)An ALLPICS file is just a sequence of portrait records concatenated until end-of-file.
There is:
ALLPICS file itselfTo parse the file, read portrait records until EOF is reached.
The game does not resolve portrait IDs directly inside ALLPICS1 or ALLPICS2. Instead, the unpacked WL.EXE contains the lookup metadata for both portrait archives.
The lookup tables are:
| Table | Location in unpacked WL.EXE |
Format |
|---|---|---|
portraitOffsets1 |
0x18AB0 |
u32_le[34] |
portraitOffsets2 |
0x18B38 |
u32_le[49] |
portraitIndices1 |
0x18E4A |
u8[80] |
portraitIndices2 |
0x18E9A |
u8[80] |
Portrait lookup is disk-specific and works in two stages:
ALLPICS1 when the portrait reference comes from GAME1, or ALLPICS2 when it comes from GAME2.u8[80] portrait-index table.0x80, then the portrait ID is invalid for that disk.u32 offset table.ALLPICS file and read one portrait record.Each portrait record contains two back-to-back msq blocks:
Both blocks use the same wrapper:
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 |
Decoded payload size in bytes |
+0x04 |
char[3] |
ASCII signature, always "msq" |
+0x07 |
u8 |
MSQ disk byte |
+0x08 |
bitstream | Huffman-coded payload |
Important details:
u32 size is the size after Huffman decoding.In the shipped Wasteland 1 portrait archives, the MSQ disk byte has a fixed value depending on file and block type:
0ALLPICS1 always uses 0ALLPICS2 always uses 1The first msq block in each portrait record contains the base portrait frame.
The decoded payload is a 4032-byte packed image buffer for a 96x84 portrait.
The bytes are still vertical-XOR encoded at this point. After Huffman decoding, the data must be vertical-XOR decoded with a row stride of 48 bytes.
After that step, the result is the final base image:
The second msq block in each portrait record contains all animation scripts and all image update data for the portrait.
Unlike the base image block, the decoded animation payload is not vertical-XOR decoded.
The decoded animation block has this layout:
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u16 |
Size of the script section in bytes |
+0x02 |
u8[scriptsSize] |
Script section |
+0x02 + scriptsSize |
u16 |
Size of the update section in bytes |
+0x04 + scriptsSize |
u8[updatesSize] |
Update section |
There is no footer, no padding, and no count field for either scripts or updates.
For a valid block:
animDecodedSize = 2 + scriptsSize + 2 + updatesSize
The script section is a byte stream containing one script after another until exactly scriptsSize bytes have been consumed.
There is no stored script count.
Each script is a sequence of (delay, updateIndex) pairs terminated by a single byte 0xFF.
| Type | Meaning |
|---|---|
u8 delay |
Delay value for this script step |
u8 updateIndex |
Zero-based index into the update table |
Parsing rule:
0xFF, the current script ends.delay, and the next byte is updateIndex.Example:
00 04 03 07 01 02 FF
This encodes one script with three lines:
delay = 0, updateIndex = 4delay = 3, updateIndex = 7delay = 1, updateIndex = 2The file format stores only the raw u8 delay value. No additional timing multiplier or per-portrait speed field is present in the animation data.
A practical timing model is to treat the delay as an additional hold count on top of a base redraw tick:
milliseconds = (delay + 1) * 54.925
This is based on the IBM PC timer tick rate of approximately 18.2065 Hz, where one tick is about 54.925 ms.
The update section is a byte stream containing one update after another until exactly updatesSize bytes have been consumed.
There is no stored update count.
Each update contains one or more patch records and ends with the 16-bit sentinel 0xFFFF.
| Type | Meaning |
|---|---|
u16 sizeAndOffset |
Packed patch header |
u8[size] data |
XOR bytes for the patch |
If sizeAndOffset == 0xFFFF, the current update ends and no patch follows.
Otherwise the packed header is decoded as:
size = (sizeAndOffset >> 12) + 1
offset = sizeAndOffset & 0x0FFF
This means:
size - 11 to 16 bytesoffset is measured in bytes within the packed 4032-byte portrait buffer, not in pixels.
To apply one patch:
for i in 0 .. size-1:
image[offset + i] ^= data[i]
To apply one update, apply all of its patches in sequence.
Because the patches use XOR, applying the same update twice restores the original bytes for the affected region.
A parser can read one portrait record like this:
msq block.u32.4032 bytes with stride 48.msq block.u32.scriptsSize.scriptsSize bytes have been consumed.updatesSize.updatesSize bytes have been consumed.Repeat until EOF to read the whole file.
Useful consistency checks when implementing a reader:
msq base-image blockmsq4032 bytes for Wasteland 1 portrait datascriptsSizeupdatesSize4032-byte image buffermsq block ends