This document describes the format of the Wasteland 1 END.CPA file.
It covers:
The compression and base-image post-processing used by this file are documented separately:
END.CPA contains a single animated ending image.
The image uses packed 4-bit pixels:
288 pixels128 pixels144 bytes144 * 128 = 18432 bytes (0x4800)All multi-byte integer fields are little-endian.
The file contains exactly two back-to-back compressed blocks:
There is no global file header, no block table, and no per-frame offset table.
The first block contains the base ending image.
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 |
Decoded payload size in bytes |
+0x04 |
char[3] |
ASCII signature, always "msq" |
+0x07 |
u8 |
Disk byte, always 0 |
+0x08 |
bitstream | Huffman-coded payload |
Important details:
u32 size is the size after Huffman decodingThe decoded payload is the 18432-byte packed base image.
After Huffman decoding it is still vertical-XOR encoded. It must be vertical-XOR decoded with a row stride of 144 bytes to produce the final base frame.
The second block contains the animation update stream.
Unlike the base image block, this block does not use the ASCII "msq" signature.
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 |
Decoded payload size in bytes |
+0x04 |
u8[3] |
Fixed magic bytes 08 67 01 |
+0x07 |
u8 |
Disk byte, always 0 |
+0x08 |
bitstream | Huffman-coded payload |
The decoded animation payload is not vertical-XOR encoded.
The decoded animation block has this layout:
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u16 |
Animation content size |
+0x02 |
variable | Update stream |
+0x02 + contentSize |
u16 |
Final end marker, always 0x0000 |
The contentSize field must equal:
decodedAnimationSize - 4
So it excludes:
0x0000 end markerThe contentSize region contains all updates plus the update-list terminator.
The update stream is a sequence of updates terminated by 0xFFFF.
It is best described as:
updateStream := update* 0xFFFF
Each update has this layout:
update := delay:u16 patch* 0xFFFF
So the same sentinel value 0xFFFF is used at two levels:
At the end of the decoded animation payload, the structure therefore ends with:
... 0xFFFF 0xFFFF 0x0000
Meaning:
Each update begins with a delay field:
| Type | Meaning |
|---|---|
u16 delay |
Delay before this update is applied |
The file format stores only the raw delay value. A practical playback model is:
milliseconds = (delay + 1) * 54.925
based on the IBM PC timer tick rate of about 18.2065 Hz.
Each patch has this layout:
| Type | Meaning |
|---|---|
u16 offset |
Patch position in a 320-pixel-wide logical screen grid |
u8[4] data |
Four replacement bytes, representing 8 pixels |
Patch data is copied into the current frame as raw bytes. It is not XOR-applied.
In pseudocode:
for i in 0 .. 3:
image[imageByteOffset + i] = data[i]
This is the unusual part of the format.
The patch offset is not measured relative to the 288-pixel-wide ending image. Instead, it is measured in units of one 8-pixel patch cell on a 320-pixel-wide logical screen.
That means:
320 / 8 = 40 patch cells288 / 8 = 36 visible patch cells per rowThe raw offset therefore wraps every 40 cells, not every 36 cells.
Given a raw patch offset:
cellX = offset % 40
y = floor(offset / 40)
x = cellX * 8
This yields the patch position in pixels within the logical 320-pixel-wide screen grid.
To convert this to a byte offset inside the packed 288x128 image:
imageByteOffset = y * 144 + cellX * 4
This works because:
8 pixels wide2 pixels per byte4 image bytesEquivalent formulas are:
x = (offset * 8) % 320
y = floor((offset * 8) / 320)
imageByteOffset = y * 144 + x / 2
For offset = 41:
cellX = 41 % 40 = 1
y = floor(41 / 40) = 1
x = 1 * 8 = 8
imageByteOffset = 1 * 144 + 1 * 4 = 148
So the patch replaces 8 pixels starting at:
x = 8, y = 1148 in the packed image bufferA parser can read END.CPA like this:
u32.144.u32.contentSize.0xFFFF is reached.0x0000.Useful consistency checks when implementing a reader:
"msq" and disk byte 018432 bytes08 67 010contentSize must equal decodedAnimationSize - 40xFFFF0x0000288x128 image area