Standalone TIM-pack format Confirmed
The “TIM-pack” is a reader, not a container of its own. The entries it opens - each scene's bundle of textures, and the two hidden entries at the head of PROT.DAT holding the cursor, menu glyphs and UI sprite sheets - are an ordinary pack sitting behind a 4-byte DATA_FIELD TIM_LIST chunk header. The reader skips that header, and its “marker byte” and +4 offset math are simply the header seen from the inside.
At a glance
- What it really is
- chunk header
(0x01 << 24) | payload_len+ an container-formats::pack of TIMs - Where on disc
- each scene's texture pack at raw-TOC
+4of its CDNAME block; raw TOC entries 0 and 1 (boot system UI, GAME OVER page) - Offset math
byte_offset = word_index * 4 + 4- Parser
legaia_prot::timpack(is_tim_pack,detected_ext); boot bundlelegaia_ui_formats::system_ui_bundle- Confidence
- Confirmed - retail uploader traced, layout pinned on the real disc
- Used by
- boot sequence, pause menu glyphs, asset viewer
Layout
Read as a TIM-pack, the first eight bytes are a discriminator and a member count; read as what they are, they are a chunk header whose top byte is the TIM_LIST type (0x01) and whose low 24 bits are the payload length, followed by the pack's own u32 count.
- Detection (
is_tim_pack):byte[3] == 0x01andbyte[2] < 0x10(the type byte, and the top of a payload length under 1 MB),tim_numpositive, and the offset table inside the blob.byte[2]is not the count. - Offsets are word indices relative to the pack, which starts four bytes in - hence
byte_offset = word_index * 4 + 4. The pack reader computes the same offsets from the pack's own start. - Member typing (
detected_ext): first byte0x10(TIM magic) →"TIM", else"BIN". The boot bundle below is why the “else” branch exists.
Raw TOC entries 0 and 1: the boot system UI and the GAME OVER page
The two entries at the head of PROT.DAT's TOC are both TIM-packs. They sit in the region the extraction index space skips - the game's own TOC counts entries 2 higher than this project's extraction, so raw entry p + 2 = extraction entry p (see PROT TOC). They are CDNAME's two head names, and they have different loaders. Raw entry 0 (init_data) is the system UI: the cursors, menu glyphs and sprite sheets that the boot image init.pak (PROT 0895) uploads once and never evicts, resident in video memory from the title screen through every scene and mode. Raw entry 1 (gameover_data) belongs to the GAME OVER screen and only that overlay loads it (loaders).
The coordinates below are framebuffer positions in the PSX's 1024×512 video memory (VRAM), where images and palettes both live as pixels. A CLUT is such a palette: a 16- or 256-colour lookup table stored as a row of framebuffer pixels.
Raw entry 0 (sectors 3..55) declares 20 members:
PROT.DAT offset | Member | VRAM placement |
|---|---|---|
0x01858 | boot cursor | - |
0x018E0 | system-UI sprite sheet | image (896,256) 64×192 |
0x07B00, 0x07F40 | UI elements | - |
| - | four small TIMs | CLUTs at (896, 498..=501) |
0x11218 | menu-glyph / interior-page atlas (extractors legaia_ui_formats::interior_page / menu_glyph_atlas) | image (960,256) 64×256, declared CLUT (0,510,16,16) |
0x19438 | UI sprite strip | image (960,400) 60×24 |
0x1A018..0x1AA7C | six bare row-patch members (below) | rows 456..462 of the atlas |
0x1AC90..0x1AED0 | four cursor-part TIMs | images at (976,256..) |
Raw entry 1's single TIM is the GAME OVER page at image (640,0) with a 256-entry CLUT declared (0,480,256,1), followed by a pack of 29 TMDs. No retail field, battle or menu save state holds its palette.
Because the bundle never leaves VRAM, field environment meshes can reference palette cells on row 510 that no scene TIM ever uploads (town01 env slots 21/26/74, rikuroa slots 50/51/63). In the port, the scene VRAM pre-pass uploads raw entry 0 the same way (SceneResources with BuildOptions::system_ui, through upload_boot_resident_to_vram), byte-exact against retail VRAM, and leaves raw entry 1 out.
Two member shapes that deviate from a plain TIM list
- Flat-strip CLUT upload. The retail per-TIM uploader writes every member TIM's CLUT block as a flattened
(clut_x, clut_y, w*h, 1)strip, NOT the declaredw × hrect - which for the row-510/511 banks would overflow VRAM aty >= 512. The bundle's strips populate CLUT rows 510/511 (fb_x0..320) plus the(896, 498..=501)/(976, 304..=307)side cells; raw entry 1's row-480 strip lands only when the GAME OVER overlay loads it. See npc-palette for the row layout and capture evidence. - Row-patch members. Six members carry no TIM magic: an 8-byte preamble followed by a bare TIM-style image block
[u32 bnum][u16 x][u16 y][u16 w][u16 h]+ halfword data (bnum = 12 + w*h*2). All six declare(960, y, 256, 1)fory ∈ {456, 457, 458, 460, 461, 462}- single rows patched over the atlas image, VRAM-edge-clipped to the visible 64 words. Live captures hold exactly these bytes over the disc atlas at those rows, in every phase.
How we know
Provenance
The retail per-TIM uploader FUN_800198E0 (ghidra/scripts/funcs/800198e0.txt) walks the pack with the word_index * 4 + 4 arithmetic and uploads each member's CLUT as a flat strip. Member offsets, VRAM placements and the six row-patch shapes are pinned on the real disc, and the engine's boot upload matches retail VRAM captures byte for byte.