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
00x00181213 220Vahn - active party slot 0
10x33BC1213 800Noa - active party slot 1
20x69A41211 656Gala - active party slot 2
30x972C36 488Savepoint (save crystal)
40xB08421 048Auxiliary 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:

  1. Read the equipment toggle byte at the character record's per-slot offset.
  2. If the byte is non-zero, source the group-10 template at TMD+0x124. If it's zero, source the group-11 template at TMD+0x140.
  3. 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)
0Vahn0+0x196group 11group 10
1Noa3+0x199group 11group 10
2Gala5+0x19Bgroup 11group 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:

RegionOffsetTypeSizeRole
chunk 00x000004TMD233 516Vahn battle (nobj=15)
chunk 10x0082F4TMD233 636Noa battle (nobj=16)
chunk 20x01065CTMD224 780Gala battle (nobj=15)
chunk 30x01672CTMD227 036Extra fighter (nobj=20)
chunk 40x01D0CCTMD233 340Extra fighter (nobj=15)
atlas 00x025804TIM~33 312256×256 4bpp + 256×1 CLUT @ (0, 490)
atlas 10x02DA28TIM~33 312CLUT @ (0, 491)
atlas 20x035C4CTIM~33 312CLUT @ (0, 492)
atlas 30x03DE70TIM~33 312CLUT @ (0, 493)
atlas 40x046094TIM~33 312CLUT @ (0, 494)
atlas 50x04E2B8TIM~33 312CLUT @ (0, 495)
atlas 60x0564DCTIM~23 332CLUT @ (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.BIN
asset battle-char-pack extracted/PROT/1204_other5.BIN --slot 0 --out-tmd vahn_battle.tmd
asset battle-char-pack extracted/PROT/1204_other5.BIN --atlas 0 --out-tim vahn_atlas.tim

Animation

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)

FunctionRole
FUN_80020224FUN_8001F05C case 2 → FUN_80026B4CSingle 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_8001EBECPer-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.BIN
asset character-pack extracted/PROT/0874_befect_data.BIN --slot 0 --equip 1 --out vahn_equipped.tmd

See also