Player-character mesh pack Confirmed
The five Legaia TMDs the retail engine keeps resident at DAT_8007C018[0..=4] every field scene - Vahn, Noa, Gala, plus two smaller auxiliary actors. They live in the head LZS section of PROT entry 0874 (befect_data); slot identity is asserted by the on-disc layout and the active-party equipment-swap patch in FUN_8001EBEC. Implementation: legaia_asset::character_pack. Surfaced live on the characters page.
On-disc layout
PROT entry 874 is a parse_player_lzs(buf, 3)-shaped container with three LZS-compressed sections. Section 0 decompresses to a canonical pack with five Legaia TMDs:
| Slot | Body offset | nobj (disc) |
Body bytes (runtime) | Role |
|---|---|---|---|---|
| 0 | 0x0018 | 12 | 13 220 | Vahn - active party slot 0 |
| 1 | 0x33BC | 12 | 13 800 | Noa - active party slot 1 |
| 2 | 0x69A4 | 12 | 11 656 | Gala - active party slot 2 |
| 3 | 0x972C | 3 | 6 488 | Savepoint (save crystal) |
| 4 | 0xB084 | 2 | 1 048 | Auxiliary actor (untriaged) |
The “runtime body bytes” column is what the LZS-bounded decode produces. Slot 4's underlying compressed stream would expand to roughly 20 KB if decoded unbounded, but the descriptor's compressed-size hint caps the decode at the first ~46 KB so slot 4 receives only its 1 048-byte TMD prefix - byte-equality verified against the live DAT_8007C018[4] allocation (see world-map-overlay § Disc-side source of [0..4]). The character pack is shared across every field scene; only the trailing field-pack at [5..] changes per scene.
TMD shape
Each pack body is a Legaia TMD with the canonical 12-byte header followed by nobj × 0x1C group descriptors:
+0x00 u32 magic = 0x80000002
+0x04 u32 flags (= 1 post-fixup)
+0x08 u32 nobj ; 12 / 12 / 12 / 3 / 2 on disc
+0x0C group descriptors ; 0x1C bytes each
Inside the active-party slots (0..=2), groups 10 and 11 are templates for the equipment-conditional swap below; the engine caps live group_count to 10 at install time so the templates aren't drawn directly.
10-group cap + equipment-conditional swap
FUN_8001E890 overwrites DAT_8007C018[party_base + 0..2]'s entry[+0x08] (TMD group_count) to 10 after the install, capping each active-party TMD at 10 live groups. The last two disc groups (10 and 11) are the equipment-conditional templates the per-frame patch loop picks between.
FUN_8001EBEC runs that loop. For each of the three active-party slots:
- Read the equipment toggle byte at the character record's per-slot offset.
- If the byte is non-zero, source the group-10 template at
TMD+0x124. If it's zero, source the group-11 template atTMD+0x140. - Overwrite the visible group descriptor at the slot's patched group index with that 28-byte template.
| Party slot | Character | Patched group | Equip-byte record offset | Template-zero (TMD+0x140) |
Template-nonzero (TMD+0x124) |
|---|---|---|---|---|---|
| 0 | Vahn | 0 | +0x196 | group 11 | group 10 |
| 1 | Noa | 3 | +0x199 | group 11 | group 10 |
| 2 | Gala | 5 | +0x19B | group 11 | group 10 |
(The “patched group index” and the offset-within-the-equip-byte-window are the same three numbers {0, 3, 5} - retail's FUN_8001EBEC reuses one tiny stack table for both roles.)
The swap is binary: each character has exactly one visible mesh group that toggles between two pre-baked variants. Different equipped items don't each get their own mesh swap; the toggle is a single bit (“weapon-bearing group is on / off”) and item identity is conveyed by the character's texture atlas, not by mesh changes.
legaia_asset::character_pack::equipment_swap::apply is the clean-room equivalent: given a slot's disc-form TMD bytes, a PatchSlot, and the character's equipment toggle byte, it returns the patched TMD buffer.
Textures
The field-form character TMDs reference texture pages and CLUTs the engine uploads from PROT 0876 (player_data), the streaming-format file with a VAB + a 256×256 TIM_LIST atlas + a small SEQ trailer. The atlas goes to VRAM fb=(768, 0) with CLUT at (0, 500); both blocks are pinned in FIELD_SHARED_BLOCKS so they survive every field-scene transition without being re-uploaded. PROT 0874 itself carries no character textures - its remaining sections are the effect 3D models (etmd.dat) and effect-texture TIMs (etim.dat), unrelated to the player mesh.
The field-form atlas (above) serves the field form only; the battle form (below) carries its own seven TIM atlases.
Battle form - assembled from the player files
A real main-game battle does not render any disc TMD directly: at battle setup the engine assembles each active party member's higher-detail mesh from that character's player battle file (data\battle\PLAYER<n>, extraction 0863..0866 - see the player battle files), picking one section per equipment slot by the character's equipped item ids (char record +0x196..+0x19A), and installs the merged TMD into DAT_8007C018[0..=2]. Chain (static SCUS): FUN_80052770 case 4 (section select) → FUN_80052FA0 (assembler, blob at ctx+0x50) → FUN_800536BC ×5 (object splice: relocated object entries, nobj += section_nobj, bone-id byte per object, surplus objects tagged as the equipment's visual meshes) → FUN_80053898 (retag 200/201/100+, attach bones at blob+nobj, sort) → FUN_800513F0 registers blob+0x18. Runtime nobj = skeleton bones + equipment extras (Vahn 15+2=17) - this closes the equipped-weapon-mesh hunt. The clean-room engine renders the battle party the same way: play-window assembles each member from their player file (legaia_asset::battle_char_assembly), applies the registration texture-band rewrite engine-side (relocate_tsb_cba, the FUN_80053a28 port: CLUT row 481 + slot, runtime texpages at y=256), and poses + animates it from the character's own idle keyframe stream in record[0] of the same player file (action-offset table at the record head, monster-format [parts][frames][9-byte TRS] stream at entry +0xAC, parts = skeleton bones; channel i drives object i, equipment extras ride their attach bone - live-pinned: a mid-battle capture's anim context points at exactly this stream, and no PROT 1203 record is resident). PROT 1204 remains the default-gear fallback, posed from its PROT 1203 bank idle (bone i → 1204 object i - 1204's own object order, which differs from the assembled blob's sorted bone-tag order, so 1203 must never pose the assembled mesh).
Provenance (byte-verified, full-party save). DAT_8007C018[0] = 0x80165E38 = the assembler's ctx+0x50 blob + 0x18 exactly; the assembled TMD reads nobj=17, bone-id bytes [0..14, 200, 201], attach array [5, 8]. With Vahn equipped Hunter Clothes / Survival Knife / Ra-Seru Meta, every one of the 17 object vertex pools byte-matches a PLAYER1 section, equipment-selectively (body only in the id 0x43 section, weapon objects only in id 0x22, the Ra-Seru extra in the Meta tiers, defaults elsewhere). The five equipped-variant objects appear nowhere in PROT 1204; the other 12 default-section objects are byte-shared with 1204 - which is what an earlier partial-match attribution ("battle renders PROT 1204, 12/17") was actually seeing. (Two earlier reads are superseded in turn: "battle reuses the field pack", then "battle renders PROT 1204 directly". FUN_8001EBEC remains a pose toggle that never changes the object count.)
The Baka Fighter fist-fight minigame loads PROT 1204 (other5), whose five pre-assembled meshes are the same characters with default equipment - it lets you play as Vahn / Noa / Gala, so it borrows the battle models (overlay_baka_fighter loads data\field\other5.lzs + PROT 1205/1206; debug string "OTHER5 %d %d"). It is a shared battle/minigame pack, not a minigame-exclusive roster. The captured battle loader FUN_800520F0 tmd_registers PROT 0x36a into the effect window DAT_8007C018[3..] (etmd.dat), not the party [0..=2]. The party-mesh install into [0..=2] is static SCUS, pinned by a DAT_8007C018[0..2] write-watchpoint at battle entry: FUN_800513F0 (lead/active actors - tmd_register(*(actor+0x50)+0x18) in a while<3 loop, alongside the FUN_80052FA0 palette decode) and FUN_800542C8 (additional members - per-member loop tmd_register(*(*rec+4))). Both are dispatched indirectly as battle state-handlers, so a static 0x8007C018 cross-reference finds no writer - which is why the install was long mis-assumed to live in an overlay. Installed pointers byte-match the battle form (Vahn → 0x80165F48).
FUN_800513F0's while<3 loop is gated per slot by DAT_8007bd10[i], the active-member ID (1=Vahn / 2=Noa / 3=Gala / 0=empty) - not a 0/1 flag. A Vahn-solo fight ([1,0,0,0]) installs only slot 0 there, with FUN_800542C8 preloading the rest; a full party ([1,2,3,0]) installs all three through the loop. Confirmed against the full-party battle save states mc1/mc6/mc7 (game_mode 0x15, party_count=3, DAT_8007C018[0..2] = 0x80165E38 / 0x8017A908 / 0x8018D550 - all three battle-form meshes).
PROT 1204 is a flat streaming-format container (no LZS wrapper) with five chunks of asset type 0x09 (TMD2) plus a terminator plus seven trailing TIMs at fixed 0x8224 stride:
| Region | Offset | Type | Size | Role |
|---|---|---|---|---|
| chunk 0 | 0x000004 | TMD2 | 33 516 | Vahn battle (nobj=15) |
| chunk 1 | 0x0082F4 | TMD2 | 33 636 | Noa battle (nobj=16) |
| chunk 2 | 0x01065C | TMD2 | 24 780 | Gala battle (nobj=15) |
| chunk 3 | 0x01672C | TMD2 | 27 036 | Extra fighter (nobj=20) |
| chunk 4 | 0x01D0CC | TMD2 | 33 340 | Extra fighter (nobj=15) |
| atlas 0 | 0x025804 | TIM | ~33 312 | 256×256 4bpp + 256×1 CLUT @ (0, 490) |
| atlas 1 | 0x02DA28 | TIM | ~33 312 | CLUT @ (0, 491) |
| atlas 2 | 0x035C4C | TIM | ~33 312 | CLUT @ (0, 492) |
| atlas 3 | 0x03DE70 | TIM | ~33 312 | CLUT @ (0, 493) |
| atlas 4 | 0x046094 | TIM | ~33 312 | CLUT @ (0, 494) |
| atlas 5 | 0x04E2B8 | TIM | ~33 312 | CLUT @ (0, 495) |
| atlas 6 | 0x0564DC | TIM | ~23 332 | CLUT @ (0, 497) - truncated, last in pack |
Slots 0–2 are Vahn / Noa / Gala default-equipment battle models (their geometry byte-shared with the player files' default sections); slots 3 and 4 carry two extra battle/minigame fighters.
asset battle-char-pack extracted/PROT/1204_other5.BINasset battle-char-pack extracted/PROT/1204_other5.BIN --slot 0 --out-tmd vahn_battle.tmdasset battle-char-pack extracted/PROT/1204_other5.BIN --atlas 0 --out-tim vahn_atlas.timAnimation
Per-character animation data is not in PROT 0874. The runtime per-action record consumed by the actor tick FUN_80021DF4 and the overlay-resident per-frame animator lives in the ANM container (asset type 0x06); the actor receives a record pointer via FUN_80024CFC (actor[+0x4C] = anm_base + record_offset). Battle actions feed through a parallel consumer struct at actor[+0x234] - see ANM § Per-actor anim state offsets.
Readers (retail)
| Function | Role |
|---|---|
FUN_80020224 → FUN_8001F05C case 2 → FUN_80026B4C | Single descriptor-walk that installs PROT 0874 §0's 5 TMDs into DAT_8007C018[0..=4]. The retail loader chain that calls FUN_8001F05C with PROT 0874 specifically is not yet pinned; the engine routes the disc bytes directly through this parser. |
FUN_8001E890 | “DATA_FIELD player loader” - post-install, caps entry[+0x08] = 10 for the three active-party slots, then dispatches the per-character equipment-conditional patch to FUN_8001EBEC. |
FUN_8001EBEC | Per-frame group-descriptor patch. Reads the equipment toggle byte and copies one of the two templates over the visible group descriptor. |
CLI
asset character-pack extracted/PROT/0874_befect_data.BINasset character-pack extracted/PROT/0874_befect_data.BIN --slot 0 --equip 1 --out vahn_equipped.tmd