Player battle files (data\battle\PLAYER1..4) Confirmed
Equip a new sword and, in the next battle, your character is holding it. These four files - one each for Vahn, Noa, Gala and Terra - carry everything a character needs in battle: mesh pieces keyed by item id, textures, palette and the battle animations. At battle load the game picks the piece matching each equipped item and splices the winners into one model.
At a glance
- Where
- PROT extraction entries 0863..0866 (Vahn / Noa / Gala / Terra); in-RAM TOC index
char_id + 0x360. Theedstati3filename labels on 0863/0864 are the CDNAME +2 label shift. - Size
- 338 / 303 / 222 / 47 sectors. Header 16 bytes, descriptor entries 12 bytes, slot region from file offset
0x8000. - Magic
- None - detected by shape (header words ordered
clut_a < clut_b < budget, descriptor chain contiguous). - Parser
legaia_asset::battle_data_pack(walker),::battle_char_palette(palette chain),::battle_char_assembly(splice, poses, uploads),::equip_transplant(editing).- Confidence
- Confirmed - loader disassembly plus byte-exact captures of the assembled mesh, VRAM bands and palette.
- Used by
- Character meshes, characters page, patcher equipment editor, battle scene.
Not to be confused with the monster archive (entry 0867), the TIM-pack, the DATA_FIELD stream or the field-pack - all nearby, all different containers.
File layout
record[0], descriptor table) is read in one go; slots are then streamed by seeking from one selected slot to the next.| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | desc_off | Descriptor-table offset. Also reads as a type-0 streaming chunk header, so stream walkers skip the head cleanly. |
+0x04 | 4 | clut_a_off | Image block A offset inside the decoded record[0]. |
+0x08 | 4 | clut_b_off | Image block B offset, same space. |
+0x0C | 4 | budget | Decoded size of record[0] (the LZS output budget). |
+0x10 | … | record[0] | LZS stream: action-offset table, action entries, art bank, two image blocks. |
+desc_off | 12 × n | descriptor table | [id, offset, size] entries, all-zero terminator, zero-padded to 0x8000. |
+0x8000 | to end | slot region | Per slot [u32 dec_size][LZS], sector-aligned, tiling the file exactly. |
0x8000 is a loader constant (the fixed 16-sector prologue read), not a header field. Measured header values:
| File | desc_off | clut_a | clut_b | budget | entries | footprint |
|---|---|---|---|---|---|---|
| 0863 Vahn | 0x55F4 | 0x5E00 | 0x7E04 | 0x9E48 | 54 | 0xA9000 |
| 0864 Noa | 0x75C4 | 0x76A8 | 0x970C | 0xB750 | 50 | 0x97800 |
| 0865 Gala | 0x6C68 | 0x7464 | 0x9488 | 0xB4AC | 43 | 0x6F000 |
| 0866 Terra | 0x6CAC | 0x83E0 | 0xA5C4 | 0xC7A8 | 5 | 0x17800 |
Descriptor table: one section per equipment slot
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | id | Item id (item table space). 0 ends a section and names its default slot. |
+0x04 | 4 | offset | Slot offset from 0x8000. |
+0x08 | 4 | size | Slot allocation, sector-aligned. offset[i+1] = offset[i] + size[i] always. |
Entries run in sections of descending ids, each closed by an id = 0 default. The five sections are the character's five equipment slots, in the order of the equipped-item bytes of the character record (+0x196..+0x19A). Terra's file has only the five defaults.
| Section | Vahn | Noa | Note |
|---|---|---|---|
| 0 | body | body | Armour re-sculpts the torso object. |
| 1 | head | head | Face rows are stamped by the facial animator. |
| 2 | weapon | Ra-Seru | Order differs per character; both fill the equipment table's Weapon slot. |
| 3 | Ra-Seru | weapon | A Ra-Seru is the character's weapon arm. |
| 4 | feet | feet | Carries two swing records instead of one. |
A section is bone geometry, not an attached object
A slot carries whole skeleton bone objects; selecting it replaces the bare object with a re-authored one that includes the gear. Vahn's right hand is 40 vertices bare, 71 with the Survival Knife, 109 with the Astral Sword; Hunter Clothes takes his torso from 79 vertices to 64. A full endgame set re-authors ten of his fifteen bones.
The item is still separable - by palette, not geometry. A weapon draws from its own CLUT column and no primitive across all 81 held-item records mixes columns; the flesh half always reaches the bone origin, the item never does. This is what powers the characters page's equipment diff and the item-alone .glb export.
Cut classes, item-alone policy and grip repair
| class | records | shape |
|---|---|---|
own-object | 6 | Already its own 0xFE object - retail shipped the split. |
separate | 21 | Own connected component, zero shared vertices. Lossless. |
welded | 53 | Palette subset joined at the grip rim; 3-64 shared vertices; exported with an open grip. |
fused | 1 (+51 armour) | No material boundary: the whole section contribution. Body / head / feet always export this way. |
- A positional set-difference does not find added geometry (the armed hand shares as few as one vertex with the bare hand). The diff highlight uses a radius envelope around the bare object (
battle_char_assembly::equip_diff). - Item-alone export (
equip_isolate, rules incrates/asset/data/equip-isolation.toml): colour diff against the bare counterpart for head / held items, identity test for body / feet, plus per-record overrides. - Grip repair (
equip_repair) lofts a tube between the two rims of a straight shaft on one object; nine of the 81 held items take a bridge, all Vahn's. - Most
0xFF(tag200+) surplus objects are byte-copies of their attach bone, drawn only through the variant track. Sixteen are not (all of Noa's section-2 records, four of her section-3, three of Gala's section-3);AssembledCharacter::duplicate_objectstests by byte comparison.
Inside a slot
Each slot is [u32 dec_size][LZS stream]; the decoder stops on the output count, so give it a generous input slice. Decoded, every slot is a 32-byte header, a loader frame wrapping a Legaia TMD, and a texture pool.
frame_off = 0x14 + 4 × attach_obj_count in every retail slot.| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | frame_off | Self-relative offset of the loader frame. |
+0x04 | 4 | swing_rec_a | Self-relative offset of the section's swing action record (sections 2..4; 0 in 0/1). |
+0x08 | 4 | swing_rec_b | Second swing record, consumed only for section 4. |
+0x0C | 4 | tmd_body_end | Where the TMD ends and the texture pool begins; the decode-buffer advance to the next section. |
+0x10 | 2 | attach_obj_count | Signed; 0 / 1 / 2 observed. |
+0x12 | 2 | upload_flag | Non-zero = the pool is uploaded to VRAM at battle init; zero = pool bytes are dead. |
+0x14 | 4 × n | attach_obj_off[] | Attach-object records; each record's +0x07 byte is its attach key. |
+frame_off | … | loader frame | u8 attach_count, u8 bone_ids[], u32 data_size at +0x08, TMD at +0x0C. |
+tmd_body_end | … | texture pool | One headerless upload block - not a PSX TIM. |
The assembler reads one bone-id byte per object while obj_index < attach_count, then tags surplus objects 0xFF (per-clip swap variant) or 0xFE (extra animated part). Assembled objects are also effect-read: the Spirit charge samples authored vertex indices of specific objects, so a rebuild must keep a real vertex pool in every slot even when it draws nothing.
History: earlier readings of the slot header
+0x04 / +0x08 were first read as nested-section end offsets and +0x10 as half of a flag word; the byte-match corpus read the loader frame's attach bytes as texture-format tags. The assembler's disassembly supersedes all of these - see settled threads.
Where the textures land in VRAM
The assembler runs once per present party member, with p the member's 0-based ordinal (not the character id). It uploads up to seven blocks: the two record[0] image blocks at fixed rects, plus one block per flagged section at the rect from a static SCUS table. Pixels go to (x0 + 0x200 + p×0x80, y0 + 0x100), the CLUT to row 0x1E1 + p with the STP bit set on every non-zero colour. The seven rects tile the member's 128×256 band exactly - texpages 0x18 + 2p / 0x19 + 2p, the pages the mesh relocation retargets.
| Block | x0 | y0 | w | h |
|---|---|---|---|---|
| record[0] A | 0x20 | 0x80 | 0x20 | 0x80 |
| record[0] B | 0x60 | 0x00 | 0x20 | 0x80 |
| section 0 body | 0x00 | 0x80 | 0x20 | 0x80 |
| section 1 head | 0x00 | 0x00 | 0x40 | 0x80 |
| section 2 | 0x40 | 0x00 | 0x20 | 0x80 |
| section 3 | 0x40 | 0x80 | 0x20 | 0x80 |
| section 4 feet | 0x60 | 0x80 | 0x20 | 0x80 |
Port: battle_char_assembly::{SECTION_TEXTURE_RECTS, RECORD0_TEXTURE_RECTS, character_texture_uploads}; validated against captured battle VRAM at 99.7-100 % per member (the residual is the facial animator's current frame).
Battle animations (record[0])
The player files are also the party's in-battle pose source. record[0] opens with a u32 action-offset table (12 disc slots) of action entries in the same family as the monster archive's: the first byte is the action tag, the rate byte sits at +0x78, and the packed [u8 parts][u8 frames][9-byte TRS] keyframe stream at +0xAC (monsters keep theirs at +0x8C). Frame 0 of the idle is the combat-stance rest pose the assembled mesh is socketed into.
| Slot | Action | Source |
|---|---|---|
0 | idle (frame 0 = rest pose) | disc |
1 | walk | disc |
2..5 | hit reactions | disc |
7 / 8 / 9 | ready / recover / defeat | disc |
0x0B | block | disc |
0xC..0xF | four direction-command swings | spliced from equipment sections 2 / 3 / 4 at load |
0x10 / 0x11 | dynamic art clips | materialised per staged art from the art bank + readef.DAT |
- Persistence. Only the region below
clut_a_offsurvives the battle: the five sections decode over the rest of the same buffer. An edit that adds per-frame data must grow that region. - Swing records are per-equipment animations: each selected section's record at slot-header
+0x04(and+0x08for section 4) is installed at action slot0xC + section − 2. Its first byte is the per-strike power run head, not a runtime slot (art-data). - Art bank (
record[0] +0x58):[u32 count]then0xD0-stride records - a0x24-byte matcher head (command combo, stream index, inline art name) followed by a standard action entry. Keyframes live outside the file, in the"ME"archives ofreadef.DAT; win poses (0x11..=0x18) decode from the base archive, everything else from main. - Facial tracks (entry
+0x8Ceyes,+0x98mouth) are stamped over the head section's texture rows per frame; during the victory quote the mouth comes from a static SCUS table instead. - Equipment-variant track (entry
+0xA4): two[start, end]frame windows that swap a0xFFvariant onto its attach-bone channel - a mesh swap, not a stamp. Live windows exist only in Noa's file.
Two container encodings, one pose format
Inline in record[0] a stream is raw: exactly 2 + parts × frames × 9 bytes, no size field, no codec. An art clip's stream is a "ME" archive body whose size word carries a bit-15 compression flag (set on every retail player art), delta-coded per 12-bit channel. Both decode to the same bytes. A raw inline stream can be rewritten in place if parts and frames hold; a "ME" body must be re-encoded and its length depends on the content. The action table in RAM (0x801C9360 + slot×4) holds rebased copies of all of the above.
Attach-object records match their +0x07 key against action-entry +0x77 bytes (then art-bank +0x9B) and are linked into the entry's +0x04/+0x08 pair by a word-granular copy whose extent two nested signed counts define.
Editing: carrying a weapon into another character's file
A file only holds records for the items its character can equip, so a weapon handed to another character through the owner mask alone lands on that character's bare-hand default. The patcher's --equip-owner builds the missing record (legaia_asset::equip_transplant):
- Cut the weapon's primitives out of the donor record (item-alone cut; CLUT columns remapped onto the target section).
- Merge them channel by channel into the target's weapon-section default - the held sections are the same three arm bones on every skeleton, only numbered differently - and re-seat each channel through a rigid transform calibrated from the weapons both files carry (
equip_hand_frame, per weapon class). - Carry the donor's texture tile as the pool under the weapon's palettes; sections 2 and 3 share a texpage and differ only by tile row.
- Keep the target default's header, loader frame, attach list and swing records; set the swing record's
+0x74to the donor weapon's cost.
Not unlocked this way: Ra-Seru forms, body, head and footwear - those records are a whole re-sculpted limb on the donor's skeleton (above), and Terra's file has no equipment sections.
Parking the records in DMY.DAT
The loader never bounds a file by its TOC span - after the prologue read it streams slots by seeking a 32-bit gap forward from the previous slot. A rebuilt file that outgrows the free sectors between entries 863..865 therefore keeps its first 0x8000 bytes in place and puts its whole slot region, in chain order, into DMY.DAT, the unused developer-fixture file at the end of the disc, with every table offset displaced accordingly. The image keeps its size, so a PPF carries it. Two rules follow: slots must stay in chain order (the gap is unsigned), and old record sectors are simply left unread.
Sizes and the annex allocator
Vahn's Astral Sword record for Noa comes out at 7 sectors. Retail files tile their footprints exactly; the optimal LZS parse frees 4 / 2 / 5 sectors in Vahn's / Noa's / Gala's files and none in Terra's, and the patcher moves the boundaries between entries 863..865 before falling back to the annex. legaia_asset::player_file_annex recognises an annexed table (chain starts at a non-zero sector-aligned offset) and materialises the retail-shaped file for every parser; DiscPatcher::{annex_player_file, read_player_file, patch_player_file} are the disc side, with a bump allocator whose LGAX marker lives in DMY.DAT's last sector. Verified in PCSX-Redux from a pre-battle save on the patched disc.
Not the monster archive
The monster stat archive (extraction 0867, legaia_asset::monster_archive) shares the [dec_size][LZS] → mesh + texture pool shape but nothing else:
| Player files | Monster archive | |
|---|---|---|
| Slots | Variable size via the descriptor table | Fixed 0x14000 stride keyed by monster id |
| Decoded head | 32-byte slot header + loader frame | Monster stat record (name, HP, action-offset array) |
| Keyframe stream | entry +0xAC | entry +0x8C |
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Streaming loader | FUN_80052770 | Dev-path open via TOC index char_id + 0x360; 16-sector prologue; per-section id match against record +0x196..+0x19A; gap-and-seek slot streaming (the loader never bounds by TOC span). | ghidra/scripts/funcs/80052770.txt |
| Assembler | FUN_80052FA0 | Slot-header field roles, swing-record splice into slots 0xC..0xF, upload_flag gate, rect table at 0x800775B8, +0x58 / +0x5C rebase. | 80052fa0.txt |
| Loader-frame reader | FUN_800536BC | Bone-id bytes per object, then 0xFF / 0xFE surplus tags. | - |
| Texture upload | FUN_80053B9C | Two LoadImages per block: CLUT at row 0x1E1 + p with STP, pixels at the banded rect. | - |
| Keyframe splice | FUN_800557B8 | Raw inline stream size (parts×frames×9 + 5) >> 2 words. | - |
| Art clip commit | FUN_8004AD80 | Slots 0x10 / 0x11 install record +0x24; reads the +0x58 art bank. | - |
| ME decoder | FUN_8002A9CC | Delta-coded 12-bit channels behind the bit-15 flag. | - |
| Facial animator | FUN_8004C7B4 | Eyes / mouth tracks at entry +0x8C / +0x98; victory mouth table at 0x80077E80. | - |
| Variant swap | FUN_8004CCD4 | +0xA4 windows swap model pointers on the render node's channel table. | - |
| Archive staging | FUN_801DABA4, FUN_801E295C | Main archive per turn; base archive (slot 3×char+2) at battle end. | - |
| Live captures | - | Assembled mesh vertex pools byte-match the selected sections; VRAM bands match the rect table; mednafen-state clut-trace byte-matches decoded pools to captured VRAM. | - |
Open: the record[0] +0x5C word
Rebased at load together with the +0x58 art-bank pointer, but with no reader in SCUS_942.54 or any extracted overlay image (11 overlay images remain dump-only). Its target is clut_a_off − 4, zero on disc. The earlier "points at the ME archive" hypothesis is refuted by the disc: no "ME" archive exists in any player file.
History: the "16 MB container at PROT 0865" reading
That figure was extraction 0865's window under the superseded entry-size expression: it spanned Gala's file, Terra's, and most of the monster archive. The same over-read produced "Vahn = PROT 0861" and the battle_data / monster archive conflation. Each file's slot region tiles its own footprint exactly, which fixes the true size. Catalogued in do-not-re-walk.
Not here: the NPC palettes at row 479
The town NPC CLUTs byte-match no player-file slot. They are plain TIMs in each scene's own bundle, uploaded at battle init - see npc-palette.
CLI
# Inspect one player file's slot table.
asset battle-data-pack extracted/PROT/0865_battle_data.BIN
# Dump every decoded slot to a directory.
asset battle-data-pack extracted/PROT/0865_battle_data.BIN --out /tmp/0865_records
# Bulk-scan a directory of PROT entries for this shape.
asset battle-data-pack-scan extracted/PROT --cdname extracted/CDNAME.TXT
Full reference, including the texture-block catalog and replacement commands: docs/formats/battle-data-pack.md.