What we know

Slot 4 of each world-map (kingdom) bundle decompresses to a fixed-size buffer that the runtime loads verbatim into RAM. Three carriers:

BundlePROT indexCDNAME labelDecoded size
Drake0085map0132304
Sebucus0244map0226964
Karisto0391map0324444

The 7-asset bundle is the standard scene_asset_table shape with type sequence (1, 2, 3, 4, 5, 6, 7). Slot 4's type byte is 0x05 per asset-type — the standard table calls this "MOVE", but the kingdom-bundle consumer interprets the bytes as something else (see "Falsified hypotheses" below).

Container layout (confirmed)

Outer pack

+0x00   u32  count                ; number of sub-bodies
+0x04   u32  byte_offsets[count]  ; absolute byte offset into the
                                  ; decoded payload (NOT word offsets,
                                  ; unlike the slot-1 TMD pack)
+offset bodies[count]             ; contiguous sub-bodies

Sub-body header (8 bytes)

+0x00   u8   count_a              ; records per group
+0x01   u8   flag_a               ; usually 0; 1 for kind=4 bodies
+0x02   u8   count_b              ; number of groups
+0x03   u8   flag_b               ; usually 0
+0x04   u16  marker               ; constant 0x080C across all bodies
+0x06   u16  kind                 ; 1, 2, or 4 (semantic ambiguous)

Body payload

+0x08   record[count_a * count_b] ; each record is 8 bytes
                                  ; ( i16 x, i16 y, i16 z, i16 attr )
+...    trailer (8 bytes)         ; always 8 zero bytes

Total body size is always 8 + count_a * count_b * 8 + 8. The math fits every body in all three kingdoms exactly. The container layout above is fully confirmed; what's not confirmed is how the runtime interprets the 8-byte records.

Per-kingdom body inventory

Drake = 15 bodies; Sebucus = 16; Karisto = 16. The leading three bodies (kind = 1, count_a = 10) are byte-identical templates across all three kingdoms - whatever they encode, the engine ships the same generic shape in every bundle.

Drake (map01, PROT 0085)

Bodycount_acount_bkindflag_arecordsX spanY spanZ span
0102010200166261076738641
1102010200byte-identical template (matches Sebucus / Karisto)
2103010300byte-identical template
32302060pinned plane (degenerate)
42202040pinned plane (degenerate)
510302030025 unique of 30 groups
6102620260
710302030025 unique of 30 groups
810320303 identical groups (filler / padding)
9123020360107252585621248
10123020360130561843231503
11121020120114922764824064
121012020120016118409631473
13141541210654851433664512
142302060

Bodies 9 / 10 / 11 have Y spans comparable to the X/Z scale - 3D mesh extent, not 2D contour data. Body 12 is nearly flat (Y span 4K). Body 13 reaches the full ±32K world bounds on X and Z and clusters in the corners.

Sebucus (map02, PROT 0244)

16 bodies; same leading three templates as Drake. Bodies 8-11 are all kind = 4 with flag_a = 1 (more boundary-style bodies than Drake's single body 13).

Karisto (map03, PROT 0391)

16 bodies; same leading three templates. Bodies 4-9 + 11 are kind = 4 with flag_a = 1. Karisto body 10 is anomalous: kind = 2, flag_a = 1 (the only non-kind=4 body with flag_a = 1 across all three kingdoms).

RAM layout (confirmed)

Slot 4 is loaded verbatim into RAM with zero per-byte diffs vs disc. Drake's payload starts at 0x8011A624 (the outer pack header) and ends at 0x80122454 exclusive — exactly 32304 bytes, matching the disc-decoded length. Body 0's records start at 0x8011A664 (0x40 past the base, after the 4-byte count and 15 × 4-byte offsets). No runtime fixup is applied.

Verified by scripts/pcsx-redux/diff_slot4_ram_vs_disc.py against a PCSX-Redux save state: every byte of all 15 bodies matches the disc-side LZS-decoded payload. The load base was pinned by signature-searching the full 2 MiB main RAM for the 64-byte outer pack header (count = 15 followed by byte_offsets[0..15]) — see scripts/pcsx-redux/autorun_dump_full_ram.lua for the procedure.

Falsified hypotheses

The container is solved. What slot 4 encodes is not. Three interpretations were systematically tested and falsified by visual inspection (PNG renders of every body × every projection plane × every topology mode):

  1. Top-down dev-menu wireframe / continent coastline. The strongest historical claim: that body 12 traces a continent coastline, body 13 traces the world boundary frame, and the remaining bodies are inner contours / decorative outlines visible in the developer top-view. Projecting all 15 bodies onto xz produces no recognizable map silhouette in any kingdom; matching PNG renders against the dev-menu top-view captured from PCSX-Redux save states found no agreement.
  2. count_a × count_b heightfield grid. Drake body 12 records pair up at fixed X-bands and consecutive groups looked like differential Z-updates over a shared topology, suggesting a coarse 10 × 120 terrain mesh. Rendering body 12 as a grid quad-mesh wireframe (row + column edges) produces the wrong silhouette; pair-wise edge interpretation ((r0,r1) (r2,r3) ...) likewise doesn't yield a coastline-like contour.
  3. Heterogeneous via a single non-xz projection. Rendering on xy (front side view) surfaces clean vertical pillar/column silhouettes for bodies 9 and 11 (which have 25K-27K Y span). The xy all-bodies overlay also looks more map-like than xz overall. But no single axis pair produces a coherent map across all 15 bodies, and the recognizable shapes in xy look like 3D objects seen sideways - not map outlines.

Consumer call sites

Two distinct SCUS-resident reader clusters consume slot 4. Both are byte-identical across all three kingdoms — same PC ranges, same caller RAs — proving the consumer is generic SCUS code, not per-kingdom overlay code.

ReaderEntry / PC rangeCaller RA
Cluster A — TMD-style primitive renderer (FUN_80043390 + per-kind handlers)dispatcher entry 0x80043390; per-kind handler bodies at 0x80043658..0x800459880x8001B47C (inside FUN_8001ada4), 0x801F78D4 (world-map overlay) — both in every kingdom
Cluster B — secondary mid-body reader0x80059DE40x80059C00 (SCUS) — identical across all three kingdoms

FUN_80043390 (712 bytes / 178 instructions) takes three arguments:

void FUN_80043390(struct *display_state, u32 cmd_flags, u32 fade_flags);
//   display_state[0]   -> vertex pool base  (a2 in handlers = param_3)
//   display_state[3]   -> non-zero gates the color/light-modulation path
//   display_state[4]   -> command-stream pointer (where slot-4 records feed in)

It reads one u32 command word from display_state[4] (the command stream pointer), extracts a 15-bit kind from bits 17-31 and a 16-bit count from bits 0-15, optionally re-arms the GTE colour registers, and tail-calls a per-kind handler through a jump table. Each handler consumes its own command's primitive batch (count items of a kind-specific stride), emits GP0 packets into the active primitive pool, then chain-calls the next kind handler at the same dispatch point — the renderer is a TMD-style display-list walker, not a fixed-size record loop.

Two parallel handler tables drive the dispatch: SCUS handlers at 0x8007657C (default), or overlay handlers at 0x801F8968 when _DAT_1F800394 & 1 is set (the alternate route for the bulk-terrain pipeline). Within the SCUS table the dispatcher adds a bank offset to the kind*4 index per the literal disassembly of FUN_80043390:

_DAT_1f800028 = 0;
if (fade_flags != 0) {                            // param_3 != 0
    _DAT_1f800028 = 0x50;                          // baseline
    if ((cmd_flags & 0x04000000) != 0) _DAT_1f800028 = 0xA0;
    if ((cmd_flags & 0x20000000) != 0) _DAT_1f800028 = 0xF0;
}

So there are four banks, not three. The two ifs are sequential (not else-if), so the 0x20000000 branch wins when both flags are set. And bank 0 / bank 1 are gated by fade_flags, not by cmd_flags bits:

fade_flagscmd_flags bitsBank offsetEffect
== 0(ignored)0x00bank 0 — kinds 12-19 use the small 0x80043658..0x80043F10 handler set
!= 0neither high bit set0x50bank 1 — kinds 12-19 swap to 0x800448B0..0x80045584
!= 00x04000000 set, 0x20000000 clear0xA0bank 2 — kind 18/19 swap to 0x800457C4 / 0x80045988
!= 00x20000000 set0xF0bank 3 — likely dev / debug mode; never observed in retail world-map render

Kinds 8-11 are shared across all banks. Empirically (Drake post-warp settled, 19,935 dispatcher-entry hits via autorun_slot4_dispatcher_args.lua), retail world-map render exercises only banks 0x00 (77%) and 0x50 (23%); both 0x04000000 and 0x20000000 cmd_flags bits are never set, so banks 2 and 3 stay dark. The bank distinction in retail play is purely the fade_flags != 0 toggle.

Per-kind primitive types

Every handler has the same shape: read N command-stream words, transform 3-or-4 vertices through the GTE, write an M-byte GP0 packet at the primitive-pool pointer. The strides give away the PSX primitive type:

KindBank 0 entryBanks 1,2 entrycmd strideGP0 strideLikely primitive
80x8004409c (shared)(shared)0x14 (20B)0x20 (32B)POLY_G4 (gouraud quad)
90x8004423c (shared)(shared)0x18 (24B)0x28 (40B)POLY_GT4 (gouraud-textured quad)
100x80044434 (shared)(shared)0x18 (24B)0x28 (40B)POLY_GT4 variant
110x800445b0 (shared)(shared)0x1c (28B)0x34 (52B)extended quad (extra per-vert data)
120x800436580x800448b00x0c (12B)0x14 (20B)POLY_F3 (flat triangle)
130x800437680x80044a3c0x0c (12B)0x18 (24B)POLY_G3 / POLY_FT3
140x80043b580x80044fdc0x14 (20B)0x1c (28B)POLY_FT3
150x80043c6c0x800451940x18 (24B)0x24 (36B)POLY_GT3
160x800438b80x80044c140x14 (20B)0x20 (32B)POLY_G4
170x800439e40x80044dc80x18 (24B)0x28 (40B)POLY_GT4
180x80043dd40x800453bc (b1) / 0x800457c4 (b2)0x1c (28B)0x28 / 0x20POLY_GT4 extended
190x80043f100x80045584 (b1) / 0x80045988 (b2)0x24 (36B)0x34 / 0x28POLY_GT4 extended-plus

Each handler decodes the per-command words as two packed vertex indices per u32 (low-16 & 0x7FF8, high-16 also & 0x7FF8 — a >>3 divisor plus 8-byte vertex stride from param_3 = the vertex pool base).

Cross-kingdom hit-count comparison

Exec-breakpoint hit counts at the eight cluster-A LW PCs + the cluster-B LW PC during a single warp-tile transition. All three kingdoms captured with LEGAIA_PC_CAP=50000 over 1800 vsyncs; no PC saturates the cap, so the totals are exact:

KingdomsstateCluster A totalCluster BCluster A RAs observed
Drakealready on map01, held UP71,3311780x8001B47C, 0x8001BC8C, 0x801F78D4
Sebucustown → map02, held DOWN90,096670x8001B47C, 0x801F78D4
Karistotown → map03, held DOWN13,5931150x8001B47C, 0x801F78D4

Sebucus's cluster-A total is higher than Drake's despite Sebucus's slot-4 being smaller — confirming hit-count tracks scene-render volume, not slot-4 record count. Cluster B's variance is the inverse: Drake walks the most slot-4 bodies, then Karisto, then Sebucus.

Per-kind delta

With the cluster-A LW PCs mapped to specific kind handlers, the per-PC × per-kingdom hit counts surface a clean signal. All three kingdoms captured uncapped (LEGAIA_PC_CAP=50000 over 1800 vsyncs):

Kind handlerPrimitive (likely)DrakeSebucusKaristo
13 banks 1,2 (0x80044A3C)POLY_G3 / POLY_FT3 triangle9,4652,04049
17 banks 1,2 (0x80044DC8)POLY_GT4 textured quad762240147
18 bank 1 (0x800453BC, ×4 LW PCs)POLY_GT4 extended quad13,561 (×4)20,601 (×4)1,820 (×4)
16 banks 1,2 (0x80044C14)POLY_G4 quad7,6888782,058
15 banks 1,2 (0x80045194)POLY_GT3 textured triangle6,8605,4122,059
cluster B (0x80059DE4)mid-body reader17867115
  • Kind 13 scales sharply: Drake (9,465) ≫ Sebucus (2,040) ≫ Karisto (49). Drake / Sebucus have many small triangle primitives; Karisto barely uses them.
  • Kind 17 scales with overall scene weight: Drake (762) > Sebucus (240) > Karisto (147). Ratio Drake / Karisto ≈ 5.2.
  • Kind 16: Karisto-heavy (2,058) / Drake-heavy (7,688) but Sebucus uses it least (878).
  • Kind 18 (extended quad) is the absolute workhorse — Sebucus dispatches 20,601 instances of it (~80% of cluster-A primitive count), Drake 13,561, Karisto 1,820. Dominant per-frame primitive across every kingdom.
  • Cluster B (the mid-body reader): Drake (178) > Karisto (115) > Sebucus (67) — Drake's larger slot 4 visits more of the secondary reader's body subset.

Captured CSVs land under captures/slot4_uncapped/ (per-row flushed, safe to inspect mid-run). The dispatcher-entry probe CSV at captures/slot4_dispatcher/ gives the first-kind / cmd_flags / fade_flags per call.

Cluster-A caller (FUN_8001ada4)

FUN_8001ada4 (2456 B / 614 instructions) is the per-actor renderer that walks a linked list of actor records and calls cluster A for each one's meshes. For each actor, the mesh-table at actor+0x44 is [u32 count, u32 mesh_ptr[count]]. Each mesh_ptr is the TMD-style struct passed as param_1 to FUN_80043390. The captured cluster-A LW PCs all have ra = 0x8001B47C (i.e., FUN_8001ada4's call site at PC 0x8001B474) for the per-frame path; the warp-transition path uses ra = 0x801F78D4 (a world-map overlay function).

Current working hypothesis

Slot 4 is most likely a runtime library of small object-local 3D meshes the world-map controller / dev-menu top-view places at world coordinates, walked through the cluster-A primitive renderer above. Plausible roles for individual bodies: collision hulls, instantiable decoration meshes, particle-emitter shapes, debug-overlay geometry, or animation rigs. This is consistent with:

  • bodies 9 / 11 having full 3D mesh-scale Y extents while body 12 is near-flat (different kinds of objects, not 2D contour vs 2D outline)
  • the leading three bodies being byte-identical templates across all three kingdoms (shared generic objects, not kingdom-specific data)
  • the corner-clustered point distribution in body 13 (kind = 4): could be four corner-anchored objects, not a single ±32K boundary frame
  • the in-game-object silhouettes visible in side projections — the user identified body-9 features that resemble specific game props
  • cluster A's primitive vocabulary (POLY_G4 / POLY_GT4 / POLY_F3 / etc.) is a standard 3D-mesh renderer, not a wireframe / coastline-line driver

The reader doesn't appear as a direct LUI+ADDIU reference to 0x8011A624 in any captured world-map overlay — the cluster-A consumer reaches slot-4 bytes through DAT_8007C018[94..113] (20 body-aligned pointers) and through per-actor mesh tables, not via a direct LUI+ADDIU materialization. Steady-state dev-menu top-view registers zero direct reads across the slot-4 region.

Tooling

These remain useful for future RE work even though their original purpose ("render the world-map wireframe") is no longer valid:

ToolRole
cargo run -p legaia-asset --bin asset -- slot4-png --input <PROT>.BIN --out <png>Container PNG renderer. --style row|col|pairs|grid|points toggles between topology interpretations; --axes xz|xy|zy switches projection plane; --only-body N / --frame-body N isolate a single body. --from-raw <bin> renders a previously-dumped slot-4 payload.
cargo run -p legaia-asset --bin asset -- kingdom-slot <PROT>.BIN --slot 4Per-body inventory dump.
legaia_asset::world_map_overlay::{parse, top_down_lines, record_points, body_axis_range}Rust API.
scripts/pcsx-redux/run_dump_slot4.sh + autorun_dump_slot4.luaPCSX-Redux closed-loop dumper: loads a save state, dumps the live slot-4 RAM region, quits.
scripts/pcsx-redux/autorun_dump_full_ram.luaFull 2 MiB main RAM dump. Use when the load base is unknown.
scripts/pcsx-redux/diff_slot4_ram_vs_disc.pyByte-compare a RAM dump against the disc-decoded payload.

The world-overview web viewer does not expose slot 4 (the wireframe interpretation is falsified). The WASM exports (slot4_wireframe_lines / slot4_wireframe_points / slot4_wireframe_bounds) remain available to re-enable a slot-4 draw if a future RE pass identifies the correct interpretation.

Slot-4 loader (loader-hunt probe)

Running autorun_slot4_loader_hunt.lua against Drake (held UP for 60 vsyncs into the warp) with Write bps tiled across slot-4 RAM (0x8011A624 + offset[0..7000]) surfaced the LZS decoder as the sole writer:

Caller chainPC of writeNotes
FUN_8001A55C (LZS decoder)0x8001A604 (sb v1, 0(s1) literal-byte write)Dominant: 5-byte bursts at every probed offset
same0x8001A664 / 0x8001A668 / 0x8001A610 / 0x8001A5ACBack-reference / literal-run / dictionary-byte paths inside the LZS loop

Every captured first-write shows pc = 0x8001A604, ra = 0x8001A58C (LZS calling itself internally), with the stack containing 0x8001F194 / 0x8001F0A0 at the call-chain slots — both inside the FUN_8001F05C asset-dispatcher region. The chain is the standard asset-load path: scene loader → FUN_8001F05C (asset dispatcher) → LZS decoder → writes slot 4 at its allocated RAM destination. No special slot-4 transcoder — the asset is just LZS-decoded verbatim into RAM, matching the byte-verified disc-to-RAM finding.

Working-buffer writers (transcoder-hunt probe)

Running autorun_slot4_transcoder_hunt.lua against Drake (held UP for 60 vsyncs into the warp transition) with Write bps tiled across the 0x801BA000 working buffer surfaced two distinct writers, not a single transcoder:

OffsetFirst-write PCRAWriter functionRole
+0x7F8 (cluster A's vertex_base)0x80028710 / 0x8002871C0x8001B160FUN_80028158 (5580 B)per-frame procedural mesh builder, called from FUN_8001ada4 case 4
+0x8E4 (cluster A's command_stream)0x800293C8 / 0x800296A00x8001B160same FUN_80028158per-frame procedural primitive-batch writer (same call)
+0x6000 (deeper region)0x8001A8C8 (memcpy)0x8001E758FUN_8001E54C (836 B)scene-load chunk loader — copies [type, size, data] chunks to the buffer

FUN_80028158 reads only the actor's +0x9C params struct (offsets +0x10..+0x22) and writes the working buffer directly — no slot-4 RAM pointers in its arguments. It is a procedural mesh generator (probably waves / sky / particle-emitter sheets), not a slot-4 transcoder.

FUN_8001E54C is the [type, size, data] streaming chunk dispatcher: switches on *(char*)(chunk + 3) (chunk type byte) and routes each chunk to memcpy (case 0/2), LZS decode (case 1/3), or another decoder (case 12). Its 4 captured writes at 0x801C0000 are scene-load chunk copies that land deeper into the buffer than cluster A's per-frame inputs.

Revised model: slot 4 is not transcoded into a single working-buffer region. At scene load, FUN_8001E54C (or a sibling streaming-chunk processor) reads the kingdom bundle's chunks and distributes their bytes across multiple destinations — actor structs, working buffer at different offsets, etc. Per-frame, cluster A reads the working buffer (now populated with scene-load data plus per-frame procedural patches from FUN_80028158). The cross-kingdom Exec-bp captures sample per-frame steady state, where cluster A reads the working buffer — NOT slot 4 directly. The high per-frame cluster-A hit counts (~2000 in 1800 frames) are procedural rendering volume, not slot-4 walks.

DAT_8007C018 — global TMD pointer table (the actual cluster-A source)

FUN_80043390's display_state arg points at a TMD's group-descriptor array (offset +0xC into a TMD blob whose +0x00 carries the Legaia magic 0x80000002). Those TMD pointers live in a global runtime table:

DAT_8007C018 : array of u32 TMD pointers; entry stride = 4
DAT_8007B774 : install counter (next free index)
DAT_8007BB38 : walk counter (last valid index, used by the table walker)
DAT_8007B824 : per-pack count (set by case 2 to *pack_header[0])

The installer is FUN_80026B4C @ PC 0x80026BA8 (called per-TMD from the asset dispatcher's case 2 TMD-pack handler):

80026b90  lui   v1, 0x8008
80026b94  lw    v1, -0x488c(v1)     ; v1 = *DAT_8007B774 (next free idx)
80026b98  addiu v0, v0, -0x3fe8     ; v0 = 0x8007C018
80026b9c  sll   v1, v1, 0x2
80026ba0  addu  v1, v1, v0          ; v1 = &DAT_8007C018[idx]
80026ba4  jal   FUN_800268dc        ; build per-group descriptor array at tmd+0xC
80026ba8  _sw   a0, 0x0(v1)         ; install: DAT_8007C018[idx] = tmd_ptr

Ghidra's static reference-database doesn't surface this store because the addu between the lui+addiu and the sw defeats its constant propagation. The materialisation scan ghidra/scripts/find_addr_materializer_dat_8007c018.py walks every lui+addiu pair that produces 0x8007C018 and looks at the next six instructions; that's how the installer was pinned.

After installation, each pointed-to TMD has the runtime shape:

[+0x00] u32 magic = 0x80000002
[+0x04] u32 flags / version
[+0x08] u32 group_count
[+0x0C] array of group_count × 0x1C-byte group descriptors
        each starts with vertex_base_ptr (u32) + vertex_count (u32)
        followed by 0x14 bytes of per-group state

Consumers

FunctionSiteRole
FUN_80021B04 (SCUS actor allocator)reads DAT_8007C018[actor[+0x64].i16]populates actor[+0x44] = [count, mesh_ptr[count]] from TMD groups
FUN_80024D78 (SCUS actor allocator — variant)reads DAT_8007C018[actor[+0x64].i16]same shape as FUN_80021B04 but also OR-sets actor[+0x10] |= 0x08000000 (a per-actor enable flag)
FUN_801D77F4 (overlay alt allocator)reads DAT_8007C018[(i16)param_2] + _DAT_8007B7DC VDF buffercopies vertex pool from sub-records into actor[+0x90]
FUN_801D8280 (overlay table walker)iterates DAT_8007C018[0..DAT_8007BB38]hands each sub-record to FUN_801D5E20
FUN_801F69D8 (world-map top-view dispatcher in world_map_top_ext)reads DAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4]walks per-tile visibility scratchpad, calls FUN_80043390(tmd+0xC, color, fog)
FUN_8001E890writes entry[+0x8] = 10 for three consecutive table indices at DAT_8007B824 + 0..2per-pack count override (overwrites the installed TMD's group_count field)
FUN_8001EBECreads DAT_8007C018[DAT_8007B824 + 0..2] (3 consecutive party-character TMDs)per-party-member group-descriptor patch — picks one of two pre-built 0x1C-byte descriptors (TMD+0x124 vs TMD+0x140) based on a per-character flag, then overwrites the indexed group descriptor. Drives equipment-conditional mesh swaps

Warp-transition caller (FUN_801F69D8)

The Drake warp-into-world-map Read-bp captured ra = 0x801F725C, pointing at the JAL into FUN_80043390 at PC 0x801F7254. The enclosing function is FUN_801F69D8 (2572 B / 643 instr) in overlay_world_map_top_ext.bin. The probe's original label "FUN_801F7088" was inside the function body, not at its entry — the prologue addiu sp, sp, -0x70 sits 1712 bytes earlier at 0x801F69D8. Force-disasm + walk-back + force-set body pinned the true extent; see ghidra/scripts/dump_world_map_top_ext_caller.py.

The function bulk-copies a 0x20-byte camera struct from 0x8007BF10 into scratchpad 0x1F8002CC, nested-loops over visible tile cells in scratchpad table _DAT_1F8003EC + 0x8000 + Y*0x100 + X*2 (padded by ±10), dereferences each 0x20-byte object record at _DAT_1F8003EC + (idx & 0x1FF) * 0x20, applies frustum bounds + visibility flags + GTE RTPT projection, then routes the TMD via DAT_8007C018 and emits the display list. Arg breakdown for the FUN_80043390 call:

  • arg0 = TMD group descriptor array start (tmd + 0xC)
  • arg1 = color RGB (0xD0D0D0 default; 0x40D0D0D0 if object record's [+0x1E] flag is set; OR'd with 0x10000000 if record[+0x12] & 0x800 — interactive)
  • arg2 = fog cue clamp((GTE_screen_z - 0x5000) >> 3, 0, 0x1000)

How slot-4 bytes reach cluster A

The cluster-A input pointer originates from DAT_8007C018 (the global TMD pointer table). Two parallel call paths funnel into the same dispatcher:

  1. Top-view dispatcher (FUN_801F69D8): reads DAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4] per tile and passes entry + 0xC to FUN_80043390. This is the warp-into-world-map render path the Read-bp probe captured.
  2. Per-actor renderer (FUN_8001ada4, caller RA 0x8001B47C): walks actor+0x44 = [u32 count, u32 mesh_ptr[count]] and passes each mesh_ptr to FUN_80043390. The mesh pointers came from actor+0x44, which is populated by FUN_80021B04/FUN_80024D78 from DAT_8007C018[actor[+0x64].i16] — same table, different actor-allocator path.

Slot 4 of the kingdom bundle (type = 0x05 = MOVE) is the largest data slot but is not directly consumed by cluster A. The MOVE buffer at the kingdom-load destination (a mid-warp Drake dump pinned it to 0x8011A624, but the address varies per build / save state) gets overwritten by later TMD-pack installs whose TMDs occupy the same physical RAM by the time the world-map enters steady state. The Read-bp probe that captured "slot-4 bytes being read by cluster A" was sampling that buffer after the TMD overwrite — cluster A was reading TMDs at addresses that had once held slot-4 bytes, not the slot-4 bytes themselves.

The slot-4 body header kind ∈ {1, 2, 4} therefore has no link to the cluster-A bank selector (see the bank-selector table above). Whatever slot 4 encodes, it's consumed during the warp's first asset pass and converted into TMD blobs by an as-yet-unpinned step before the world map runs.

Live snapshot — settled field scene

Provenance correction: the local dump is named drake_world.bin, but its scene id (0x80084540 = 0x3c), scene name (dolk), and game_mode 0x03 identify it as the dolk field scene, not the Drake world map. DAT_8007C018 is filled identically by every field-scene load (the single FUN_80020224 descriptor-walk), so the layout/counters below are a valid generic field-scene example; the [5..142] entries are this scene's field-file TMD pack (one contiguous 138-entry pack), not a "kingdom bundle". type-0x05 slot-4 does not install into DAT_8007C018 — only dispatcher cases 0x02/0x09 reach FUN_80026B4C.

RAM dump after the scene load has settled (full RAM captured by autorun_dump_full_ram.lua + the classifier at scripts/classify_dat_8007c018.py):

FieldValue
DAT_8007B774 (install counter)143
DAT_8007BB38 (walker counter)142
DAT_8007B6F8 (kingdom-TMD prefix)5
DAT_8007B828 (error bits)0x00000000 (no magic mismatches)
Index rangeCountContent
[0..4]5Character-mesh TMDs at 0x8014D554..0x801585C0, group_count 10/10/10/3/2. Disc source: § Disc-side source of [0..4] below.
[5..142]138Kingdom-derived TMDs at 0x800F7908..0x80138D44 (group_count 1..10, mixed sizes)
[143..255]113Either zero (uninstalled) or stale junk past the walker counter — never read by code because every reader gates on DAT_8007BB38 or an explicit index ≤ install counter

Every populated entry is a valid Legaia TMD (magic 0x80000002, flags = 1, group_count > 0). The table is homogeneous in the steady state. The kingdom-derived 138 TMDs include the slot-4 body-aligned addresses formerly classified as a separate carrier (e.g. [94..113] land in 0x8011A7B0..0x8012202C — all inside the slot-4 RAM window — but the bytes there have already been overwritten with TMD blobs by the time the snapshot is taken; the slot-4 outer-pack signature is absent from steady-state RAM).

Disc-side source of [0..4]

The five character-mesh TMDs at DAT_8007C018[0..4] originate from PROT entry 0874 (befect_data), not from the dev-tree path data\field\player.lzs (whose runtime name maps to PROT 876 — player_data — which actually carries a VAB + TIM_LIST + SEQ streaming-format payload with zero TMDs; see data-field.html for the chunk shape).

PROT 0874 is a parse_player_lzs(buf, 3)-shaped container with three LZS-compressed sections:

SectionType byteCompressed sizeFile offsetContent
00x010xB49C (46 236 B)0x205-TMD pack (LZS decodes to 65 536 B)
10x020x41E0 (16 864 B)0x5037Secondary TMD payload
20x030x1D524 (120 100 B)0x7055MAN-shape data

Decoding section 0 (LZS-decompress from file offset 0x20) yields a canonical TMD pack — [u32 count][u32 word_offsets[count]][TMD bodies] with word offsets in 4-byte units (same convention as tim-pack / kingdom slot 1):

Pack slotBody offsetnobj (disc)Body bytes (to next slot)
00x00181213 220
10x33BC1213 800
20x69A41211 656
30x972C3 6 488
40xB0842 20 348 (trailing padding to pack end)

Byte-equality check against a Drake post-warp RAM snapshot (local-only captures/ram_dumps/drake_world.bin):

  • Pack slot 3 vs RAM DAT_8007C018[3] (un-fixup the runtime's absolute-pointer group descriptors back to disc-form offsets using disc_off = abs_ptr - (tmd_base + 0xC)): the full 6488-byte body matches byte-for-byte (0 differences over 0x1958 bytes compared).
  • Pack slot 4 vs RAM DAT_8007C018[4]: the first 1048 bytes match byte-for-byte. The runtime allocates only the in-use prefix; the trailing ~19 KB of disc padding is not copied.
  • Pack slots 0/1/2 vs RAM DAT_8007C018[0..2]: the first three group descriptors and groups 4..9 match byte-for-byte. RAM's nobj=10 vs disc's nobj=12 is a deliberate runtime override (see “10-group cap” below); RAM's slot-3 group descriptor is sourced from disc's group 11 (the FUN_8001EBEC patch).

10-group cap + equipment-conditional group patch

The five disc TMDs ship with nobj=12 (for the three active-party slots) and nobj=3 / 2 (for the trailing two — confirmed nobj from the disc pack matches RAM exactly for those). The active-party post-install loop in FUN_8001E890 overwrites DAT_8007C018[DAT_8007B824 + 0..2]'s entry[+0x08] (TMD group_count) to 10, capping each of the first three TMDs at 10 active groups. The last two disc groups (10 and 11) are equipment-conditional descriptors: FUN_8001EBEC reads two per-character bytes (from 0x80084xxx, equipment slots) and for each of the three active party slots picks either TMD+0x124 (= group 10) or TMD+0x140 (= group 11) and overwrites the indexed live group descriptor with that pre-built 0x1C-byte template. This is the equipment-conditional mesh swap (weapon variant, etc.).

Loader chain — partly open

FUN_8001E890's retail-PROT branch (DAT_8007B8C2 != 0) calls FUN_8003eb98(0x36C, piVar2, 1), which loads PROT 876's raw bytes into piVar2. The downstream LZS calls then interpret piVar2[2..7] as three (size, offset) pairs — but PROT 876's bytes there are streaming-format chunk data (the start of a VABp header inside chunk 0), not LZS descriptors. That branch is therefore incompatible with PROT 876's actual layout in retail and either (a) is gated off by DAT_8007B8C2 == 0 in retail or (b) is dead code. The data\field\player.lzs string and PROT-876 fast path both fall over the same shape mismatch.

The retail loader that actually installs PROT 0874's section 0 into DAT_8007C018[0..4] is not yet pinned. FUN_800520F0 (the battle scene loader) loads PROT 873+874 contiguously, but its two install loops walk both buffers as flat [count, offsets[], data] packs and process PROT 874's count = 3 entries via FUN_8001fbcc (VDF install). PROT 874 section 0 is gated on the type byte (0x01), so a different dispatch site must funnel section 0 through the TMD-pack handler (FUN_8001F05C case 2FUN_80026B4C) rather than VDF. Tracing that site is in Open work below.

Live snapshot — Sebucus mid-warp

The Sebucus dump captures the warp transition partway through the asset install:

FieldValue
DAT_8007B774 (install counter)92
DAT_8007BB38 (walker counter)91
DAT_8007B6F8 (kingdom-TMD prefix)5
DAT_8007B828 (error bits)0x00000000

Entries [0..91] are valid TMDs; the install is in flight, so the TMD-pack handler has not yet completed pushing every member. Entries [92..] carry leftover pointers from a previous game-state's table fill, but DAT_8007BB38 = 91 means no consumer ever reads past index 91. The mid-load state is what historical "non-TMD entry classification" passes appear to have sampled — those reads went past the walker counter and treated stale leftover pointers as table content, producing the previously-reported "[45..53] FFFAFFFA" / "[114..193] mixed text/vertex/texture" classifications. With DAT_8007BB38 as the authoritative bound, those characterisations are out-of-bounds reads, not table contents.

Open work

  1. Slot-4 → TMD converter. Slot 4 is loaded into RAM as MOVE bytes during the warp's first pass, but the same physical RAM gets overwritten by TMD blobs from a later-loaded TMD-pack before the world-map settles. The intermediate converter that walks the slot-4 15/16-body outer pack and emits TMDs at the right RAM offsets has not been pinned. Candidates: a chunk-handler case in FUN_8001E54C that turns slot-4 sub-bodies into TMD-shaped blobs in-place, or an asset-load-chain step that doesn't lower through FUN_8001F05C at all. A Read-bp probe watching DAT_8007C018[*] entry addresses (not slot-4 RAM) during the install pass would identify which loader call populates the kingdom-derived entries [5..N].

    Static-side evidence narrowing the hunt (sweep via scripts/scan_funcs_for_addr_range.py across SCUS + every captured overlay dump):

    • _DAT_8007B888 (MOVE-buffer pointer set by FUN_8001F05C case 5): 7 accessor sites in the entire dump corpus. SCUS: FUN_8001F05C (writer, the case-5 store), FUN_8002541C (writer, streaming-walker reset), FUN_800204F8 (reader — Tactical Arts move-table parser). Overlays: 4 reader sites, all in overlay_baka_fighter (the boxing minigame). Zero readers in any overlay_world_map* dump. If the kingdom slot-4 → world-map pipeline went through the standard MOVE buffer, the world-map controller would need to read _DAT_8007B888 somewhere — and it doesn't, statically. So the converter either runs before the warp's overlay swap-in (in SCUS code that doesn't read the MOVE pointer by name), or the slot-4 MOVE bytes are consumed via the asset-loader chain itself (a hook inside FUN_8002541C or its descriptor-walker sibling FUN_80020224) before the world-map overlay even sees them.
    • DAT_8007C018[94..113] (the index range whose live snapshot once held slot-4-body-aligned pointers): zero specialized readers — no function statically materializes any address in 0x8007C190..0x8007C1E0 via lui+addiu, lui+lw_with_offset, or positive-offset lw from the table base. Consistent with the live-snapshot finding that those entries are real TMDs in steady state and are reached only through the generic table walkers that iterate [0..DAT_8007BB38].
  2. Per-record 4th i16 (attr). 0 for body 4, 22 distinct values in body 5, 214 distinct in body 12. Body-12 attr-values cluster at ±1280, ±1792, 1793, ±1281, ±1025 — look like packed (high-byte, low-byte) tags rather than indices. There is no body-kind ↔ cmd_flags bank link — Drake's dispatcher-entry probe shows neither 0x04000000 nor 0x20000000 is ever set in retail world-map play; only banks 0x00 and 0x50 are exercised. So body kind ∈ {1, 2, 4} is slot-4-internal data with no link to cluster-A bank dispatch.
  3. Banks 2 (0xA0) and 3 (0xF0). Banks reachable in the dispatcher but never observed during retail world-map play. Candidates: dev/debug menu render modes, battle-overlay re-use of the dispatcher, or cutscene render paths. Setting up a wide-coverage cmd_flags-capture probe across multiple non-world-map game modes would pin which (if any) caller passes those flags.
  4. PROT 0874 section-0 loader site. The byte-equality match between PROT 0874 section 0 (LZS-decoded from file offset 0x20) and the 5 TMDs at DAT_8007C018[0..4] is conclusive (see § Disc-side source of [0..4] above). The inner dispatch is fully pinned: FUN_80020224(asset_type) walks _DAT_8007B85C as an asset-descriptor pack, calling FUN_8001F05C(buf + offset, size, type, 0) per record; case 2 of FUN_8001F05C LZS-decodes the section, walks the [u32 count][u32 word_offsets[count]][TMD bodies] pack and dispatches each entry through FUN_80026B4C (the TMD-pack installer). The corpus callers of FUN_80026B4C include FUN_8001E890, FUN_8001E928, FUN_800520F0, FUN_800513F0, FUN_800542C8, and the muscle-dome minigame loader.
    The outer producer that populates _DAT_8007B85C with PROT 0874's bytes is not pinned in the static SCUS_942.54 dumps: FUN_8001E890's retail-PROT branch targets PROT 876 (0x36c) via FUN_8003eb98, but PROT 876 is a streaming-format file (VABp header + TIM_LIST + SEQ) whose layout is shape-incompatible with the downstream parse_player_lzs(buf, 3) walk - so either the branch is dead code in retail, or _DAT_8007B85C is populated from PROT 874 elsewhere first. FUN_800520F0 (battle scene loader) is the only static SCUS caller that issues FUN_8003eb98(0x36A, ...), but it processes PROT 874 through FUN_8001fbcc (VDF install) rather than the asset-descriptor walker. The dispatch most likely lives in an overlay-resident scene loader (the FUN_801D6704 family) that populates _DAT_8007B85C from PROT 874 before invoking FUN_80020224 - a Write-bp probe on DAT_8007C018[0] during boot / first-battle init would isolate the exact site.
    A further static narrowing: the FUN_8001F05C case-2 "freeze" sub-path (if (param_3 == 1) { _DAT_8007B704 = size; _DAT_8007B824 = pack_count; }) is the sole SCUS sw writer of _DAT_8007B824 (at PC 0x8001F2F8). The freeze sets the persistent-base index that FUN_8001E1B4 later reads to reset the install cursor (DAT_8007B774 = _DAT_8007B824), so a non-zero _DAT_8007B824 would mark slots [0..pack_count-1] as carried across mode transitions. A corpus grep over every call site shows zero static SCUS callers of FUN_8001F05C pass param_3 == 1 (the three direct callers - FUN_80020224, FUN_8002541C, and overlay_baka_fighter_801d4c50 - pass s6, 0, and 0 respectively), and zero dumped overlay callers of FUN_80020224 pass param_1 == 1. So either the freeze path is in an overlay not yet captured, or _DAT_8007B824 stays at its BSS-init value of zero throughout retail play and every mode rebuilds the TMD pool from index 0 (in which case the "persistent slots" semantic is vestigial, not load-bearing). The dynamic probe should also break on _DAT_8007B824 writes to settle which case holds.

See also