Fonts, menu skin and effects (POLICES/)
Reader: decoders/bp3.py, font.rs (.BP3), decoders/bi2.py, effect.rs (.BI2). State: decoded to the last byte, all 4 .BP3 and 3 .BI2 files (measured, 2026-10-23).
.BP3: fonts and menu skin
FNT_MENU (menu font), J_GAME and J_MENU (in-race and menu labels drawn as pictures: "your best", "lap record"), HABILAGE (the menu skin: frames, buttons, letters). In the block container, key 0x0FAE. The game's readers: FUN_0046e620, FUN_0046e7bc.
After the container header (observed in FUN_0046e7bc and load_font_file, FUN_0046e620; measured on all 4 files):
| Part | Content |
|---|---|
| u32 | Number of texture blocks (1; 2 in HABILAGE) |
| per block | A name (strings.md: Gothic3.pag, habille.PAG, polka.PAG); in FNT_MENU a 0x300-byte palette (the copy FUN_0046e7bc reads); a texture block (textures.md) of 8-bit 256 × 256 pages with its palette inside the block |
| glyph tables | HABILAGE, FNT_MENU: u32 count, then per table a font name (typo025, Typo042.fnt) and the table. J_GAME, J_MENU: one table alone, with no count and no font name |
| the rest | The writer's leftover buffer (container.md, "Leftovers"): every byte fits |
A glyph table (read_glyph_table, FUN_0046e0e0): a texture name, u32 glyph count, then 21 bytes per glyph: u8 character (@ is stored for the space and read back as one), u32 page of that texture, i32 x0, y0, x1, y1 on the page.
| File | Loaded by | Tables |
|---|---|---|
FNT_MENU | FUN_0046e7bc (Fnt_Menu.Bft, at start-up) | Typo041.fnt … Typo043.fnt: the menu fonts (68 to 83 glyphs) |
HABILAGE | load_font_file (Habilage.Bft, with a track) | typo020 … typo032, typo040: the race display fonts that HABI.BIN names (lists.md) |
J_GAME, J_MENU | not found: no string in WINPOD.EXE names them | labels drawn as pictures: "your best", "lap record" (17 and 64 entries) |
Until 2026-10-23 the readers took FNT_MENU's pages from right after the records; the palette copy inside the block comes first, so they drew three rows of palette bytes at the top of page 0 and lost the last three rows of page 2. Fixed in textures.py and texture.rs.
Each glyph or label is also a sub-texture of its page (textures.md). Long names fill the 32-byte name field with no zero (best_3_laps.T). 4 files, 12 pages, 19 glyph tables, 667 glyphs.
.BI2: 32-bit effect textures
PVRSMOKE (smoke), PVRSHADO (car shadow) and SHADOTIN (a small shadow); in the block container with the track key 0x0F7E. State: decoded for all 3 files (measured, 2026-10-23: decoders/bi2.py decode(r) reads every byte; effect.rs reads all three).
After the container header: a name (strings.md: a length byte, then the text) that starts with IMAP, u32 width, u32 height, then 4-byte pixels in the order B, G, R, A (measured by drawing them). PVRSMOKE and PVRSHADO have the 4-byte name IMAP and 256 × 256 pixels. SHADOTIN has a 44-byte name, IMAP and 39 bytes of what looks like leftover memory, then 32 × 32 pixels. "PVR" suggests the PowerVR chip (inferred). The shadow's alpha is 0 everywhere: how it is drawn is open.
After the pixels, the rest of the last block is leftover writer memory (measured):
- In the 17-block files, it is the previous block's bytes as the writer's buffer still held them, encoded: decoded byte
iequals decoded bytei - 0x3FFCXOR the key's bytei mod 4. Every byte of both files fits (container.md, "Leftovers"). - In
SHADOTIN(one block), it is zero up to0x3F84, then 120 bytes of unknown content.
Writer
Font::encode and Effect::encode (crates/pod-formats/src/font.rs, effect.rs): round-trip on all 4 fonts and 3 effects (measured). The font's leftover buffer is made again by its rule (each byte is the byte one block earlier XORed with the key); the effects' tail is kept as stored.