At a glance

Magic
none - the type byte is the top 8 bits of a u32 packed with the 24-bit size
Where on disc
every DATA_FIELD chunk header and every asset descriptor carries one
Dispatcher
FUN_8001F05C in SCUS_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.

type:8 | size:24 u32 in chunk header dispatcher FUN_8001F05C memcpy copy_only != 0 LZS decode copy_only == 0 per-type handler TIM TMD MES ... return 1 bit / type
One call per asset: the packed word selects the buffer and the handler; the 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_size packs 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 byteNameWhat it isHandlingReturn
0x00TIMone texture0x11800-byte buffer. TIM0x01
0x01TIM_LISTa pack of textures0x70800-byte buffer. pack0x01
0x02TMDa pack of meshesMesh installer called per submesh. TMD0x02
0x03MANscene script + tablesRaw load0x04
0x04MESdialog textMES0x08
0x05MOVEanimation bundleRaw load. Carries the per-scene player ANM bundle despite the label0x10
0x06ANMCLUT-walk tableNot the player ANM source - the palette-cycling table for water and kingdom scenes0x20
0x07VDFvertex-deformation packMorph-delta pack, post-processed per sub-entry0x40
0x08SIN-Raw load0x80
0x09TMD2one meshSingle bare TMD blob, same format as one TMD-pack member; parse with crates/tmd::parse0 or asset-error bit
0x0BMOVE2animation bundleRaw load with cleanup of the prior buffer0x10 (same as MOVE)
0x0AFLAGmarkerNo malloc, no decompress, no register0xA00
0x0FFLAGmarkerAs above0xF00
0x14FLAGmarkerAs above0x1400
Type-byte details: the MOVE / ANM label swap, and VDF
  • 0x05 MOVE is where the per-scene player ANM bundle lives on disc: a canonical ANM container with marker_1 = 0x080C records (ANM). The dispatcher's "ANM malloc-err" string at case 6 indexes this type, not type 0x06.
  • 0x06 ANM carries the CLUT-walk MoveImage walker table: case 6 installs it at DAT_8007B7C8 and FUN_8001ADA4 case 0xB steps it. Populated in 12 bundles - the 3 kingdoms plus 9 water / waterfall field scenes - and a 4-byte count = 0 placeholder everywhere else. See field ambient FX; parser legaia_asset::clut_walk::from_scene_bundle.
  • 0x07 VDF is installed at DAT_8007B7DC and post-processed by FUN_8001FBCC per sub-entry (pointer table 0x80083E58). Populated in 61 scene bundles; parser legaia_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.

CallerAddressReached from
Streaming walker, 0x14 (DATA_FIELD) branchFUN_8002541CSCUS_942.54 static call graph
Descriptor-pair walkerFUN_80020224Town/field overlay: FUN_801D67040x801D6B0C with a0 = 0; see asset descriptor

How we know

FunctionAddressWhat it provesDump
Asset-type dispatcherFUN_8001F05CThe type table, buffer sizes, copy-vs-decode branch and return bitsghidra/scripts/funcs/8001f05c.txt
memcpyFUN_8001A8B0The copy_only != 0 pathghidra/scripts/funcs/8001a8b0.txt
LZS decoderFUN_8001A55CThe copy_only == 0 pathghidra/scripts/funcs/8001a55c.txt
Mesh installerFUN_80026B4CCalled per submesh for TMD, directly for TMD2ghidra/scripts/funcs/80026b4c.txt
Malloc-error stringss_Tim_Malloc_Err_800104E8, s_tmd_malloc_err_80010504, …The developers' own name for each type byteSCUS_942.54 string table

Full page: docs/formats/asset-type.md.

See also