Skip to content

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):

PartContent
u32Number of texture blocks (1; 2 in HABILAGE)
per blockA 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 tablesHABILAGE, 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 restThe 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.

FileLoaded byTables
FNT_MENUFUN_0046e7bc (Fnt_Menu.Bft, at start-up)Typo041.fnt … Typo043.fnt: the menu fonts (68 to 83 glyphs)
HABILAGEload_font_file (Habilage.Bft, with a track)typo020 … typo032, typo040: the race display fonts that HABI.BIN names (lists.md)
J_GAME, J_MENUnot found: no string in WINPOD.EXE names themlabels 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 i equals decoded byte i - 0x3FFC XOR the key's byte i mod 4. Every byte of both files fits (container.md, "Leftovers").
  • In SHADOTIN (one block), it is zero up to 0x3F84, 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.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.