A disc, a bad block and the first key
2026-10-23. Tools: GameFrame, Ghidra 12.1.4, Python 3.13.
The goal of this project is plain: write down POD's data formats (tracks, cars, images and sounds) well enough that people can change them. A format counts as done when every file of its kind decodes completely, writes back byte-identical, and loads in the game after a change.
The disc
The starting point is the PC CD-ROM as a CUE/BIN image: one Mode 1 data track and one audio track of 14 minutes 39 seconds, which is the game's music. The volume was mastered by Ubi Studios on 13 January 1997, and the executables report version 2.1.0.0.
The first check failed. The image had the right size but the wrong MD5. The download had been resumed, and a resume can splice in the wrong bytes. Instead of fetching 564 MB again, I used the archive's .torrent file, which stores a SHA-1 hash for every 512 KB piece. One piece out of 1,075 was bad. Fetching just that byte range and writing it back gave an image whose MD5 and SHA-1 both match the published ones.
What is on it
The data track holds 781 files. The game itself is eight Windows builds, all compiled with Watcom C/C++ on 7 March 1997, one for each renderer: software (DirectDraw), ATI 3D Rage, S3 ViRGE and 3dfx Glide, each for a plain Pentium and for MMX. This project reads WINPOD.EXE, the plain software build.
The data folders have French names: CIRCUITS (tracks), VOITURES (cars), RESSON (sound), POLICES (fonts), DICOS (text), MIMG (menu images), RECORDS (best times). Every track name turns up four times: geometry in .BL3/.BL4, sounds in .MEG, a preview in .IMG and a best-times file in .BRT.
The first hex dumps showed a pattern. Where a file should hold runs of zeros, it holds the same four bytes over and over, a different value in each kind of file. That pointed to a simple XOR of every 32-bit word with a per-file key, not to compression. XORing a car file with its most common word gave readable texture names at once.
The reader in the game
The Ghidra export of WINPOD.EXE has 1,725 functions. A group of them share one structure: a file handle at +0x108, a buffer at +0x124 and a key at +0x10c. One of them, FUN_00468358, is the whole story. It reads a block, XORs every word but the last with the key, adds up the decoded words, and stops the game with an error if the sum is not the last word.
So each file is a run of fixed-size blocks, with a checksum in the last word of each. The key and the block size are not in the file. They are constants at each call to the open routine, FUN_004694b8: the key in EDX and the block size in EBX. Watcom passes arguments in registers, and Ghidra's pseudocode dropped EBX, so the block sizes came from the disassembly.
There are seven pairs: tracks 0xF7E in blocks of 16 KB, cars 0xF2E, chrome 0xDD6, fonts 0xFAE, video stills 0xF3A in 8 KB blocks, text 0xEA1E in 512-byte blocks, and best times 0xEAFA in 4 KB blocks.
Measured, not guessed
A small decoder now tries every pair on every file of the disc. 98 files decode with every block's checksum matching, and encoding each decoded stream again gives the original file byte for byte. That is the outer layer done for every one of those formats. It is also the first thing a mod tool needs: change a file without fixing the checksums, and the game refuses to load it.
The block container page has the layout, the keys and the reader's fields.
Next
- Inside the decoded streams: a header word, an entry count and a table of offsets. Find out whether the offsets count decoded bytes or raw file bytes.
- Some track streams still show the track key in places: a second layer, or plain data?
- The formats outside the container:
.IMGimages with plain headers,.MEGsound banks, and theSCRIPTSlists.