Asset type dispatcher Confirmed
How the game knows what a loaded blob of bytes is. A texture, a 3D model and a page of dialogue all arrive from the disc as anonymous bytes; what tells them apart is a single type byte travelling alongside each one - 0 for a TIM texture, 2 for a TMD mesh, 4 for MES dialogue, and so on. One routine in the executable reads that byte and routes the data to the right format handler, decompressing on the way if needed. Every per-format branch in the engine is reached through it.
At a glance
- Magic
- none - the type byte is the top 8 bits of a
u32packed with the 24-bit size - Where on disc
- every DATA_FIELD chunk header and every asset descriptor carries one
- Dispatcher
FUN_8001F05CinSCUS_942.54- Parser
crates/asset/src/lib.rs::AssetType- Confidence
- Confirmed - the dispatcher is dumped in full, and the developers' own malloc-error strings name each type (how we know)
- Used by
- asset loader, LZS decoder (the compressed path)
What a type byte is
Legaia's containers do not label their contents with a per-format magic word the way a PNG or a ZIP does. The container that carries an asset - a streaming chunk or a descriptor table - stores a one-byte code next to its size, and the code alone decides what happens next: which buffer is allocated, whether the bytes are copied or LZS-decompressed, and which subsystem is told "your data has arrived". The code space is a dozen values, fixed in the executable, so the same table serves every scene, battle and menu on the disc.
copy_only argument selects copy versus LZS decode; the caller ORs the returned bits into a "what was in this stream" summary.Calling convention
result = FUN_8001f05c(byte *src_data, u32 type_and_size, int param3, int copy_only);
type_and_sizepacks type in the high 8 bits, size in the low 24 bits.copy_only != 0→ the asset is uncompressed and is copied into its buffer.copy_only == 0→ the asset is LZS-compressed and goes through the LZS decoder, the game's dictionary decompressor.
Type table
The full type space, with the bit each type contributes to the return value. The names are the developers' own: each type has a per-type malloc-error string in the executable.
| Type byte | Name | What it is | Handling | Return |
|---|---|---|---|---|
0x00 | TIM | one texture | 0x11800-byte buffer. TIM | 0x01 |
0x01 | TIM_LIST | a pack of textures | 0x70800-byte buffer. pack | 0x01 |
0x02 | TMD | a pack of meshes | Mesh installer called per submesh. TMD | 0x02 |
0x03 | MAN | scene script + tables | Raw load | 0x04 |
0x04 | MES | dialog text | MES | 0x08 |
0x05 | MOVE | animation bundle | Raw load. Carries the per-scene player ANM bundle despite the label | 0x10 |
0x06 | ANM | CLUT-walk table | Not the player ANM source - the palette-cycling table for water and kingdom scenes | 0x20 |
0x07 | VDF | vertex-deformation pack | Morph-delta pack, post-processed per sub-entry | 0x40 |
0x08 | SIN | - | Raw load | 0x80 |
0x09 | TMD2 | one mesh | Single bare TMD blob, same format as one TMD-pack member; parse with crates/tmd::parse | 0 or asset-error bit |
0x0B | MOVE2 | animation bundle | Raw load with cleanup of the prior buffer | 0x10 (same as MOVE) |
0x0A | FLAG | marker | No malloc, no decompress, no register | 0xA00 |
0x0F | FLAG | marker | As above | 0xF00 |
0x14 | FLAG | marker | As above | 0x1400 |
Type-byte details: the MOVE / ANM label swap, and VDF
0x05 MOVEis where the per-scene player ANM bundle lives on disc: a canonical ANM container withmarker_1 = 0x080Crecords (ANM). The dispatcher's "ANM malloc-err" string at case 6 indexes this type, not type0x06.0x06 ANMcarries the CLUT-walkMoveImagewalker table: case 6 installs it atDAT_8007B7C8andFUN_8001ADA4case0xBsteps it. Populated in 12 bundles - the 3 kingdoms plus 9 water / waterfall field scenes - and a 4-bytecount = 0placeholder everywhere else. See field ambient FX; parserlegaia_asset::clut_walk::from_scene_bundle.0x07 VDFis installed atDAT_8007B7DCand post-processed byFUN_8001FBCCper sub-entry (pointer table0x80083E58). Populated in 61 scene bundles; parserlegaia_asset::scene_vdf.
Return-value bitfield
Every return is a small bitfield - one bit per data-bearing type, and type << 8 for the markers. Both callers (the streaming walker and the descriptor walker) OR every return into one accumulator, so the result summarises what a whole stream contained:
return_value & 0x00FF = bit per data-bearing asset type seen
return_value & 0xFF00 = bit per FLAG-type marker seen
Why FLAG types exist
The FLAG cases let the dispatcher accept chunks whose type byte falls outside the data-bearing range without aborting the streaming walk. The walker still skips the data bytes (advance = 4 + (size & ~3)), but the dispatcher never reads them. FLAG chunks are stream-level out-of-band markers: any caller that looks at the returned bitfield can detect "this stream contained a 0x14-typed marker" without the marker carrying a parsed asset. Where the markers are consumed is open - possibly by code that reads past the streaming terminator (the DATA_FIELD trailer).
AssetType Rust enum
pub enum AssetType {
Tim, // 0
TimList, // 1
Tmd, // 2
Man, // 3
Mes, // 4
Move, // 5
Anm, // 6
Vdf, // 7
Sin, // 8
Tmd2, // 9
Move2, // 0xB
Flag(u8), // 0xA, 0xF, 0x14
Unknown(u8),
}
Who calls the dispatcher
Two call sites exist, and only one is visible from the executable's static call graph. The other is reached from the town/field overlay - a chunk of MIPS code the game loads into RAM on demand, where most game logic lives. Zero static callers does not mean dead.
| Caller | Address | Reached from |
|---|---|---|
Streaming walker, 0x14 (DATA_FIELD) branch | FUN_8002541C | SCUS_942.54 static call graph |
| Descriptor-pair walker | FUN_80020224 | Town/field overlay: FUN_801D6704 → 0x801D6B0C with a0 = 0; see asset descriptor |
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Asset-type dispatcher | FUN_8001F05C | The type table, buffer sizes, copy-vs-decode branch and return bits | ghidra/scripts/funcs/8001f05c.txt |
| memcpy | FUN_8001A8B0 | The copy_only != 0 path | ghidra/scripts/funcs/8001a8b0.txt |
| LZS decoder | FUN_8001A55C | The copy_only == 0 path | ghidra/scripts/funcs/8001a55c.txt |
| Mesh installer | FUN_80026B4C | Called per submesh for TMD, directly for TMD2 | ghidra/scripts/funcs/80026b4c.txt |
| Malloc-error strings | s_Tim_Malloc_Err_800104E8, s_tmd_malloc_err_80010504, … | The developers' own name for each type byte | SCUS_942.54 string table |
Full page: docs/formats/asset-type.md.