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/container-formats/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);

The second argument is the same little-endian word a DATA_FIELD chunk header stores, so in memory it reads size first, type last:

  • 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 NPC / prop ANM bundle despite the label0x10
0x06ANMCLUT-walk tableNot an ANM clip 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 NPC / prop ANM bundle lives on disc: a canonical ANM container with marker_1 = 0x080C records (ANM). Its arm allocates with the move_malloc_err string into _DAT_8007B888, the buffer the clip driver FUN_800204F8 binds clips from.
  • 0x06 ANM carries the CLUT-walk MoveImage walker table despite its anm_malloc_err label: case 6 installs it at DAT_8007B7C8, FUN_80024CFC spawns one walker actor per entry (state 0xB), 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_scene_formats::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_scene_formats::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:

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. It is not the bytes past the streaming terminator: the DATA_FIELD trailer is last-sector fill that nothing reads.

In the port

legaia_container_formats::AssetType mirrors the table one variant per type byte, with Flag(u8) for the three markers and Unknown(u8) for everything else - the same silent no-op the retail dispatcher performs (below).

Who calls the dispatcher

Three call sites exist, and only one is visible from the executable's static call graph. The others are reached from overlays - chunks 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_801D6704 → 0x801D6B0C with a0 = 0; see asset descriptor
In-overlay DATA_FIELD walk0x801D4DA8PROT 0976, the Baka Fighter minigame overlay. It re-implements the same walk, passes copy_only = 1, and discards the return value — so the flag bitfield above is a property of the two SCUS callers only.

What an unrecognised type byte does

242 of the 256 possible bytes are unrecognised: the seven in-range gaps 0x0C, 0x0D, 0x0E, 0x10–0x13, and everything from 0x15 up. The range check sltiu $v0, $v1, 0x15 sends the high ones to 0x8001F660, and the jump table sends the seven gaps to exactly the same address.

That address is not a default handler — it is the function's return tail. The accumulator register is zeroed on entry and nothing on an unrecognised path writes it, so the call returns 0 having done nothing: no allocation, no decompression, no copy, no registration, and no error raised. It is a silent no-op. A reference sweep of the jump table's own address range across 84 images finds one hit, the instruction that indexes it, so nothing installs a handler into the empty slots at runtime.

How we know

Provenance: the dispatcher, its two decode paths, and the malloc-error strings
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

Source of record: docs/formats/asset-type.md.

See also