{"slug": "reading-jpak", "title": "Reading JPAK", "summary": "A developer reverse-engineered the proprietary JPAK compression format used in the 2006 racing game TOCA Race Driver 3, identifying it as LZSS with a 4,096-byte window and flag bits read least-significant-first. The analysis, published in a blog post, decoded all 166 files in a per-track AI pack, including configs, car models, and racing-line data, by leveraging predictable content in an INI file and tracing back-references.", "body_md": "By 2006, TOCA Race Driver 3 was compressing the per-track AI. The racing\nlines sit inside a pack whose first four bytes are `JPAK`\n\n. There is no public\nspecification.\n\nI did not start by guessing the algorithm. I read the container, then one file I could predict, then the first back-reference. The codec fell out of those three observations. It is LZSS. 4,096-byte window. Flag bits least-significant-first.\n\nA compressed archive is a directory plus payloads. The directory is uncompressed. Read that first.\n\nA small `.jpk`\n\nhas a 32-byte header: magic, version, member count, a table\noffset, a data-start offset, a timestamp, a hash. Then a table of 32-byte\nrecords. Columns that only go up are offsets. Columns that bounce around are\nsizes.\n\n```\ncol A  0x310  0x317  0x322 …   tiny steps, only up     name-table offsets\ncol B  0x161  0x1ae7 0x1f1 …   varies                  compressed size\ncol C  0x440  0x5c0  0x20c0 …  big steps, only up      compressed offset\ncol D  0x2c2  0x7936 0x373 …   varies                  uncompressed size\n```\n\nB divided by D sits between 1.7× and 4.5×, so the payloads really are\ncompressed. Column A points at a string table. The names are ordinary:\n`ad.ini`\n\n, `forcefed.ini`\n\n, `gl_*.p3d`\n\n. One of those names is useful.\n\n`ad.ini`\n\nis an adaptive-difficulty config. It almost certainly opens with a\ncomment. That is the whole attack. Pull the compressed chunk:\n\n```\nff 3b 20 23 20 41 44 41 50   →  · ; # ADAP\nff 54 49 56 45 20 44 49 46   →  · TIVE DIF\nff 46 49 43 55 4c 54 59 20   →  · FICULTY\n```\n\n`; # ADAPTIVE DIFFICULTY`\n\nis sitting in the stream, with `ff`\n\nevery eight\ncharacters. `0xff`\n\nmeans eight literals follow. That is LZSS: a flag byte\nwhose bits each say “literal” or “back-reference”. Incompressible banner\ntext goes through untouched.\n\nStorer and Szymanski, 1982, on top of LZ77 from 1977.\n\nEvery textbook LZSS layout decoded the literals and then produced garbage at the first back-reference. So I traced the bytes instead.\n\nLiterals until the first match give\n`…[GENERAL]\\r\\nFixedDifficulty=0\\r\\nLogging=1`\n\n. The next five bytes in the\nstream are `69 6e 67 3d 31`\n\n, which is `ing=1`\n\n. The output already ended in\n`Logging=1`\n\n, so those five bytes are more literals, not a match. The flag\nbits are consumed least-significant-first. I had been reading them the\nother way.\n\nLSB-first, the first real match is flag `0x1f`\n\n, operand `0x0110`\n\n. Read as a\n12-bit back-distance that is 272, which is larger than the 77 bytes emitted\nso far. Impossible. The operand is not a distance. It is a position in a\n4,096-byte ring buffer, pre-filled with spaces. Okumura’s LARC/LHarc\nlayout, the one behind LHA:\n\n```\nposition = b0 | ((b1 & 0xf0) << 4)\nlength   = (b1 & 0x0f) + 2\n```\n\nPosition 16 maps back to a `\\r\\n\\r`\n\nalready in the output. An INI file\nneeds that carriage return after a key. It fits.\n\n`ad.ini`\n\ndecompresses to 706 bytes, ending on a `[mtru2]`\n\nsection. That is\nthe size recorded in the directory. Then all 23 members of the test archive\ndecode to their recorded sizes. Then all 143 members of a real per-track AI\npack: configs, `CP3D`\n\ncar models, and the `AILD`\n\nracing-line data. 166\nfiles. If the bit packing were still wrong, it would miss.\n\nThe four steps, as used:\n\nThis is the sequel to [Reading a Binary Game Format in Ruby](/2026/06/30/reading-binary-in-ruby.html),\nwhich is the uncompressed BIGF container. JPAK is what TOCA 3 wrapped around\nthe same racing-line data.", "url": "https://wpnews.pro/news/reading-jpak", "canonical_source": "https://davidslv.uk/2026/08/23/reading-jpak.html", "published_at": "2026-08-23 00:00:00+00:00", "updated_at": "2026-08-23 06:42:33.551241+00:00", "lang": "en", "topics": ["artificial-intelligence"], "entities": ["TOCA Race Driver 3", "JPAK", "LZSS", "Storer", "Szymanski", "LZ77", "Okumura", "LHA"], "alternates": {"html": "https://wpnews.pro/news/reading-jpak", "markdown": "https://wpnews.pro/news/reading-jpak.md", "text": "https://wpnews.pro/news/reading-jpak.txt", "jsonld": "https://wpnews.pro/news/reading-jpak.jsonld"}}