The block container
The outer layer of most of POD's binary files. A modder must decode it before anything else, and encode it again after a change, or the game stops with a checksum error.
Reader: decoders/container.py (check DIR, decode FILE OUT). State: decoded and round-trip for all 98 files below (measured, 2026-10-23: every block's checksum matches and encoding the decoded stream gives the original bytes).
Layout
A file is a run of blocks, all BLOCK bytes long; the file size is a multiple of BLOCK.
| Offset in block | Size | Content |
|---|---|---|
| 0 | BLOCK - 4 | Data: each 32-bit little-endian word XORed with KEY |
BLOCK - 4 | 4 | Checksum: the sum, modulo 2³², of the decoded data words of this block |
Decoding a file joins the data of its blocks into one stream of (size / BLOCK) × (BLOCK - 4) bytes. Observed: FUN_00468358 and FUN_004694b8 decode a block in place with *p ^= key over BLOCK / 4 - 1 words, add the decoded words, and call the error routine FUN_004912b8 when the sum differs from the last word.
Keys
The key and block size are constants at each call to the open routine FUN_004694b8: the key in EDX, the block size in EBX (Watcom register arguments; Ghidra's pseudocode drops EBX, the disassembly shows it). Observed at the call sites; the file kinds are measured by which key decodes which files.
| Key | Block | Call site (function) | Files |
|---|---|---|---|
0x0F7E | 0x4000 | 0x46ef96 (FUN_0046ef04) | CIRCUITS/*.BL3, *.BL4; POLICES/*.BI2 |
0x0F2E | 0x4000 | 0x46fbb6 (FUN_0046fb14) | VOITURES/*.BV3, *.BV4 |
0x0DD6 | 0x4000 | 0x4ad48d (FUN_004ad400) | VOITURES/CHROME.CH3, .CH4 |
0x0FAE | 0x4000 | 0x46e6a2, 0x46e813 | POLICES/*.BP3 |
0x0F3A | 0x2000 | 0x469dc0, 0x469f64, 0x46a325 | VIDEO/*.BBM, CREDITS.BVB |
0xEA1E | 0x200 | 0x474b83 (FUN_00474b4c) | DICOS/*/*.BMD |
0xEAFA | 0x1000 | 0x4b23b0 (FUN_004b2300) | DATA/RECORDS/*.BRT |
The .BI2 files decode with the track key, although the fonts' routine loads the other POLICES files: which routine opens them is open.
Not in this container (measured: no key matches): SCRIPTS/*.BIN, MIMG/*.IMG, RESSON/*.MEG, the AVI videos and the text files.
The stream header
The open routine reads three things from the start of the stream (observed in FUN_004694b8):
| Stream offset | Size | Stored at | Meaning |
|---|---|---|---|
| 0 | 4 | +0x100 | Equal to the raw file size in every file checked (measured); use unknown |
| 4 | 4 | +0x110 | Entry count; the low 30 bits are used (& 0x3fffffff) |
| 8 | 4 × count | table at +0x114 | One offset per entry |
The first offset equals 8 + 4 × count, the end of the header (measured on all 98 files).
Offsets count raw file bytes, checksum words included. FUN_00468dc4 reads the block at offset - offset % BLOCK and starts at offset % BLOCK inside it (observed). In the decoded stream, an offset o is at (o // BLOCK) × (BLOCK - 4) + o % BLOCK. Measured: no offset in the 98 files falls in a checksum word, and in every track file each offset mapped this way lands on the start of a name, while read as a stream offset it lands inside other data (decoders/offsets_probe.py).
Unused entries hold raw zeros, so they decode to the key itself (for example 0x0F7E as the last track entry, and nine entries of 0x0DD6 in CHROME.CH3). A raw zero word is the only way a decoded word can equal the key, and these words still count in the checksum.
The reader object
Fields of the reader, from FUN_004694b8 (open), FUN_00468358 (read) and FUN_00468dc4 (seek). All observed.
| Offset | Type | Meaning |
|---|---|---|
+0x000 | char[0x100] | File name |
+0x100 | u32 | First word of the stream |
+0x104 | u32 | Block size |
+0x108 | HANDLE | File handle |
+0x10c | u32 | Key |
+0x110 | u32 | Entry count |
+0x114 | u32* | Entry offset table |
+0x124 | u8* | Block buffer |
+0x128 | i32 | Bytes left in the current block |
+0x12c | u8* | Read position in the buffer |
Leftovers
The writer (FUN_00468fa4, FUN_0046859c) encodes each block in place in one buffer. When the data ends inside the last block, the rest of that block keeps what the buffer held: the previous block, already encoded. Encoded again on writing, those bytes decode to the previous block's decoded bytes XOR the key (aligned to the word). Measured on every byte after the pixels in PVRSMOKE.BI2 and PVRSHADO.BI2 (fonts.md). A writer that wants identical files must reproduce this; the .BRT files have zeros there instead (records.md).
Open questions
- What do the two words after
CHROME.CH3's header mean, and why does chrome have its own key?