MDT - the MOVE buffer (animation clip bank) Confirmed
When a scene tells an actor to "do move 12" - a villager's wave, a door swinging, a scripted
stumble - the frames come from the scene's move table: the buffer the engine keeps in RAM at the
MOVE base, one record per move id. Despite the name, that buffer is the scene's
ANM clip bank: byte-equality against the live bank in a field save state shows the
type-0x05 payload is an ANM container, and its records are per-bone keyframes, not
move-VM bytecode. Every scene ships its own bank inside its asset
bundle; there is no single global one. And a naming trap for anyone browsing an extracted disc: the files
literally named move_program_no.BIN are not move tables at all (below).
At a glance
- In the game
- Scripted actor motion in the field - the script VM's
0x22 EXEC_MOVEbinds one of these clips to an actor - Magic / marker
- None - an ANM container:
u32 count, thencountbyte offsets, then the records (each header carriesmarker_1 = 0x080C) - Lives in
- Each scene's
scene_asset_tableentry (the second PROT entry of the scene's CDNAME block), descriptor 4, type byte0x05; loaded to theMOVEbase_DAT_8007B888on area transition - Retail reader
FUN_800204F8, the ANM clip driver (binding and playhead advance); the entries are decoded byFUN_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
- Script asks for a moveThe field VM's
0x22 EXEC_MOVEstores amove_idin the actor's+0x5Cand calls the clip driver (script VM). - Pick a bufferActor flag
0x01000000set → alternate base_DAT_8007B75C; elseMOVEfor ids< 0x400,MOVE2for>= 0x400. - Offset tableThe driver reads word
move_id & 0x3FFof the container head: word 0 is the record count, worddisoffsets[d-1]. - Record headerClamp the playhead, pick the frame divisor, find the per-frame data (layout).
- 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:
move_id of the head is readu32 × countEach 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.
framesclamps 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.
descriptor[4] type 0x05 = Move7 × 8Examples verified by mednafen save-state diff against _DAT_8007B888:
| Scene block | Slot-1 PROT entry | Move size | Notes |
|---|---|---|---|
dolk (60) | 0061_dolk.BIN | 0xE370 (58224) | Loaded as MOVE at 0x800E412C (Drake Castle save) |
suimon (77) | 0078_suimon.BIN | 0x09A0 (2464) | Loaded at 0x801355D0 (Suimon-block saves) |
map01 (85) | 0086_map01.BIN | 0x7E30 (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
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Clip driver | FUN_800204F8 | Offset-table indexing, record header fields, playhead clamp / advance, the MOVE / MOVE2 / alternate-base routing | funcs/800204f8.txt |
| Entry decoder | FUN_8001BE80 | The per-frame data is ANM 8-byte (bone, frame) entries, decoded to a rigid transform per object - not move-VM bytecode | funcs/8001be80.txt |
| Clip-bank byte match | field 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 table | mednafen automation |
| MOVE2 base | _DAT_8007B840 | Zero across every observed save state - the alternate table is rare or unused | save-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.