Slot-4 records (the world map's animation bank) Confirmed
Each of Legaia's three overworld continents (Drake, Sebucus, Karisto) ships as a “kingdom bundle” - a per-continent asset bundle whose numbered slots hold the map's textures, meshes and data. Slot 4 holds none of those: it is the overworld scene's animation bank, the same ordinary container every field scene carries. Each body is one clip, and each 8-byte entry in a clip is the position and rotation of one piece of a moving object in one frame.
At a glance
- Where on disc
- Slot 4 of the kingdom bundles: PROT
0086(Drake,map01),0245(Sebucus,map02),0392(Karisto,map03). Type byte0x05. - Magic
- None. Every sub-body carries the constant marker
0x080Cat+4. - Size
- 15-16 clips per kingdom; 24-32 KB decoded; 8 bytes per (frame, part) entry.
- In RAM
- Loaded verbatim (LZS-decoded, no fixup) at the type-
0x05buffer pointer_DAT_8007B888, so the base differs per kingdom - see RAM layout. - Parser
legaia_scene_formats::world_map_overlay.- Confidence
- Confirmed - container and RAM placement byte-verified, every header and entry field pinned to the instruction that reads it, clip lookup checked against a live
map01state. - Used by
- World map, World-overview viewer
What it is
A kingdom's slot 4 is an animation bank: 15-16 self-contained clips, each an 8-byte header plus a table of 8-byte poses laid out frame by frame. An actor on the overworld picks a clip by number, the engine advances a cursor through it, and for each frame it applies one pose to each piece of the actor's model before drawing that piece. The model itself lives elsewhere - slot 4 carries only the motion.
- Not geometry of any kind. No vertices, no triangles, no coastlines. Slot 4 was read as a vertex pool for a long time; that reading is falsified (see History) and it is why the format looked like it had a missing “command stream” that nobody could find.
- Not the continent itself. The bulk terrain (~4300 textured quads in the top view) comes from the kingdom's slot-1 TMD pack, drawn through the overlay-mode dispatch table - see world map - bulk terrain.
- Not a world-map format. The type byte
0x05is what asset-type labels “MOVE”, and every field scene's type-0x05section has the same shape: the same0x080Cmarker, the same header fields, the same size law. A town's bank is this format with more clips in it. See ANM.
| Bundle | PROT index | CDNAME label | Decoded size | Bodies |
|---|---|---|---|---|
| Drake | 0086 | map01 | 32304 | 15 |
| Sebucus | 0245 | map02 | 26964 | 16 |
| Karisto | 0392 | map03 | 24444 | 16 |
Layout
Three nested levels: an outer pack of clips, each clip a header plus a frame-ordered table of poses, each pose eight bytes.
Outer pack
The offsets are absolute byte offsets into the decoded payload - unlike the slot-1 TMD pack next door, whose offsets count words.
Clip header (8 bytes)
| Field | Meaning |
|---|---|
part_count | Pieces posed per frame. Must equal the model's object count or the whole draw is skipped |
flags | Bit 0 enables blending between consecutive frames; no other bit is ever set |
frame_count | Clip length in frames (the high byte is 0 in every clip on the disc) |
marker | Constant 0x080C - the animation-record marker, never read by the game |
rate | Sub-frame divisor 1 / 2 / 4; only its low byte is read, and only when blending is on (below) |
Clip payload
Entry (frame f, part p) sits at +8 + (f × parts + p) × 8, so a clip is always 8 + parts × frames × 8 + 8 bytes. That arithmetic fits every clip in all three kingdoms exactly, and every trailer is zero.
Each entry is one pose
The pose decoder unpacks three 12-bit signed translation components out of the first five bytes - byte 2 carries the high nibbles of X (low nibble) and Y (high nibble), byte 4's low nibble carries the high nibble of Z - sign-extends each, and pushes the vector through the GTE against the piece's rotation matrix. Bytes 5, 6 and 7 are the three rotation angles, each shifted << 4 into the console's 4096-per-turn angle units.
Translations are small - the whole corpus spans roughly -541 .. 384 units - because they are offsets of one piece within a model, not world positions. When blending is on, the decoder also unpacks the next frame's entry for the same piece and interpolates both translation and rotation with the cursor's sub-frame fraction, wrapping to frame 0 at the end of the clip unless the actor is set to hold its last frame.
rate - the sub-frame divisor
The header's +0x06 field is the clip's sub-frame divisor: the cursor advances by ceil(speed × 2 / rate) when blending is on, and by the actor's plain speed when it is off. That is why the field and the blend flag co-vary - every rate = 4 clip sets the flag. Values 1, 2 and 4 are the only ones on the disc.
Clips are shared the way actors are: the three leading rate = 1 clips are byte-identical in all three kingdoms, a trailing cluster is shared across all three, and other clips are shared between kingdom pairs. Degenerate clips (one part, all-zero entries) are empty placeholders.
The one field nothing reads
Every field of the entry is read every frame except the high nibble of byte 4, which is zero in all 22 228 entries across the three kingdom banks and a town's bank - structural padding, not dropped data.
Per-kingdom clip inventory
Drake = 15 clips, Sebucus = 16, Karisto = 16. Clips 0-2 are the shared rate = 1 templates in every kingdom, and an actor selects a clip by its index plus one.
Drake (map01, PROT 0086)
| Clip | Parts | Frames | Rate | Blend | Entries | Notes |
|---|---|---|---|---|---|---|
| 0 | 10 | 20 | 1 | no | 200 | shared template |
| 1 | 10 | 20 | 1 | no | 200 | shared template |
| 2 | 10 | 30 | 1 | no | 300 | shared template |
| 3 | 2 | 30 | 2 | no | 60 | half the entries are zero |
| 4 | 2 | 20 | 2 | no | 40 | drives a live 2-part actor |
| 5 | 10 | 30 | 2 | no | 300 | 25 unique of 30 frames |
| 6 | 10 | 26 | 2 | no | 260 | |
| 7 | 10 | 30 | 2 | no | 300 | 25 unique of 30 frames |
| 8 | 10 | 3 | 2 | no | 30 | 3 identical frames (placeholder) |
| 9 | 12 | 30 | 2 | no | 360 | shared with Sebucus / Karisto 12 |
| 10 | 12 | 30 | 2 | no | 360 | shared with Sebucus / Karisto 13; drives a live 12-part actor |
| 11 | 12 | 10 | 2 | no | 120 | shared with Sebucus / Karisto 14 |
| 12 | 10 | 120 | 2 | no | 1200 | the longest clip on any map |
| 13 | 14 | 15 | 4 | yes | 210 | drives a live 14-part actor |
| 14 | 2 | 30 | 2 | no | 60 | drives a live 2-part actor |
Sebucus (map02, PROT 0245)
16 clips. Clips 8-11 are the blended rate = 4 group.
Karisto (map03, PROT 0392)
16 clips. Clips 4-9 and 11 are blended at rate = 4; clip 10 is the corpus's one blended rate = 2 clip.
Full dump: asset kingdom-slot <PROT>.BIN --slot 4.
RAM layout
The LZS decoder writes slot 4 straight to its destination - byte-identical to the disc, no fixup - and the destination is the asset dispatcher's type-0x05 buffer pointer _DAT_8007B888, freshly allocated per scene load. That is why the base differs per kingdom, and it is also how a RAM probe should find it: read the pointer rather than hunting for the bytes. Clip 0's entries start 0x40 past the base.
| Kingdom | Bundle | _DAT_8007B888 | End (excl.) | Bytes |
|---|---|---|---|---|
| Drake | map01 / 0086 | 0x8011A624 | 0x80122454 | 32304 |
| Sebucus | map02 / 0245 | 0x80119CE4 | 0x80120638 | 26964 |
| Karisto | map03 / 0392 | 0x80108D84 | 0x8010ED00 | 24444 |
In a live Drake overworld save the pointer reads 0x8011A624 and the 32 304 bytes there match the disc payload exactly - which settles the identity of that address rather than leaving it a coincidence of allocation order.
How it is played
Four plain calls, each reading a field of what it was handed. Only after the last one does the ordinary primitive dispatcher draw the piece.
- Install -
FUN_8001F05Ccase 5The scene loader LZS-decodes slot 4 into a fresh buffer and stores the pointer at_DAT_8007B888. - Select + clock -
FUN_800204F8The actor tick resolves the actor's clip number to a clip and advances its 1/16-frame cursor. - Draw -
FUN_8001B964The animated-actor renderer works out the frame, checks the part count, and walks the model's pieces. - Pose -
FUN_8001BE80Decodes one entry into a GTE translation and rotation; the piece is then drawn by the primitive dispatcher.
The clip lookup is one-based: the actor's clip number indexes the offset table with no +4, so number 1 reads the first offset. An actor with the party flag set draws from the party bank instead, and numbers above 0x3FF from the “MOVE2” bank - neither of which the overworld uses.
- In place. A read-watchpoint tiled across the Drake window catches only reads (363 of 365 captures); nothing copies the bank anywhere.
- Six references, image-wide. The buffer pointer is written twice, read once in the actor tick, and read three times in the Baka Fighter minigame overlay. No world-map overlay touches it, and the primitive dispatcher never receives it.
- A hard structural check. If a clip's part count does not equal the model's object count, the renderer skips the whole draw. On a live Drake overworld save every animated actor satisfies it - 2, 12, 14 and 2 parts against models of exactly those sizes.
What actually draws the pieces
The primitive dispatcher FUN_80043390 is a TMD-style display-list walker: it reads a command word (primitive kind plus count), optionally loads the GTE depth-cue registers, and tail-calls a per-kind handler through a jump table - 0x8007657C in SCUS, or 0x801F8968 while the world-map overlay is paged in. Each handler transforms 3-4 vertices through the GTE and writes one GP0 packet into the ordering table. Its vertex pools are ordinary models from the global TMD table - never slot 4. The per-kind handler table and what each kind draws are on the renderer page.
Handler-bank selection and per-kingdom render load
The dispatcher adds a bank offset to the kind × 4 jump-table index. With no fade argument it uses bank 0; with one it loads the GTE far colour and depth-cue interpolant and switches kinds 12-19 to the depth-cue (fog) handler set. Two tint-word bits raise the bank further. The tests are sequential, so 0x20000000 wins when both are set. The bank is not the blend equation - that is a separate field taken from the tint word's bits 24-25.
fade_flags | cmd_flags bits | Bank | Effect |
|---|---|---|---|
== 0 | ignored | 0 (+0x00) | Kinds 12-19 use the plain handler set |
!= 0 | neither high bit | 1 (+0x50) | Kinds 12-19 swap to the depth-cue handler set |
!= 0 | 0x04000000 | 2 (+0xA0) | As bank 1, but kinds 18 / 19 swap again |
!= 0 | 0x20000000 | 3 (+0xF0) | As bank 2, with its own kind-19 handler; never observed in retail world-map render |
Over 19,935 dispatcher entries on Drake, 77% take bank 0 and 23% bank 1; neither high flag bit is ever set. Unobserved in one capture is not unreachable - banks 2 and 3 are real handlers. Exec-breakpoint totals over one warp transition (1800 vsyncs): Drake 71,331 handler hits, Sebucus 90,096, Karisto 13,593 - a measure of per-kingdom render load, not of slot 4. Kind 18 dominates in every kingdom (about 80% of Sebucus's primitives); kind 13 is heavy on Drake (9,465) and nearly absent on Karisto (49).
A second candidate reader at 0x80059DE4 is the generic VRAM LoadImage DMA (FUN_80059BD4) incidentally touching the window - not a slot-4 consumer.
The dispatcher's callers, and where the animation joins them
The primitive dispatcher takes (display_state, cmd_flags, fade_flags): display_state[0] is the vertex pool, [3] gates colour / light modulation, [4] is the command stream. All of those come from a model in the global TMD table.
- Top-view dispatcher
FUN_801F69D8(world-map overlay, 2572 bytes). Copies a 0x20-byte camera struct into scratchpad, loops over visible tile cells (±10 padding), dereferences each 0x20-byte object record, applies frustum bounds and visibility flags, projects through the GTE, then calls the dispatcher with colour0xD0D0D0(variants for the record's+0x1Eflag and the interactive bit+0x12 & 0x800) and a fog cueclamp((screen_z - 0x5000) >> 3, 0, 0x1000). - Per-actor renderer
FUN_8001ADA4(SCUS, 2456 bytes). Walks a linked list of actors and switches on each actor's render mode. Mode 5 draws the actor's model chain as-is. Mode 1 runs an optional screen-space cull and then callsFUN_8001B964, which re-poses every piece from the clip before drawing it - that is the path slot 4 feeds. A third mode, 11, steps a palette-cycling table that happens to live in the same actor field; it is a different format.
The neighbouring TMD table
The same dispatcher draws field characters and per-tile kingdom objects from a global table of TMD pointers, DAT_8007C018. That is a separate path: slot 4 is walked at its resident base and never installed into the table, even though a field-scene RAM snapshot fills the same address range with that scene's TMD pack.
DAT_8007C018 - installer, consumers, party-mesh source
| Global | Role |
|---|---|
DAT_8007C018 | Array of u32 TMD pointers, stride 4; each target is a Legaia TMD (magic 0x80000002) with +0x08 group count and 0x1C-byte group descriptors from +0x0C |
DAT_8007B774 | Install counter (next free index) |
DAT_8007BB38 | Walk counter (last valid index) - every reader gates on it, so entries past it are never content |
DAT_8007B824 | Per-pack count / persistent-base index |
DAT_8007B6F8 | Kingdom-TMD prefix (5 in every snapshot) |
| Function | Role |
|---|---|
FUN_80026B4C | Installer, called per TMD from the asset dispatcher's TMD-pack case; builds the group-descriptor array at tmd + 0xC and stores the pointer. Ghidra misses the store because an addu sits between the lui+addiu and the sw. |
FUN_80021B04, FUN_80024D78 | SCUS actor allocators - fill actor+0x44 from DAT_8007C018[actor+0x64]; the variant also sets actor+0x10 |= 0x08000000 |
FUN_801D77F4, FUN_801D8280 | Overlay allocator and table walker |
FUN_801F69D8 | Top-view dispatcher - indexes [(object_kind + DAT_8007B6F8)] per tile |
FUN_8001E890, FUN_8001EBEC | Cap the three party TMDs at 10 groups and patch one equipment-conditional group descriptor per member (disc group 10 or 11) |
FUN_801D6704 | Overlay scene loader: installs PROT 0874 section 0 into [0..4] via FUN_80020118, then the scene's own TMD pack via FUN_80020224 - the same walk every field scene runs |
Entries [0..4] are the party character meshes from PROT 0874 section 0 (a 5-TMD pack, LZS at file offset 0x20; slots ship with 12 / 12 / 12 / 3 / 2 groups). A settled dolk field snapshot shows 143 installed entries, [5..142] being that scene's field TMD pack; a mid-warp Sebucus snapshot shows 92 with the install still in flight. Extraction 874 is raw TOC index 0x36C (876), which is what the CDNAME player_data define and the dev-tree path data\field\player.lzs name (numbering space); FUN_8001E890 loads it with li a0,0x36c. Extraction 0876 is a different file (a VAB + TIM list + SEQ stream with no TMDs).
In the port
legaia_scene_formats::world_map_overlay parses a kingdom's slot 4 (parse, the clip header accessors, Slot4Record::transform for the decoded pose). The bank is the same container every scene's type-0x05 section uses, so the generic ANM reader handles it too.
asset kingdom-slot <PROT>.BIN --slot 4asset player-anm <PROT>.BIN --desc-count 7The first dumps each clip's parts / frames / rate / blend flag; the second is the generic animation-bank detector, which reports a kingdom bundle exactly as it reports a town.
The older slot4-png / wireframe projections plot raw 16-bit slices that straddle the entries' nibble boundaries - byte-inspection aids, never geometry.
How we know
Every header and entry field is pinned to the instruction that reads it, the resident bank is byte-identical to the disc payload in all three kingdoms, and the clip lookup matches every animated actor on a live map01 state.
Provenance: routines, sites and captures
| Function / site | Address | What it proves | Evidence |
|---|---|---|---|
| Asset dispatcher case 5 | 0x8001F38C, store at 0x8001F3A8 | Slot 4 lands at _DAT_8007B888, the generic type-0x05 buffer | Disassembly |
Clip selector FUN_800204F8 | bank pick 0x80020534, lookup 0x80020584 | One-based clip lookup out of that buffer; cursor advance and wrap | Disassembly; live map01 state matches all four animated actors |
Animated renderer FUN_8001B964 | frame math 0x8001BAC0, part check 0x8001BAF0, draw call 0x8001BC84 | Frame-major entry indexing; the part-count / model-object-count invariant | Disassembly; live actor list |
Pose decoder FUN_8001BE80 | translation unpack 0x8001BF04, angles 0x8001C0CC | Three packed 12-bit translations plus three byte-scaled angles; the blend path | Disassembly |
| Reference sweep of the buffer pointer | 0x8007B888 | Six references image-wide, none in a render path | find-gp-relative-refs.py over SCUS + every overlay |
| Resident bases | Drake 0x8011A624, Sebucus 0x80119CE4, Karisto 0x80108D84 | Byte-identical to the disc payload for every clip; each equals that kingdom's buffer pointer | diff_slot4_ram_vs_disc.py, locate_slot4_base.py, live state read |
LZS decoder FUN_8001A55C | writes at 0x8001A604 etc. | Sole writer of the window: decoded verbatim, no transcoder | autorun_slot4_loader_hunt.lua |
Primitive dispatcher FUN_80043390 | 0x80043390; SCUS jump table 0x8007657C, overlay table 0x801F8968 | Command-word decode and four-bank handler selection - the renderer the posed pieces go through | Disassembly; captures/slot4_dispatcher/ |
TMD installer FUN_80026B4C | store at 0x80026BA8 | How the global model table is filled | find_addr_materializer_dat_8007c018.py |
| Party-mesh source | PROT 0874 section 0 | The table's first five entries byte-match the disc pack | Drake post-warp RAM dump vs LZS-decoded section |
Full capture transcripts and reproduction steps: docs/formats/world-map-overlay.md.
Readings that turned out wrong
Slot 4 is not geometry: neither a vertex pool waiting on a missing command stream nor a coastline wireframe. Both readings are falsified, along with a handful of smaller ones.
The retired hypotheses and what killed each
- A vertex pool with a missing command stream. The long-running reading: each 8-byte entry a 3D vertex
(x, y, z, attr)loaded straight into the geometry coprocessor, with the triangle topology and object placement in a separate stream nobody had located. The stream was never found because it does not exist - the buffer pointer reaches no renderer, the entry's field boundaries fall on nibbles rather than 16-bit words, and the pair of bytes calledattris the Y and Z rotation. It came from reading a primitive handler that genuinely works that way and assuming slot 4 was its pool. - Coastline wireframe / dev-menu outline. Rendering every body on every projection plane and topology mode produces no map silhouette in any kingdom - unsurprising, since the plotted values are packed transform fields. A
parts × framesheightfield grid fails the same test. - “Slot 4 is transcoded into the working buffer.” The
0x801BA000writes belong to the per-actor procedural stream, and the “window full of models” snapshot was adolkfield scene, not a world map. - “Sebucus shows no render reads.” A wrong-base assumption; against the verified base, 171 of 177 reads land in the window - and their return address sits in the pose decoder's caller, which is what identified the reader.
- “The world-map overlay renderer reads it.” A capture recorded a return address of
0x801F78D4. A MIPS return address is the call site plus eight, so the call is two instructions back - and decoding that word in all 68 images that share the slot-B load address, three hold a call there and the world-map renderer is not one of them. The only one that can be resident during a warp is the summon renderer, calling the per-primitive draw wrapper. So the value resolves, and it still is not evidence of a world-map terrain pass: a read watchpoint fires on an address, and the wrapper is handed whatever pointer the heap gave it. - Table entries past the walk counter are stale pointers, not content.
- PROT 0085 / 0244 / 0391 named the entry an over-read window started in; the bundle is the next entry. See PROT.
Catalogued on do-not-re-walk.
Open questions
- Which actor plays which clip. The numbers come from each scene's script, so naming a clip “the windmill’s spin” needs a script pass, not more format work.
- Which caller, if any, selects primitive-handler banks 2 and 3 - candidates are debug render modes, battle reuse of the dispatcher, or cutscenes.
- Whether the
_DAT_8007B824persistent-slot path is live.FUN_8001F05Ccase 2 withparam_3 == 1is its sole SCUS store andFUN_8001E1B4resets the install cursor from it, but no dumped caller passes1; a write breakpoint on_DAT_8007B824answers it.