Asset descriptor format Confirmed
When a town loads, the game pulls one compressed bundle off the disc and has to find the pieces inside it - the meshes, textures and animations that become the scene. The asset descriptor is that bundle's table of contents: a count, then a list of (type_size, data_offset) pairs, each pointing at one asset in the buffer and saying what kind it is.
At a glance
- In the game
- Every field scene load - the town overlay's init walks one of these to install the scene's assets.
- Magic / marker
- None - a leading
u32 countand a header word, then the pairs. - Lives in
- In RAM, at the head of a decoded buffer; PROT 0874 (
player.lzs, extraction-index space) is the three-descriptor instance the character mesh pack is read through. - Retail reader
- The descriptor walker, reached from the town overlay's init (how we know).
- Parser
legaia_container_formats::parse_player_lzs(crates/container-formats/src/lib.rs).- Confidence
- Confirmed - walker disassembled, overlay caller traced, and the second header word measured across every carrier on the disc.
Layout
Two header words, then a flat array of 8-byte pairs. data_offset is byte-relative to the buffer start; type_size packs the asset-type byte (the 8-bit tag saying TIM / TMD / MES / …) in its high byte with the payload's decompressed size in the low 24 bits. Each payload is an LZS stream when the container is a player.lzs-style chain.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | count | Number of descriptor pairs that follow. |
+0x04 | 4 | total_decompressed_size | The sum of the descriptors' decompressed sizes - see the second header word. Retail never reads it, and editors must keep it consistent. |
+0x08 + 8i | 4 | type_size_i | (type << 24) | size: asset-type byte in the high byte, size in the low 24 bits. |
+0x0C + 8i | 4 | data_offset_i | Byte offset of asset i from the buffer start. |
The second header word
The word at +0x04 is the container's total decompressed size: the sum of every descriptor's size field. Across the 105 carriers on the disc the identity holds 105 times out of 105, and in all 105 the sum is larger than the PROT entry that carries it - which is what rules out reading it as a file size or a sector count. It is where the last asset ends once the container has been unpacked into the runtime buffer, not a position inside the entry.
Retail never reads it. The walker takes its loop count from +0x00 and steps descriptor pairs from +0x08, so +0x04 is never addressed. Sweeping the executable and every extracted overlay for dereferences of the table-base pointer _DAT_8007B85C - including the lui+load and materialised-base forms an absolute-address scan cannot see - finds one store that publishes the buffer base and four sites that dereference the pointer, every one of them at displacement 0x0; every other site hands the pointer straight to a callee as a load destination.
Inert at runtime is not the same as free to change. The word is an authoring total, so an editor that rewrites a descriptor's size has to keep it consistent; the Rust side checks it with SceneAssetTable::total_size_is_consistent.
Where it appears
- On the disc, at the top level. A structural sweep of all 1233 PROT entries (the numbered slots of
PROT.DAT, the disc's main asset archive) finds 105 entries wearing this header at offset 0 - the scene bundles, theplayer.lzs-style character and effect containers, and the minigame art container. An earlier reading of "zero hits" came from a detector that demanded a fixed descriptor count; the count word varies (80 carriers use 7, the rest 1, 3, 4, 5 or 6). The same corpus is what scene bundles classes asscene_asset_table. - Reached only through an overlay. The executable's static call graph never touches the walker - it looks like dead code until the town overlay's init is followed (how we know). This "zero static callers but alive via overlay" pattern recurs across the whole codebase; treat a zero-xref result as "needs an overlay sweep", never as "dead".
- Installs the party's field meshes. The same walk registers PROT 0874 §0's five TMDs into the model slots the field-form character meshes draw from.
The Rust parser is named parse_player_lzs after the container it was first read through; the format itself is not player-specific.
How we know
Provenance: the walker, its overlay caller, and the TMD handler chain
| Function | Address | What it proves |
|---|---|---|
| Descriptor walker | FUN_80020224 | Reads count, then walks the (type_size, data_offset) pairs and dispatches on the high type byte. |
Town overlay MAIN_INIT | FUN_801D6704, call at 0x801D6B0C | Calls the walker with a0 = 0 on every field scene load; result stored at 0x80087AF8. Zero static xrefs exist in SCUS_942.54. |
| Type-2 (TMD) handler chain | FUN_8001F05C case 2 → FUN_80026B4C | Registers PROT 0874 §0's five TMDs into DAT_8007C018[0..=4] - the field character-mesh slots. |