Pack format (inside TIM_LIST and TMD chunks) Confirmed
The simplest way Legaia bundles several assets into one blob: a count, a table of offsets, then the assets packed back-to-back. When a scene needs a dozen textures or a handful of meshes at once, this is the shape they arrive in - the data payload of a TIM_LIST or TMD chunk inside a DATA_FIELD streaming container. It is one of three similar-looking pack layouts on the disc, each with different header math; the table below tells them apart.
At a glance
- Magic
- none - a bare
u32 countopens the pack; identity comes from the enclosing chunk's type byte (0x01TIM_LIST or0x02TMD) - Where on disc
- inside DATA_FIELD chunks in scene and effect entries of
PROT.DAT - Parser
crates/asset/src/pack.rs- Confidence
- Confirmed - the type-
0x02arm of the dispatcherFUN_8001F05Cwalks it and callsFUN_80026B4Cper member - Used by
- asset loader, asset viewer
Overview
Inside a streaming chunk the bytes say nothing about what they are - the chunk header already said "a list of TIMs" or "a pack of meshes". So the pack itself is minimal: how many members, where each starts, then the members. Offsets count 4-byte words from the start of the pack data, and there is no per-pack magic.
Implementation: crates/asset/src/pack.rs.
Layout
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | u32 | count | number of sub-assets |
+0x04 | u32 × count | word_offset[] | each in 4-byte words from the start of THIS pack data |
word_offset[0] * 4 | to chunk end | sub-assets | packed back-to-back, no padding rule beyond the 4-byte word grid |
Sub-asset i lives at byte range [word_offset[i] * 4 .. word_offset[i+1] * 4), with the last sub-asset ending at the chunk's end.
Example
A TIM_LIST chunk header followed by a 2-TIM pack (a TIM is the PlayStation's standard texture format):
chunk header: 6c 02 01 01 type=0x01 (TIM_LIST), size=0x01026C
pack count: 02 00 00 00 count = 2
offset[0]: 03 00 00 00 word offset 3 -> byte 12 (= start of TIM 0)
offset[1]: 8b 20 00 00 word offset 0x208B -> byte 0x822C (= start of TIM 1)
[then 2 PSX TIMs back-to-back]
Word offset 3 is byte 12 - exactly the size of the count word plus the two-entry table, so the first member starts right after the header.
Distinction from the standalone TIM-pack (and the third sibling)
Three pack formats coexist on the disc. They look alike in a hex dump and each has its own header math, so apply the one that matches the source:
| asset::pack (this page) | Standalone TIM-pack | Field-pack | |
|---|---|---|---|
| Lives | inside a DATA_FIELD TIM_LIST / TMD chunk | as a whole standalone PROT.DAT entry | as a whole PROT.DAT entry (scene TIM/TMD bundle) |
| Identified by | the enclosing chunk's type byte | byte[3] == 0x01, byte[2] < 0x10 | magic 0x01059B84 |
| Header | u32 count at +0 | 4 discriminator bytes, then u32 count at +4 | magic-prefixed; see its page |
| Offset table at | +4 | +8 | see its page |
| Member offset | word_index * 4 | word_index * 4 + 4 | see its page |
| Parser | crates/asset::pack | crates/prot::timpack | crates/asset field-pack detector |
The two header shapes side by side:
| Offset | asset::pack | Standalone TIM-pack |
|---|---|---|
+0x00 | u32 count | u8 magic_lo, u8 magic_hi, u8 disc (< 0x10), u8 0x01 |
+0x04 | u32 word_offset[0] | u32 tim_num |
+0x08 | u32 word_offset[1] ... | u32 word_index[0] ... |
member i at | word_offset[i] * 4 | word_index[i] * 4 + 4 |
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Chunk dispatcher | FUN_8001F05C (type-0x02 arm) | Walks the count + word-offset table and hands each member to the TMD handler | funcs/8001f05c.txt |
| Per-member handler | FUN_80026B4C | Consumes one member at word_offset[i] * 4, no +4 constant | funcs/80026b4c.txt |
Source of record: docs/formats/pack.md.