Slot-4 records (consumer pinned) Container confirmed; per-record semantic open
Status: container confirmed; consumer pinned; per-record semantic open. The container layout below is byte-verified against live RAM. The consumer is now pinned to FUN_80043390 — a TMD-style display-list renderer that dispatches over 22 per-kind handlers (kinds 8-19 across 3 SCUS banks) and emits GP0 primitive packets into the active scene primitive pool. See Consumer call sites. The historical "world-map wireframe / coastline" reading was falsified; the working interpretation is a runtime library of small object-local 3D meshes rendered as PSX triangles and quads.
The bulk continent terrain itself — the ~4300 POLY_FT4 prims that tile the kingdom continent in the dev-menu top-view — is not sourced from slot 4. It comes from the same kingdom slot-1 TMD pack the landmarks draw from, routed through FUN_80043390's overlay-mode dispatch table at 0x801F8968 whose eight high-mode renderers replace the SCUS variants when the world-map overlay is paged in. See subsystems / world-map — bulk continent terrain emit mechanism.
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:
| Bundle | PROT index | CDNAME label | Decoded size |
|---|---|---|---|
| Drake | 0085 | map01 | 32304 |
| Sebucus | 0244 | map02 | 26964 |
| Karisto | 0391 | map03 | 24444 |
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)
| Body | count_a | count_b | kind | flag_a | records | X span | Y span | Z span |
|---|---|---|---|---|---|---|---|---|
| 0 | 10 | 20 | 1 | 0 | 200 | 16626 | 10767 | 38641 |
| 1 | 10 | 20 | 1 | 0 | 200 | byte-identical template (matches Sebucus / Karisto) | ||
| 2 | 10 | 30 | 1 | 0 | 300 | byte-identical template | ||
| 3 | 2 | 30 | 2 | 0 | 60 | pinned plane (degenerate) | ||
| 4 | 2 | 20 | 2 | 0 | 40 | pinned plane (degenerate) | ||
| 5 | 10 | 30 | 2 | 0 | 300 | 25 unique of 30 groups | ||
| 6 | 10 | 26 | 2 | 0 | 260 | |||
| 7 | 10 | 30 | 2 | 0 | 300 | 25 unique of 30 groups | ||
| 8 | 10 | 3 | 2 | 0 | 30 | 3 identical groups (filler / padding) | ||
| 9 | 12 | 30 | 2 | 0 | 360 | 10725 | 25856 | 21248 |
| 10 | 12 | 30 | 2 | 0 | 360 | 13056 | 18432 | 31503 |
| 11 | 12 | 10 | 2 | 0 | 120 | 11492 | 27648 | 24064 |
| 12 | 10 | 120 | 2 | 0 | 1200 | 16118 | 4096 | 31473 |
| 13 | 14 | 15 | 4 | 1 | 210 | 65485 | 14336 | 64512 |
| 14 | 2 | 30 | 2 | 0 | 60 | |||
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):
- 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
xzproduces 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. count_a × count_bheightfield 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.- Heterogeneous via a single non-
xzprojection. Rendering onxy(front side view) surfaces clean vertical pillar/column silhouettes for bodies 9 and 11 (which have 25K-27K Y span). Thexyall-bodies overlay also looks more map-like thanxzoverall. But no single axis pair produces a coherent map across all 15 bodies, and the recognizable shapes inxylook 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.
| Reader | Entry / PC range | Caller RA |
|---|---|---|
Cluster A — TMD-style primitive renderer (FUN_80043390 + per-kind handlers) | dispatcher entry 0x80043390; per-kind handler bodies at 0x80043658..0x80045988 | 0x8001B47C (inside FUN_8001ada4), 0x801F78D4 (world-map overlay) — both in every kingdom |
| Cluster B — secondary mid-body reader | 0x80059DE4 | 0x80059C00 (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_flags | cmd_flags bits | Bank offset | Effect |
|---|---|---|---|
== 0 | (ignored) | 0x00 | bank 0 — kinds 12-19 use the small 0x80043658..0x80043F10 handler set |
!= 0 | neither high bit set | 0x50 | bank 1 — kinds 12-19 swap to 0x800448B0..0x80045584 |
!= 0 | 0x04000000 set, 0x20000000 clear | 0xA0 | bank 2 — kind 18/19 swap to 0x800457C4 / 0x80045988 |
!= 0 | 0x20000000 set | 0xF0 | bank 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:
| Kind | Bank 0 entry | Banks 1,2 entry | cmd stride | GP0 stride | Likely primitive |
|---|---|---|---|---|---|
| 8 | 0x8004409c (shared) | (shared) | 0x14 (20B) | 0x20 (32B) | POLY_G4 (gouraud quad) |
| 9 | 0x8004423c (shared) | (shared) | 0x18 (24B) | 0x28 (40B) | POLY_GT4 (gouraud-textured quad) |
| 10 | 0x80044434 (shared) | (shared) | 0x18 (24B) | 0x28 (40B) | POLY_GT4 variant |
| 11 | 0x800445b0 (shared) | (shared) | 0x1c (28B) | 0x34 (52B) | extended quad (extra per-vert data) |
| 12 | 0x80043658 | 0x800448b0 | 0x0c (12B) | 0x14 (20B) | POLY_F3 (flat triangle) |
| 13 | 0x80043768 | 0x80044a3c | 0x0c (12B) | 0x18 (24B) | POLY_G3 / POLY_FT3 |
| 14 | 0x80043b58 | 0x80044fdc | 0x14 (20B) | 0x1c (28B) | POLY_FT3 |
| 15 | 0x80043c6c | 0x80045194 | 0x18 (24B) | 0x24 (36B) | POLY_GT3 |
| 16 | 0x800438b8 | 0x80044c14 | 0x14 (20B) | 0x20 (32B) | POLY_G4 |
| 17 | 0x800439e4 | 0x80044dc8 | 0x18 (24B) | 0x28 (40B) | POLY_GT4 |
| 18 | 0x80043dd4 | 0x800453bc (b1) / 0x800457c4 (b2) | 0x1c (28B) | 0x28 / 0x20 | POLY_GT4 extended |
| 19 | 0x80043f10 | 0x80045584 (b1) / 0x80045988 (b2) | 0x24 (36B) | 0x34 / 0x28 | POLY_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:
| Kingdom | sstate | Cluster A total | Cluster B | Cluster A RAs observed |
|---|---|---|---|---|
| Drake | already on map01, held UP | 71,331 | 178 | 0x8001B47C, 0x8001BC8C, 0x801F78D4 |
| Sebucus | town → map02, held DOWN | 90,096 | 67 | 0x8001B47C, 0x801F78D4 |
| Karisto | town → map03, held DOWN | 13,593 | 115 | 0x8001B47C, 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 handler | Primitive (likely) | Drake | Sebucus | Karisto |
|---|---|---|---|---|
13 banks 1,2 (0x80044A3C) | POLY_G3 / POLY_FT3 triangle | 9,465 | 2,040 | 49 |
17 banks 1,2 (0x80044DC8) | POLY_GT4 textured quad | 762 | 240 | 147 |
18 bank 1 (0x800453BC, ×4 LW PCs) | POLY_GT4 extended quad | 13,561 (×4) | 20,601 (×4) | 1,820 (×4) |
16 banks 1,2 (0x80044C14) | POLY_G4 quad | 7,688 | 878 | 2,058 |
15 banks 1,2 (0x80045194) | POLY_GT3 textured triangle | 6,860 | 5,412 | 2,059 |
cluster B (0x80059DE4) | mid-body reader | 178 | 67 | 115 |
- 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:
| Tool | Role |
|---|---|
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 4 | Per-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.lua | PCSX-Redux closed-loop dumper: loads a save state, dumps the live slot-4 RAM region, quits. |
scripts/pcsx-redux/autorun_dump_full_ram.lua | Full 2 MiB main RAM dump. Use when the load base is unknown. |
scripts/pcsx-redux/diff_slot4_ram_vs_disc.py | Byte-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 chain | PC of write | Notes |
|---|---|---|
FUN_8001A55C (LZS decoder) | 0x8001A604 (sb v1, 0(s1) literal-byte write) | Dominant: 5-byte bursts at every probed offset |
| same | 0x8001A664 / 0x8001A668 / 0x8001A610 / 0x8001A5AC | Back-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:
| Offset | First-write PC | RA | Writer function | Role |
|---|---|---|---|---|
+0x7F8 (cluster A's vertex_base) | 0x80028710 / 0x8002871C | 0x8001B160 | FUN_80028158 (5580 B) | per-frame procedural mesh builder, called from FUN_8001ada4 case 4 |
+0x8E4 (cluster A's command_stream) | 0x800293C8 / 0x800296A0 | 0x8001B160 | same FUN_80028158 | per-frame procedural primitive-batch writer (same call) |
+0x6000 (deeper region) | 0x8001A8C8 (memcpy) | 0x8001E758 | FUN_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
| Function | Site | Role |
|---|---|---|
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 buffer | copies 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_8001E890 | writes entry[+0x8] = 10 for three consecutive table indices at DAT_8007B824 + 0..2 | per-pack count override (overwrites the installed TMD's group_count field) |
FUN_8001EBEC | reads 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 (0xD0D0D0default;0x40D0D0D0if object record's[+0x1E]flag is set; OR'd with0x10000000ifrecord[+0x12] & 0x800— interactive)arg2= fog cueclamp((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:
- Top-view dispatcher (
FUN_801F69D8): readsDAT_8007C018[(visible_object_kind8 + DAT_8007B6F8) * 4]per tile and passesentry + 0xCtoFUN_80043390. This is the warp-into-world-map render path the Read-bp probe captured. - Per-actor renderer (
FUN_8001ada4, caller RA0x8001B47C): walksactor+0x44 = [u32 count, u32 mesh_ptr[count]]and passes eachmesh_ptrtoFUN_80043390. The mesh pointers came fromactor+0x44, which is populated byFUN_80021B04/FUN_80024D78fromDAT_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):
| Field | Value |
|---|---|
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 range | Count | Content |
|---|---|---|
[0..4] | 5 | Character-mesh TMDs at 0x8014D554..0x801585C0, group_count 10/10/10/3/2. Disc source: § Disc-side source of [0..4] below. |
[5..142] | 138 | Kingdom-derived TMDs at 0x800F7908..0x80138D44 (group_count 1..10, mixed sizes) |
[143..255] | 113 | Either 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:
| Section | Type byte | Compressed size | File offset | Content |
|---|---|---|---|---|
| 0 | 0x01 | 0xB49C (46 236 B) | 0x20 | 5-TMD pack (LZS decodes to 65 536 B) |
| 1 | 0x02 | 0x41E0 (16 864 B) | 0x5037 | Secondary TMD payload |
| 2 | 0x03 | 0x1D524 (120 100 B) | 0x7055 | MAN-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 slot | Body offset | nobj (disc) | Body bytes (to next slot) |
|---|---|---|---|
| 0 | 0x0018 | 12 | 13 220 |
| 1 | 0x33BC | 12 | 13 800 |
| 2 | 0x69A4 | 12 | 11 656 |
| 3 | 0x972C | 3 | 6 488 |
| 4 | 0xB084 | 2 | 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 usingdisc_off = abs_ptr - (tmd_base + 0xC)): the full 6488-byte body matches byte-for-byte (0 differences over0x1958bytes 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'snobj=10vs disc'snobj=12is 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 2 → FUN_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:
| Field | Value |
|---|---|
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
- 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_8001E54Cthat turns slot-4 sub-bodies into TMD-shaped blobs in-place, or an asset-load-chain step that doesn't lower throughFUN_8001F05Cat all. A Read-bp probe watchingDAT_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.pyacross SCUS + every captured overlay dump):_DAT_8007B888(MOVE-buffer pointer set byFUN_8001F05Ccase 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 inoverlay_baka_fighter(the boxing minigame). Zero readers in anyoverlay_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_8007B888somewhere — 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 insideFUN_8002541Cor its descriptor-walker siblingFUN_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 in0x8007C190..0x8007C1E0vialui+addiu,lui+lw_with_offset, or positive-offsetlwfrom 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].
- 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 neither0x04000000nor0x20000000is ever set in retail world-map play; only banks0x00and0x50are exercised. So bodykind ∈ {1, 2, 4}is slot-4-internal data with no link to cluster-A bank dispatch. - 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-coveragecmd_flags-capture probe across multiple non-world-map game modes would pin which (if any) caller passes those flags. - 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 atDAT_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_8007B85Cas an asset-descriptor pack, callingFUN_8001F05C(buf + offset, size, type, 0)per record; case 2 ofFUN_8001F05CLZS-decodes the section, walks the[u32 count][u32 word_offsets[count]][TMD bodies]pack and dispatches each entry throughFUN_80026B4C(the TMD-pack installer). The corpus callers ofFUN_80026B4CincludeFUN_8001E890,FUN_8001E928,FUN_800520F0,FUN_800513F0,FUN_800542C8, and the muscle-dome minigame loader.
The outer producer that populates_DAT_8007B85Cwith PROT 0874's bytes is not pinned in the staticSCUS_942.54dumps:FUN_8001E890's retail-PROT branch targets PROT 876 (0x36c) viaFUN_8003eb98, but PROT 876 is a streaming-format file (VABpheader + TIM_LIST + SEQ) whose layout is shape-incompatible with the downstreamparse_player_lzs(buf, 3)walk - so either the branch is dead code in retail, or_DAT_8007B85Cis populated from PROT 874 elsewhere first.FUN_800520F0(battle scene loader) is the only static SCUS caller that issuesFUN_8003eb98(0x36A, ...), but it processes PROT 874 throughFUN_8001fbcc(VDF install) rather than the asset-descriptor walker. The dispatch most likely lives in an overlay-resident scene loader (theFUN_801D6704family) that populates_DAT_8007B85Cfrom PROT 874 before invokingFUN_80020224- a Write-bp probe onDAT_8007C018[0]during boot / first-battle init would isolate the exact site.
A further static narrowing: theFUN_8001F05Ccase-2 "freeze" sub-path (if (param_3 == 1) { _DAT_8007B704 = size; _DAT_8007B824 = pack_count; }) is the sole SCUSswwriter of_DAT_8007B824(at PC0x8001F2F8). The freeze sets the persistent-base index thatFUN_8001E1B4later reads to reset the install cursor (DAT_8007B774 = _DAT_8007B824), so a non-zero_DAT_8007B824would mark slots[0..pack_count-1]as carried across mode transitions. A corpus grep over every call site shows zero static SCUS callers ofFUN_8001F05Cpassparam_3 == 1(the three direct callers -FUN_80020224,FUN_8002541C, andoverlay_baka_fighter_801d4c50- passs6,0, and0respectively), and zero dumped overlay callers ofFUN_80020224passparam_1 == 1. So either the freeze path is in an overlay not yet captured, or_DAT_8007B824stays 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_8007B824writes to settle which case holds.