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. There is exactly one pack format on the disc; the "standalone TIM-pack" and the "field-pack" are this same pack read from behind a 4-byte chunk header, as the last section shows.
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
legaia_container_formats::pack- 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.
Layout
Each word_offset counts 4-byte words from the start of this pack's data (not from the end of the table), and members sit on the 4-byte word grid with no other padding rule.
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):
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.
One format, two readers
A hex dump of a scene's texture entry looks like a second pack format: a marker byte 0x01 at +3, a count at +4, and member offsets that need an extra +4. It is not a second format. Those first four bytes are a DATA_FIELD chunk header, (TIM_LIST << 24) | payload_len, and what follows is an ordinary pack. The older prot::timpack reader simply reads the pack with the header still attached, which is where both its marker test and its +4 come from. The "field-pack" word 0x01059B84 is the same chunk header on town01's texture pack.
+4 in the older reader is the chunk header it did not step over; the "marker byte" it tests is that header's type.| Offset in the entry | Read as a chunk + pack | Read by prot::timpack |
|---|---|---|
+0x00 | chunk header; its high byte 0x01 is the TIM_LIST type | "magic" bytes; byte[3] == 0x01 is the marker test |
+0x04 | pack count | tim_num |
+0x08 | pack word_offset[0] … | word_index[0] … |
member i | 4 + word_offset[i] × 4 | word_index[i] × 4 + 4 - the same byte |
Use legaia_container_formats::pack on a pack that starts at offset 0 (a chunk payload), and prot::timpack - or skip the 4-byte header first - on a whole chunk. legaia_scene_formats::field_pack::scene_pack locates the chunk-headered scene pack inside a CDNAME block. The former effect bundle (0x02018B0C) is not a different layout: it is this pack behind a type-0x02 chunk header, carrying 29 TMDs.
How we know
The chunk dispatcher's type-0x02 arm walks the count and word-offset table and hands each member to the TMD handler, which consumes it at word_offset[i] × 4 with no added constant.
Provenance: the two routines
| 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.