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), AGL (action) 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 walk/approach cycle, and the rest correspond to the monster's spell / special actions. The entry's first byte is a semantic action tag: 2/3 light hit reactions, 4 knockdown, 5 get-up, 0x0B block (the battle loaders cache a tag → entry-index map at actor +0x1EF..+0x1F3 the damage path stages reactions from), and 0x20/0x21/0x22 the attack pre-approach / close-in / victory family the action SM resolves by first-byte search.

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).

The transform is absolute model-space, not a delta from a rest pose: each TMD object is modelled at its own local origin, and the per-part [tx, ty, tz] places that object at its socket while [rx, ry, rz] orients it about its local origin (world = Rz·Ry·Rx · v_local + t). Frame 0 is therefore the assembled rest pose - and a raw monster TMD rendered without its animation looks like a collapsed heap of parts piled at the origin; assembling frame 0 spreads it into its full silhouette.

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 per-frame cursor advance lives in the anim-node tick FUN_80047430: phase += (frame_dt * actor[+0x21D] * record[+0x78]) >> 1, where +0x21D is the actor's speed scale (normally 4) and the entry's +0x78 byte is its playback rate (1 or 2 across the retail corpus). The enemy-table viewer mirrors this: it plays each action applying each part's keyframe transform as the absolute model-space pose, so frame 0 is exactly the assembled rest pose.

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