Where it lives

Each monster's decoded archive block is [stat record + +0x4C action-offset array][name][TMD mesh @ +0x04][per-action entries][texture pool @ +0x08]. The magic_count (+0x4A) action entries the +0x4C u32 array points at are not just “spells” — each is an action descriptor whose head holds the action id (+0x00), SP cost (+0x74), and a sub-id (+0x77), and whose +0x8c field begins a packed transform-keyframe stream. (The runtime keyframe pointers at entry +0x04/+0x08 and the self-pointer at +0x88 are zero on disc; the loader reconstructs them, with +0x88 pointing at the +0x8c stream.)

Action index 0 (id 0x00) is the neutral idle animation the engine loops when the monster isn't acting; index 1 (id 0x01) is the basic attack, and the rest correspond to the monster's spell / special actions.

Texture pool (record +0x08)

The same archive block carries the model's colours in a texture pool the battle loader decodes from FUN_80055468: a 0x1E0-byte region of fifteen 16-colour palettes followed by a 4bpp index page (always 256 rows tall, 128 or 256 texels wide). Each TMD primitive selects its palette from the low six bits of its CBA (cba & 0x3F) and samples the page at its UVs — exactly the index-then-palette lookup the PSX GPU performs from VRAM (see the TIM 4bpp / CLUT model and the renderer). 186 of the 194 archive slots carry a clean TMD + texture pool; the rest are filler ids.

Packed stream (entry +0x8c)

u8  part_count    // animated objects per frame == TMD object count
u8  frame_count
frames[frame_count]:
  parts[part_count]:
    u8 b[9]       // six packed 12-bit fields (see below)

Each part record is 9 bytes encoding six 12-bit fields. Low bytes sit at [0,1,3,4,6,7]; the high nibbles are packed into [2,5,8]:

v0 = b0 | (b2 & 0x0f) << 8     tx  (translation X)
v1 = b1 | (b2 & 0xf0) << 4     ty
v2 = b3 | (b5 & 0x0f) << 8     tz
v3 = b4 | (b5 & 0xf0) << 4     rx  (rotation X)
v4 = b6 | (b8 & 0x0f) << 8     ry
v5 = b7 | (b8 & 0xf0) << 4     rz
  • tx, ty, tz are sign-extended 12-bit (-2048..2047) translation in TMD model units.
  • rx, ry, rz are unsigned 12-bit Euler angles (0..4095, where 4096 = a full turn); values near 4095 are small negative rotations.

One part maps to one TMD object (a rigid body part). Across the retail roster the part count equals the TMD object count for >98% of actions (one model carries an extra non-animated object).

Playback

The renderer (FUN_80048a08) keeps a 12.4 fixed-point phase in the per-actor draw struct (+0x68): integer frame index = phase >> 4, sub-frame fraction = phase & 0xf. The decoder (FUN_8004998c) interpolates between frame i and i+1: linear for translation, shortest-path angle-wrap for rotation. The result is written to a pose buffer (6 shorts per object) and applied per object via the GTE in the draw loop, then FUN_800495c8 / FUN_8005b038 blend it onto the object vertices. The enemy-table viewer mirrors this: it plays each action and poses every part relative to its rest frame, so frame 0 is exactly the static model.

Export (glTF)

legaia_asset::monster_gltf::export_glb packs a monster's mesh, its baked texture, and every action animation into one binary glTF (.glb) — the universal interchange format. The rigid-per-object model maps straight onto glTF node animation: each TMD object becomes a node, the keyframe stream's translation + Euler rotation drive that node's translation / rotation channels (the Rz·Ry·Rx order recomposed as a quaternion), and a root node rotates the rig 180° about X to convert PSX +Y-down space to glTF's +Y-up. The per-prim CLUTs (cba & 0x3F) that a single material can't index are baked into a vertical palette atlas, with each vertex's V remapped into its band.

asset monster-archive … --id N --glb enemy.glb

The enemy table exposes the same export as a per-enemy download button.

Provenance (Ghidra trace)
  • FUN_8004998c — packed-stream decoder + frame interpolation.
  • FUN_80048a08 — per-actor battle draw; reads the phase, drives the decoder, applies the pose per object.
  • FUN_800495c8 / FUN_8005b038 — GTE vertex blend of the decoded pose.
  • FUN_80054cb0 — monster init; copies the action/effect pointer (record +0x04) into actor +0x230.

See also