Player-character mesh pack Confirmed
Where the playable characters' 3D models live - and the answer is different for the two places you see them. The field form (walking around towns) is straightforward: five Legaia TMDs - Vahn, Noa, Gala, plus two smaller auxiliary actors - stored whole in PROT entry 0874 (befect_data; PROT.DAT is the disc's single big archive of 1233 numbered entries) and kept resident at DAT_8007C018[0..=4] every field scene. The battle form is the twist: it exists on disc only as parts.
At battle setup the engine assembles each character's high-detail mesh from per-equipment-slot pieces chosen by what that character has equipped - so the exact model you fight with may exist nowhere on the disc as a single file (see battle form). 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 (LZS being the game's own dictionary compression). 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 textures live in PROT 0874 section 2 - the third LZS descriptor of the same player.lzs container (parser legaia_asset::field_char_textures): an 8-entry pack of standard PSX TIMs uploaded via FUN_800198e0. Entries 1/2/3 tile horizontally to fill the 4bpp texpage (832, 256) every field-form character prim samples, with per-character CLUTs on row 478 (Vahn cols 0..63 / Noa 64..127 / Gala 128..191) - uploaded as flat horizontal strips, not the declared CLUT rects, which is why one character spans several CBA columns of a single row. They are not in extraction PROT 0876 (a VAB + empty TIM_LIST + SEQ trailer) - that entry was only searched because its filename label says player_data; the retail player_data define names extraction 0874 itself under the -2 numbering correction. Byte-exact against a live field VRAM dump; kept resident across transitions by FIELD_SHARED_BLOCKS.
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
This is the part that defeats a naive “extract the model” approach. 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].
Assembly 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) - the equipped weapon's visual mesh is one of those spliced extras.
Battle pose source + engine port
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.
Install sites + party gating
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. 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 catalogued full-party battle save states (game_mode 0x15, party_count=3, DAT_8007C018[0..2] = 0x80165E38 / 0x8017A908 / 0x8018D550 - all three battle-form meshes).
Superseded readings
- "Battle reuses the field pack (PROT 0874 §0)" - superseded; the field pack is field-form only.
- "Battle renders PROT 1204 directly (12/17 partial match)" - the 12 matching objects were the default sections byte-shared between 1204 and the player files; the five equipped-variant objects appear nowhere in 1204.
- The party-mesh install was long mis-assumed to live in an overlay because a static
0x8007C018cross-reference finds no writer - the installers are indirectly-dispatched SCUS battle state-handlers. FUN_8001EBECis a pose toggle that never changes the object count (not an equipment mesh swap-in).
The pack is two entries. PROT 1204 is a flat streaming-format container (no LZS wrapper) with five chunks of asset type 0x09 (TMD2) plus a terminator, and ends there - 0x25800, its own 75 sectors exactly. PROT 1205 is the sibling container holding the eight character atlases as type-0x00 (TIM) chunks at 0x8224 stride:
| Region | Entry | Offset | Type | Size | Role |
|---|---|---|---|---|---|
| chunk 0 | 1204 | 0x000004 | TMD2 | 33 516 | Vahn battle (nobj=15) |
| chunk 1 | 1204 | 0x0082F4 | TMD2 | 33 636 | Noa battle (nobj=16) |
| chunk 2 | 1204 | 0x01065C | TMD2 | 24 780 | Gala battle (nobj=15) |
| chunk 3 | 1204 | 0x01672C | TMD2 | 27 036 | Extra fighter (nobj=20) |
| chunk 4 | 1204 | 0x01D0CC | TMD2 | 33 340 | Extra fighter (nobj=15) |
| atlas 0 | 1205 | 0x000004 | TIM | 33 312 | 256×256 4bpp + 256×1 CLUT @ (0, 490) |
| atlas 1 | 1205 | 0x008228 | TIM | 33 312 | CLUT @ (0, 491) |
| atlas 2 | 1205 | 0x01044C | TIM | 33 312 | CLUT @ (0, 492) |
| atlas 3 | 1205 | 0x018670 | TIM | 33 312 | CLUT @ (0, 493) |
| atlas 4 | 1205 | 0x020894 | TIM | 33 312 | CLUT @ (0, 494) |
| atlas 5 | 1205 | 0x028AB8 | TIM | 33 312 | CLUT @ (0, 495) |
| atlas 6 | 1205 | 0x030CDC | TIM | 33 312 | CLUT @ (0, 497) |
| atlas 7 | 1205 | 0x038F00 | TIM | 33 312 | CLUT @ (0, 496) |
Correction: eight atlases, and none truncated. This table used to place seven atlases inside PROT 1204 at 0x25804 + k*0x8224, the last "truncated, last in pack", and called row 496 skipped. All of that was the pre-correction PROT entry size: 1204's declared span was 184 sectors instead of 75, so it ran 0x36800 into 1205 - 0x25804 is 1205's offset 0x4, the window stopped between atlas 6 and atlas 7, and the "~23 332 truncated" size was the window's end rather than the TIM's. 1205's own 131 sectors hold eight whole TIMs (4 + 8×0x8224 = 0x41124 of 0x41800). The live capture already agreed: a played battle uploads party CLUTs at rows 490..497 including 496.
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 --atlas-entry extracted/PROT/1205_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-entry extracted/PROT/1205_other5.BIN --atlas 0 --out-tim vahn_atlas.timAnimation
The party's field clips are in PROT 0874: section 1 is the party locomotion ANM bundle (walk / run / idle; three 7-record character banks + savepoint + aux - see ANM § Disc sources). Bank slot 1 is the standing idle, and frame 0 of that clip is the field rest-pose assembly transform: the field TMDs' vertices are object-local (each group models one body part about its own joint origin), so a drawn character poses each group with the clip's flat R·v + T, bone i driving group i one-to-one (FUN_80024D78 fills the actor object table straight off the pool TMD; FUN_8001B964 requires the counts to match - the 10-bone clips animate exactly the 12 disc groups minus the 2 equipment templates).
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