At a glance

In the game
Scripted actor motion in the field - the script VM's 0x22 EXEC_MOVE binds one of these clips to an actor
Magic / marker
None - an ANM container: u32 count, then count byte offsets, then the records (each header carries marker_1 = 0x080C)
Lives in
Each scene's scene_asset_table entry (the second PROT entry of the scene's CDNAME block), descriptor 4, type byte 0x05; loaded to the MOVE base _DAT_8007B888 on area transition
Retail reader
FUN_800204F8, the ANM clip driver (binding and playhead advance); the entries are decoded by FUN_8001BE80
Parser
crates/mdt (MoveBuffer), legaia_scene_formats::scene_asset_table::move_descriptor, engine-scene::scene_bundle::extract_move_payload
Confidence
Confirmed - consumer disassembly for the layout; per-scene source verified by mednafen save-state diff against _DAT_8007B888

How a move is found

  1. Script asks for a moveThe field VM's 0x22 EXEC_MOVE stores a move_id in the actor's +0x5C and calls the clip driver (script VM).
  2. Pick a bufferActor flag 0x01000000 set → alternate base _DAT_8007B75C; else MOVE for ids < 0x400, MOVE2 for >= 0x400.
  3. Offset tableThe driver reads word move_id & 0x3FF of the container head: word 0 is the record count, word d is offsets[d-1].
  4. Record headerClamp the playhead, pick the frame divisor, find the per-frame data (layout).
  5. Pose the frameThe per-frame data is one 8-byte (bone, frame) entry per bone, decoded to a rigid transform per TMD object (ANM).

Layout the consumer reads

The driver masks the move id to 10 bits, so a table is addressable up to 1024 words; a real bank is an ANM container holding far fewer records (69-72 in the field scenes), with the record bodies packed right after its offset table:

Each record is an ANM clip (the field names this page used before the ANM reading in brackets):

  • Scale bit 0 = "use frame divisor" (and blend adjacent frames); the divisor is consulted only when it is set.
  • frames clamps the playhead at (frames * 16) - 1: the cursor is in 1/16-frame units.

FUN_800204F8 clamps actor[0x68] (the playhead) to [0, frames * 16), advances it by actor[0x6A] (frame delta) optionally scaled by the divisor at record[6], and binds the clip for the renderer to pose. This is ANM clip playback itself, not a sibling format sharing its frame cursor.

On-disc source - per-scene scene_asset_table slot 4

(PROT.DAT is the disc's single big archive; CDNAME is the name map that groups consecutive entries into per-scene blocks.) The MOVE base is populated per scene during area transitions, not from a boot-time entry. Every scene block's second PROT entry - the one classified scene_asset_table - carries an Asset(0x05) = Move descriptor whose payload is that scene's runtime table.

Examples verified by mednafen save-state diff against _DAT_8007B888:

Scene blockSlot-1 PROT entryMove sizeNotes
dolk (60)0061_dolk.BIN0xE370 (58224)Loaded as MOVE at 0x800E412C (Drake Castle save)
suimon (77)0078_suimon.BIN0x09A0 (2464)Loaded at 0x801355D0 (Suimon-block saves)
map01 (85)0086_map01.BIN0x7E30 (32304)Loaded at 0x8011A624 (every map01-resident save, incl. menu and battle states layered on it)

The descriptor's data_offset is measured from the start of the bundle entry, and the payload lies inside that entry's own sectors (an entry's size is the sector gap to the next TOC entry). In the port, engine-scene::scene_bundle::extract_move_payload slices and decodes it, and scene_asset_table::move_descriptor is the typed slot accessor:

let s = legaia_scene_formats::scene_asset_table::detect(&prot_bytes)?;
let move_descriptor = s.move_descriptor()?; // type_byte = 0x05

The MOVE2 base (_DAT_8007B840) is zero across every observed save state, and no Move2 payload has been observed in a retail scene bundle - the alternate table is rare or unused. (The scene_scripted_asset_table that once looked like its carrier was an entry-size over-read; see PROT.DAT.)

The move_program_no name

The extraction files named 0972 / 0973 move_program_no.BIN are not move tables. Under the +2 CDNAME numbering shift they sit in the other_game block: 0972 is the fishing minigame overlay (dev other1) and 0973 the 1-sector OTHER2 dev module. The move_program_no define actually covers extraction 0970..0971 - a \DATA\MOV*.STR FMV program / path table (the block names MOVie program numbers; see the STR FMV table), not Tactical-Arts moves. mdt classify correctly reports that neither file matches the runtime layout above.

Caveat: MoveBuffer::parse over-reads the offset table

Real per-scene buffers have offset tables far shorter than the 1024-word mask and pack record data densely right after the real table end, so MoveBuffer::parse keeps reading record bytes as offsets. Most of those point past the buffer and are counted as bogus_offsets, which makes the strict fitness() score (used - 2*bogus) strongly negative for valid retail data. Use MoveBuffer::looks_like_move_buffer() instead - it requires records.len() > 0 && used > bogus, which real per-scene buffers pass while random data fails - and classify()'s OffsetTableLayout verdict routes through it, so the CLI reports the same shape the engine accepts.

How we know

Consumer disassembly and save-state evidence
FunctionAddressWhat it provesDump
Clip driverFUN_800204F8Offset-table indexing, record header fields, playhead clamp / advance, the MOVE / MOVE2 / alternate-base routingfuncs/800204f8.txt
Entry decoderFUN_8001BE80The per-frame data is ANM 8-byte (bone, frame) entries, decoded to a rigid transform per object - not move-VM bytecodefuncs/8001be80.txt
Clip-bank byte matchfield save state (v0_1_pre_battle_tetsu)The live bank at _DAT_8007B888 byte-matches the scene bundle's type-0x05 payload decoded as an ANM container (ANM)scenario manifest
Per-scene source_DAT_8007B888 diffs (dolk / suimon / map01 saves)The MOVE base is refilled per scene from slot-4 of the scene's asset tablemednafen automation
MOVE2 base_DAT_8007B840Zero across every observed save state - the alternate table is rare or unusedsave-state corpus

CLI

mdt classify <PATH>                        # which layout?
mdt records  <PATH> --limit 8              # decode as flat record table
mdt slots    <PATH> --limit 8              # decode as offset-table layout

Source of record: docs/formats/mdt.md.

See also