World Map
The overworld traversal mode. Two overlay variants share a code window at 0x801C0000..0x801EFFFF: the normal walk view and a debug top-down camera. The main controller is FUN_801E76D4 (9320 bytes); the per-entity tick is FUN_801DA51C (260 bytes). Sources: overlay_world_map.bin (walk-view) and overlay_world_map_top.bin (top-view debug) mednafen captures.
Overlay structure
Both variants load into the same address window. The top-view variant starts its first prologue ~0x1400 bytes earlier in the window to fit extra sprite-rendering code.
| Variant | First prologue | Loaded when |
|---|---|---|
Normal walk (overlay_world_map) | 0x801CFC40 | Standard world-map mode |
Top-view debug (overlay_world_map_top) | 0x801CE850 | Debug toggle combo (see below) |
Both variants share the core field VM (FUN_801DE840), the move-VM overlay extension (FUN_801D362C), and all rendering helpers. The view-mode toggle flag is at DAT_801F2B94. The world-map overlay variants extend past 0x801F0000 - capture them with a wider window (0x801C0000..0x801F9000, 228 KB) to include the prim-mode dispatch table at 0x801F8968 and its eight overlay-resident emit leaves at 0x801F7644..0x801F8690. The old 192 KB default clipped both; scripts/ghidra-analysis/extract-mednafen-overlay.py now defaults to the wider window.
World map controller - FUN_801E76D4
Entry: (ctx_ptr). Three top-level paths based on DAT_801F2B94:
- Debug top-view toggle - fires when
_DAT_8007B98C != 0(debug flag) AND pad mask_DAT_8007B850 == 0x4AAND held mask_DAT_8007B874 == 0x40. On trigger:DAT_801F2B94 ^= 1, captures current actor camera position into_DAT_801F35A8/AA/AC, clearsctx[+0x54]andctx[+0x50], callsFUN_80035C10. - Top-view camera controls (
DAT_801F2B94 != 0) - D-pad adjusts scroll globals; L/R buttons adjust azimuth and zoom:- Left/Right →
_DAT_80089120 ±= 8(X scroll) - Up/Down →
_DAT_80089118 ±= 8(Z scroll) - Circle/Square →
_DAT_8007B794 ±= 0x14(azimuth) - R1/L1 →
_DAT_8007B6F4 ±= 4(zoom/height)
- Left/Right →
- Normal-walk path (
DAT_801F2B94 == 0) - standard per-frame world-map update: field VM tick, actor step, camera follow via the motion VM.
World map entity tick - FUN_801DA51C
Entry: (entity_ptr). A 5-state dispatcher on entity[+0x8A] (jump table at 0x801CEC28). Called once per world-map entity per frame by the entity pool tick loop.
- State 0 (Idle) - when
_DAT_80083808 == 0: callsFUN_800243F0(BGM/asset resolver) to look up the scene associated with the entity's location; checks_DAT_8007BB38pad buttons for entity interaction. - States 1–4 - Activating, Transitioning, and Terminal states for encounter and location-entry sequences.
Encounter-record installation
The body at 0x801DA620..0x801DA678 populates the global encounter formation cell from a per-encounter record pointed at by entity[+0x94]:
- Clear the 4-slot formation array at
0x8007BD0C..0x8007BD0F(slots 3, 2, 1, then 0 - slot 0 is cleared in the delay slot ofJAL 0x801DE190). - Read
monster_count = entity[+0x94][+0x3]. - Copy
entity[+0x94][+0x4 .. +0x4 + monster_count]into the formation cell, byte-for-byte.
This copy runs in SM state 1 (entity[+0x8A] == 1). The same invocation clears entity[+0x94], advances entity[+0x8A] to 2, and then falls through the case 2/3 arm, which writes _DAT_8007B83C = 8 (the game-mode handoff that launches the battle), sets entity[+0x8A] = 4, and clears the 0x80000 "encounter active" flag on the player context (_DAT_8007C364[+0x10], which state 1 had raised). So the formation install and the battle launch happen in the same tick the carrier reaches state 1. State 0 reaches state 1 either via the random roll FUN_801D9E1C or - in a 0%-random town like town01 - via a scripted advance from the scene's interaction bytecode.
The carrier entity_ptr is a dedicated field entity, distinct from the player context _DAT_8007C364 (corpus-stable at 0x80083794, which carries no clean +0x8A/+0x94 SM): the routine reads the SM fields off its param_1 but writes the flag onto _DAT_8007C364 separately. The entity-update loop reaches the carrier through per-entity update-function-pointer dispatch (no direct jal), so it is one of the scene's MAN-placed entities, not the player.
The encounter-record format consumed here is documented on the encounter record page. The 4-byte formation cell at 0x8007BD0C is the input to the battle-scene loader (FUN_800520F0); the adjacent byte at 0x8007BD11 is a battle-data PROT-id selector that picks between PROT entries 0x367 and 0x36D.
The pointer at entity[+0x94] is set by field-VM op handlers inside the script-VM dispatcher (FUN_801DE840); see the script-VM page and the op-handler family at 0x801DEEDC / 0x801DEF08 / 0x801DEFA0 / 0x801DF038 / 0x801DF3FC / 0x801E1C38 / 0x801E1F44 / 0x801E21C0. Each handler is a different "trigger encounter on actor X" op; they all share the clause:
sw <record_ptr>, 0x94(<actor>)
sh $zero, 0x54(<actor>)
ori $tmp, $tmp, 0x400 ; raise "encounter armed" flag in actor[+0x10]
sw $tmp, 0x10(<actor>)
Render pipeline
The per-frame world-map render dispatches from the SCUS-resident game loop into the overlay-loaded code window. Two chains converge: a per-frame dispatch tick that reads the prim emitter's gate flag, and a one-shot arm path that sets the gate plus a small param block.
Per-frame dispatch (SCUS-resident)
Two SCUS-resident handlers from the 28-mode dispatch table at 0x8007078C reach the world-map render tick:
| Address | Mode-table role | Tick call |
|---|---|---|
FUN_80025EEC | Default per-mode handler (used by 13 of 28 modes - not world-map-specific). | FUN_8001698C → FUN_80016444(1) → FUN_80016B6C. |
FUN_80025F2C | Mode 13 (MAPDSIP MODE) - field/world-map display per-frame handler. | FUN_8001698C → func_0x801CE850 (overlay entry) → FUN_80016444(0). |
The a0 arg controls whether FUN_80016444 skips its early FUN_8005FB84 block (Mode 13 skips it; the default handler runs it). Both reach the world-map render branch deeper in the function, so the horizon emitter can fire from any of the 14 modes that route through FUN_80016444 whenever the submode register holds 2.
FUN_80016444 - SCUS world-map render tick (1352 bytes)
Entry: (submode_flag). Iterates the world-map render passes for one frame. The pipeline includes a gated direct call into the overlay-resident POLY_FT4 emitter:
80016750 lui v1, 0x8008
80016754 lw v1, -0x43c4(v1) ; v1 = _DAT_8007BC3C (submode register)
80016758 li v0, 0x2
8001675c bne v1, v0, 0x8001676c ; skip unless submode == 2
80016764 jal 0x801d7ea0 ; -> overlay-resident emitter
Same 0x8007BC3C register has six SCUS writers (FUN_80016230 - the two-write set/clear path - plus FUN_80025980, FUN_80025DA0, FUN_8001D424); the writer that stores 2 is the entry point for the world-map render branch.
FUN_801D7EA0 - world-map POLY_FT4 batch emitter (832 bytes)
Entry: (). One-shot emitter gated by _DAT_801F351C:
if (_DAT_801F351C != 0) {
_DAT_801F351C = 0; // self-clear gate
iVar11 = 4;
local_30 = 0x2C808080; // POLY_FT4 GP0 cmd + neutral grey
uVar6 = _DAT_801F3518
+ DAT_1F800393 * _DAT_801F3524; // angle += per-frame-tick * step
_DAT_801F3518 = uVar6;
local_3c = _DAT_801F3520;
local_34 = _DAT_801F3520 / 5;
local_38 = _DAT_801F3520 - local_34;
do {
iVar10 = cos_table[(uVar6 & 0xFFF)]; // 0x8007B81C cos LUT
// emit 2x POLY_FT4 (chain tag 0x9000000) + 1 small prim
// (chain tag 0x3000000); vertex coords are cos-rotation-
// projected with local_3c/local_38 as scale moduli.
...
uVar6 += 0x10;
iVar11++;
} while (iVar11 < 0xE4); // 224 iterations
}
_DAT_8007B81C is the cos lookup table (see the memory map). The function emits ~670 prims per call (2 POLY_FT4 + 1 small per iter across 224 iters). Vertex coordinates project via the cos table, so the rendered output rotates with the camera angle - consistent with a horizon / sky / animated-background plane, not a fixed continent mesh.
The case-5 path of the per-actor render dispatcher FUN_8001ADA4 draws every landmark TMD (castle, towers, bridges, gates) - each world-map actor's actor[+0x44] mesh chain points into Drake's 40-TMD landmark pack at PROT entry 0085 slot 1, which the dispatcher walks once per frame through FUN_8002735C (the 60-GTE Legaia TMD renderer). That accounts for the landmark prims in the GPU pool.
Top-view bulk-terrain render path (overlay-replaced per-prim renderers)
This is the top-view / overview render path (game mode 0x0D), distinct from the walk-view continent (a heightfield, see the engine port). The top-view's bulk terrain prims are not produced by a procedural emitter sibling of FUN_801D7EA0; they come out of ordinary case-5 TMD rendering (of the overview-pool meshes placed per cell) whose per-prim dispatch is mode-switched to overlay-resident renderers when the top-view overlay is paged in. FUN_80043390 (the SCUS-side per-prim TMD renderer at the leaf of the actor-mesh-chain walk) selects one of two function-pointer tables based on _DAT_1F800394 & 1:
| Flag | Table base | Rows | Where it lives |
|---|---|---|---|
| clear | 0x8007657C | 4 (alpha 0/50/A0/F0) | SCUS_942.54 |
| set | 0x801F8968 | 1 (alpha 0 only) | world-map overlay |
The overlay path skips the alpha offset (_DAT_1F800028 is not added on the overlay branch), so only the first row of the overlay table is meaningful. Slots 8..11 of row 0 share the same low-mode dispatchers as SCUS (0x8004409C, 0x8004423C, 0x80044434, 0x800445B0); slots 12..19 carry the eight overlay-resident high-mode renderers at 0x801F7644, 0x801F7838, 0x801F7F78, 0x801F8198, 0x801F7AA4, 0x801F7CCC, 0x801F8454, 0x801F8690. Each is a per-primitive emitter that loads vertex indices from the TMD prim body, looks vertices up in the actor's vertex pool, runs them through the GTE, and emits one GPU prim packet.
Static addprim hunters never surfaced these because (a) the cmd byte is loaded from a per-mode descriptor table rather than as a lui/li immediate, and (b) the captured overlay window stopped at 0x801F0000 - which clipped every leaf address. The dynamic prim-pool-writers probe confirms top-of-list PC hits at 0x801F7344..0x801F8DBC, exactly the eight high-mode renderer addresses.
Source mesh data is the same kingdom slot-1 TMD pack the landmarks draw from. mednafen-state prim-dispatch-table <save> decodes both tables out of a save state's main RAM and surfaces the eight overlay-resident targets; pass --overlay-targets-only to pipe addresses into a Ghidra dump_funcs.py TARGETS list. The companion subcommand mednafen-state prim-dispatch-survey <save>... compares multiple saves side-by-side and asserts the SCUS table is byte-identical across them.
Per-slot delta vs SCUS sibling
Capstone disassembly (scripts/ghidra-analysis/disasm-overlay-fn.py --batch leaves) plus Ghidra decompilation (ghidra/scripts/dump_world_map_top_prim_leaves.py) confirms every overlay leaf is the SCUS sibling body plus a per-vertex distance-cue fog post-process inserted between the GTE projection and the OT packet write. The fog block is gated on (u8 *)(t2-0x2D1) & 0x10; when set, it computes a clamped max-Z, shifts it by (u8 *)(t2+0x90), mixes a far-Z reference at t2-0x2E0 into the prim cmd, invokes GTE.dpcs (single colour, slots 12-14 / 16-18) or GTE.dpct + dpcs (textured quads, slots 15 / 19), then indexes the per-Z RGB LUT at t2-0x2BC three times to ADD-write fog tints into offsets 8 / 0xC / 0x10 of the OT packet.
| Slot | SCUS sibling | Overlay leaf | Overlay-only GTE ops |
|---|---|---|---|
| 12 | 0x80043658 (68 instr) | 0x801F7644 (125 instr) | dpcs |
| 13 | 0x80043768 (84 instr) | 0x801F7838 (155 instr) | dpcs |
| 14 | 0x80043B58 (69 instr) | 0x801F7F78 (136 instr) | dpcs |
| 15 | 0x80043C6C (90 instr) | 0x801F8198 (175 instr) | dpct + dpcs |
| 16 | 0x800438B8 (75 instr) | 0x801F7AA4 (138 instr) | dpcs |
| 17 | 0x800439E4 (93 instr) | 0x801F7CCC (171 instr) | dpcs |
| 18 | 0x80043DD4 (79 instr) | 0x801F8454 (143 instr) | dpcs |
| 19 | 0x80043F10 (99 instr) | 0x801F8690 (182 instr) | dpct + dpcs |
Slots 15 and 19 are the textured-quad (POLY_FT4 / POLY_GT4) variants that need dpct for the first three vertices plus a final dpcs for the fourth; every other slot needs only dpcs. Each leaf ends with j 0x80043580; addiu $a1, $a1, 0xc - tail call to the SCUS dispatcher continuation plus the loop's per-prim source-pointer advance. The engine port mirrors this dispatch in legaia-engine-vm::prim_dispatch; SceneLoadKind::WorldMap selects the overlay variant.
Slot 4 of each kingdom bundle is not the bulk-terrain source. Its records are something else (a runtime library of object-local 3D meshes, see slot-4 records format doc) - that hunt is independent of the continent terrain emit mechanism.
The horizon emitter is called by direct jal from SCUS - it does not need function-pointer dispatch. Ghidra's reference manager misses the cross-program call when sweeping the overlay alone; sweep SCUS to surface the caller.
FUN_80016444 jal-target audit (background)
Audit of FUN_80016444's direct jal targets finds 12 unique targets across 60 call sites. None is a bulk-terrain emitter on its own - that conclusion lines up with the dispatch-table mechanism documented above (the bulk prims come from many small case-5 TMD renders going through the overlay-replaced per-mode renderers, not from a single emit function called from this site):
| Target | Calls | Role |
|---|---|---|
0x8001a068 | 19 | Actor-list pass dispatcher (8 of the 19 hit at the function head). |
0x8005fb84 | 18 | Early-return block; skipped when submode == 2. |
0x8002519c | 6 | Per-frame render-pass iterator (5 lists per frame). |
0x8001d140 | 6 | Stack-swap wrapper → FUN_8001ADA4 (per-actor render dispatcher). |
0x800172c0 | 1 | GTE matrix setup helper (FUN_80026988(&local_18, 0x1F8003A8)). |
0x800179c0 | 1 | Small helper. |
0x800188c8 | 1 | Debug-HUD text renderer (PSX_TEST_PROGRAM string). |
0x8001d058 | 2 | 48-byte scratchpad / GPU register flush. |
0x8002b688/790 | 2 / 2 | Tiny accessors (60-80 bytes each). |
0x80046978 | 1 | Screen-tint fade emitter (gated on gp+0x9d4). |
0x801d7ea0 | 1 | Horizon emitter (overlay-resident, single direct call). |
None of these dispatch into a function whose body contains a bulk POLY_FT4 loop. Static addprim hunters find only the horizon emitter inside any world_map overlay variant; SCUS-side addprim candidates are five non-terrain emitters (HUD sprite batches, screen-tint, digit batchers).
Both negatives are explained by the descriptor-table-driven dispatch documented in the section above: the world-map top-view's bulk terrain prims come out of FUN_80043390 → overlay-resident high-mode renderer where the cmd byte is loaded from a per-mode descriptor table, not built with lui/li immediates. That makes the leaves invisible to addprim hunters, and the overlay extraction window stopped at 0x801F0000 so the leaves weren't even in the captured binary.
Per-frame render-pass iterator - FUN_8002519c
Five times per frame, FUN_80016444 invokes the SCUS-resident actor-list iterator FUN_8002519c (328 bytes) against five linked-list heads at _DAT_8007C34C..._DAT_8007C36C. Each list is one render pass. The iterator walks the chain and per node either takes an early-return path (when bit 0x8 of node[+0x10] is set) or calls the tick function at node[+0xC] via jalr.
Per-actor record layout consumed by the iterator:
| Offset | Type | Role |
|---|---|---|
+0x00 | actor * | Next pointer (singly linked list, NULL terminates). |
+0x0C | void (*)(actor *) | Tick function (the entry point jalr calls). |
+0x10 | u32 | Flags; bit 0x8 selects the early-return path, bit 0x200 is the "already-emitted this frame" guard. |
+0x14 | u32 | Saved next-pc copy used by the early-return path. |
+0x18 | u16 | Halfword count exposed at +0x20 for the early-return path. |
+0x44 | chain * | Optional prim-chain head; freed via FUN_80017b94 when bit 0x800 is set. |
+0x48 | u8 * | Move-VM bytecode base (for actors whose tick is FUN_80021df4). |
+0x70 | u16 | Move-VM PC in halfword units; the actual byte offset is 2 * actor[+0x70]. |
Standard tick functions observed in the world-map render passes:
| Tick function | Where | Role |
|---|---|---|
FUN_80021DF4 (SCUS) | per-frame actor tick | Steps the move VM via FUN_80023070(actor). The eight actors in list _DAT_8007C350 use this tick. |
FUN_8003BC08 (SCUS) | per-actor tick | Calls the motion VM (FUN_8003774C), move-buffer setup (FUN_800204F8), and overlay helper FUN_801D79E8. The fourteen actors in list _DAT_8007C354 use this tick. |
FUN_801E76D4 (world_map overlay) | world-map controller | Top-view debug toggle + camera scroll/azimuth/zoom + dev-menu render. |
FUN_801DA51C (world_map overlay) | per-entity tick | 5-state SM on entity[+0x8A]. |
FUN_801D1344 (world_map overlay) | horizon gate-arm wrapper | See the gate-arm chain below. |
Per-actor render dispatcher - FUN_8001ADA4
In addition to the five TICK calls into FUN_8002519c, the same frame issues six RENDER calls into the stack-swap wrapper FUN_8001D140, which forwards into the per-actor render dispatcher FUN_8001ADA4 (2456 bytes). The render dispatcher walks the same actor lists but runs a different switch - on actor[+0x56] (render mode 1..0xB):
- case 4 (multi-target). Dispatches on
actor[+0x9e]flags: bit0x4000→FUN_8002A5A4(SCUS); bit0x2000→FUN_801CFA48(overlay-resident); else →FUN_80028158(SCUS - distinct from the 6692-byte motion bytecode VMFUN_80038158). - case 5 (full TMD). Iterates the mesh chain at
actor[+0x44](puVar5[0]= count,puVar5[1..n]= mesh pointers) and per entry callsFUN_80043390(textured TMD),FUN_80029888(environment-mapped whenactor[+0x7a] != 0), orFUN_8002735C(60-GTE Legaia TMD renderer for bone-animated meshes - the landmark emit leaf: each landmark TMD in Drake's 40-mesh kingdom pack passes through here). - cases 1, 2, 3, 6, 7, 8, B - distance-LOD / particle / sprite-billboard branches calling per-effect helpers.
Static addprim hunters do not surface FUN_8002735C as a POLY_FT4 emitter because the cmd byte is read from the per-mode descriptor table at DAT_8007326C, not built with lui/li immediates. That is why the landmark TMD emitter eluded static analysis: the addprim scan flags every direct emitter (the horizon, the HUD sprite batch FUN_8002C69C, the screen-tint, etc.) but skips the TMD renderer where the landmark prims actually originate. The top-view's bulk terrain follows the same dispatch-table pattern - via FUN_80043390's overlay-mode jump table at 0x801F8968 and its eight overlay-resident high-mode renderers (the walk-view continent ground is instead a heightfield, see the engine port).
Gate-arm chain - FUN_801D1344 → FUN_801D8258
The one-shot gate _DAT_801F351C is armed by a 40-byte trigger function called from a 1332-byte parameter-prep wrapper:
| Address | Role |
|---|---|
FUN_801D1344 | World-map gate-arm wrapper. 1332 bytes; function-pointer-only entry (Ghidra incoming=0). Reads three globals at _DAT_8007BCD0/_D4/_D8 and forwards them to FUN_801D8258 as the scale / step / OT-layer params at PC 0x801D1470: jal 0x801D8258. Same RAM address holds a different function when the dialog overlay is paged in - that variant is the actor frame handler (see the functions reference). |
FUN_801D8258 | 40-byte gate setter. Writes _DAT_801F351C = 1, then _DAT_801F3520 = param_2, _DAT_801F3524 = param_3, _DAT_801F3528 = param_4 - the inputs the emitter consumes on its next run. |
FUN_801C2B2C | Code-identical relocation copy of FUN_801D1344 in the 0897 field overlay. Same body, different load address; calls jal 0x801D8258 at PC 0x801C2C58. Active during field-mode entry transitions. |
The gate flag _DAT_801F351C is in the persistent 0x801F0000+ region, so it survives overlay swaps. The flag is shared - both the world-map overlay's FUN_801D7EA0 and the 0897 field overlay's FUN_801C9688 read + clear it.
Engine port
Two modules in the clean-room port cover this subsystem:
engine-vm::world_map- clean-room port ofFUN_801DA51C:EntityStateenum (Idle / Activating / Transitioning / Terminal),WorldMapEntityHosttrait (encounter / interact / scene-transition callbacks),step(entity_idx, ctx, host).Worlddrives oneWorldMapEntityCtxper installed overworld entity eachSceneMode::WorldMaptick: the Idle-state encounter latches the configured formation, which the world resolves into a battle through the sameformation_tablemachinery as a field encounter - tagged viabattle_return_modeto return to the overworld - while interactions surface as aFieldInteractevent. Each entity carries an optional per-entity role (WorldMapEntityConfig): anEncounterZonespawns its own formation, aPortalsurfaces aWorldMapTransitionwith its target map when engaged, and anNpccarries its interaction id plus the inline dialog-text bytes from its placement record's structural0x1F-lead segment pool (the actual message - not from a0x3Fop, which is the named scene-change).- Overworld player movement + region encounters -
tick_world_mapwalks the player actor from the held d-pad (step_world_map_locomotion) and, on each 128-unit tile crossing (live_world_map_tick, mirroring the fieldlive_field_tick), rolls the scene's region-keyed encounter table (set_world_map_regions, the clean-room port ofFUN_801D9E1C): the player's tile selects the first region whose AABB contains it, the region's rate depletes a step counter, and a<= 0counter rolls a formation from the region's slice and flipsWorldMap → Battle(returning to the overworld). A camera-only world map (no region table) is unchanged. In walk mode the nativeplay-windowcamera follows the player; the top-view debug camera keeps the free scroll. - Collision + camera-relative movement - the walk overlay's locomotion is byte-for-byte the field
FUN_801d01b0+FUN_801cfe4cagainst the same_DAT_1f8003ec + 0x4000walkability grid (the kingdom maps carry thousands of wall sub-cells there), stepped through the sharedWorld::advance_with_collision.World::world_map_camera_relative_bitsrotates the held d-pad through the camera azimuth so "screen up" walks toward the top of the screen for any framing (the retailfunc_0x800467e8remap). The kingdom pack is drawn Y-flipped (PSX Y-down → renderer Y-up); the world→screen axes come from the realworld_map_camera_mvpmatrix and are pinned by a disc-free projection test. - Geometry: the kingdom landmark pack (slot 1) - a
map\d\dscene load decodes the kingdom bundle's slot-1 Legaia-TMD pack (Drake 40 / Sebacus 36 / Karisto 56 meshes) and slot-0 TIM atlas straight from the 7-asset descriptor table (SceneLoadKind::WorldMap); only the scene's primary kingdom entry contributes. TheDAT_8007C018pool the tile dispatcher reads is filled by one descriptor-walk (FUN_801D6704→FUN_80020118party meshes [0..4] +FUN_80020224, dispatching viaFUN_8001f05c; only cases0x02/0x09install). A realmap01walk capture settles it to 45 entries: 5 party meshes + the 40-mesh slot-1 landmark pack (prefix=5) - so the walk pool is the landmark pack. Parsed live, the 40 meshes are small object-local tile/prop meshes (dx/dz ≤ ~768): the landmark layer (trees, mountains, the castle), not the continent ground. (Walk-view and overview pools are mutually exclusive - 0085's and 0093's slot-0 atlases target the same VRAM pages.) - The continent ground is a procedural heightfield, not instanced meshes - confirmed by
FUN_80019278(SCUS, always-resident, no overlay aliasing), the bilinear ground-height sampler: it gates on the object-grid0x1000cell bit and interpolates the floor height from the 2×2 block of+0x4000nibbles (each& 0xf→ the0x1f80035cLUT, weighted by sub-tile position). So+0x4000is terrain elevation and the0x1000continent is a smooth heightfield surface; the.MAP+0x10field feeds only the sparse placed-landmark layer (FUN_8003A55C,flags & 0x4;+0x10 == 0for bulk ground cells). The only per-cell terrain emitter isFUN_801F69D8(the top-view overview renderer, gate0x2000), which draws per-cell meshes via+0x10throughFUN_80043390-FUN_80019278(the height math) is the reliable anchor for the walk-view heightfield geometry. - Walk
.MAPsource (pinned) - the field-file loaderFUN_8001f7c0is dual-mode; on retail (_DAT_8007b868 == 0 && _DAT_8007b8c2 != 0) it resolves the.MAPby PROT entry index viaFUN_8003e8a8(indexing the in-RAM PROT TOC at0x801c70f0), the index read from the global at0x80084540. A live Drake walk reads0x80084540 = 85, so the records+grid is the raw0x10000region at PROT.DAT0x655800(=toc[87], no compression; 99.7% byte-identical to the live buffer). The per-entry extractor mis-slices it (its0085_map01.BINcount=46 pack at0x668000is the field object/script pack; the real.MAPis under the overlapping manifest entry 83). Parsing the raw region with the walk rules (0x1000gate,pool = record[+0x10] + prefix) reproduces the live placements exactly. - Entities + boot-path seeding - the overworld shares game mode 0x03 with towns, so the engine classifies it by scene (
is_world_map_scene); a field-VMscene_transitionresolving to amapNNlabel routesSceneHost::tickthroughenter_world_map_scene(region table + world-map mode). On-map entities are the MAN partition-1 records (FUN_8003A1E4, decoded byManFile::actor_placements/Scene::field_actor_placements): each is[u8 N][N×2 locals][model][action_count][tile_x][tile_z][script], giving spawn position, model index, and a per-entity field-VM script.classify_placementsreads the kind off the script: a genuine warp (the base0x3Ewithop0in100..=106) → Portal (target map idop0 - 100, i.e. one of the 7 door-warp scene-type overlays); an inline0x1Fdialog-text block (structural, not an opcode) / interact (0x3E,op0 < 100) → NPC; otherwise Plain. The walk is over-approximating and desyncs inside embedded message / SJIS text, so a genuine-warp gate (!extended && op0 in 100..=106) rejects phantoms - e.g.geremi(op0=200) and the leftover-JPother7(op0=175/179) used to mis-classify as portals to non-existent maps. Across the PROT corpus exactly 11 genuine portals survive (town01: 14 NPCs;koin1maps 3/4/5,koin3map 6,baldenmap 3, plus one overworld fishing-spot warp each on map02/map03).enter_world_map_sceneinstalls the typed Portal/NPC entities with their spawn positions from disc. - Rendering - the continent ground renders as a heightfield surface:
Scene::walk_heightfield→build_walk_heightfieldsweeps the0x1000cells and emits one quad per cell, each corner's Y from the+0x4000floor-nibble grid via the floor LUT (theFUN_80019278math), verified vs the real disc (map01/02/03 build >10k-quad heightfields with genuine elevation).play-windowdraws it as the ground with the placed landmarks (Scene::walk_object_placements, theflags & 0x4slot-1 pack meshes viarecord[+0x10]+prefix) on top. Placed entities draw as kind-coded upright markers (World::world_map_entity_markers; portals cyan, NPCs green, encounter zones red), the player as a white-yellow marker with a facing tick (World::world_map_player_marker;step_world_map_locomotionrecords the heading into the actor'srender_26). Slot-4 vertex-pool inspection overlay: the per-kingdom object-mesh library (kingdom-bundle slot 4) decodes ontoSceneResources::world_map_slot4for everySceneLoadKind::WorldMapscene (and only those); withLEGAIA_WORLDMAP_SLOT4=1,play-windowbuilds a colour-by-kindLineListfromworld_map_overlay::wireframe_segments_3dand merges it into the world-map overlay-lines buffer, so the decoded pool is visible in the live 3D view. It is an inspection overlay, not faithful world geometry - raw object-local coordinates + group-polyline topology, because the per-object placement transform and true triangle topology live in the unpinned cluster-A command stream. Off by default. Ground texturing - per-cell multi-page atlas pinned + shipped: the walk-view ground is per-cellPOLY_FT4(cmd0x2C) quads, one32×32quad per visible cell, emitted in a row-major world-cell sweep. The texture is selected per cell from a terrain-type-keyed multi-page atlas - grass, mountain, water, and forest cells each sample a different VRAM page - via the cell's object-record+0x14..+0x18run:+0x14=8×8atlas tile index (u=(id%8)×32,v=(id/8)×32),+0x15= PSXtpage(the terrain page/type:0x1Agrass,0x0Cmountain,0x1B/0x1Cwater,0x0Bforest),+0x16..+0x18= PSXclutword. Verified by aligning each quad run's UV→tile sequence to the.MAP's+0x14grid (scripts/ghidra-analysis/analyze-walk-ground-tiles.py --verify-rule): tile/page/clut match the record 100% across mountain + coast captures.build_walk_heightfieldbakes the per-cell UV +[clut,tpage](WalkHeightfield::uvs/::cba_tsb) so one ground mesh samples the right page per cell. Ocean animation - shipped: the water tile's CLUT row at fb(0, 506)(CBA0x7E80) cycles 13 precomputed BGR555 frames for the rolling-wave shimmer; the 13-frame table is the shared global asset across kingdoms while the tile texture + base CLUT are per-kingdom (legaia_asset::ocean). The ocean texture + base CLUT already reach VRAM via the slot-0 TIM pass and ~39% of map01's heightfield verts carry CBA0x7E80, soplay-windowanimates the sea by writing each frame's 16 entries into the CPU VRAM CLUT row and re-uploading (advance_ocean_animation; the retail DMA cadence isn't pinned, so the frame interval is approximate). Capture diffs (map01 overworld vs field-menu states) show the cycling reaches beyond the row-506 head: rows 508/509 each animate a few entries in place (shoreline shimmer inside the mountain/forest terrain palettes), row 508's entries 32..47 mirror the live frame of its own 0..15 head (a map01 script behaviour - on Sebucus / Karisto the cell holds a different strip frame), and row 506's tail (entries ~40..47) holds a runtime-generated pure-channel palette present in no disc bundle. The writer is located: the cells are rewritten by the field overlay's script-driven CLUT-cell effects -FUN_801E4C58(field-VM0x4Cn6 sub-0x61, a one-shot 16×1MoveImagecell copy / flat-colour fill whose coordinates are script operands) andFUN_801E4794(a multi-frame CLUT cross-fade state machine) - sourcing 13-frame palette strips parked at VRAM rows 498/501..505; the lockstep phase coupling is sibling ops sharing the frame counter, not one wider rect. The VRAM parity oracle excludes exactly the censused columns for world-map scenes (vram_oracle::WORLD_MAP_CLUT_CYCLE_CELLS, including the(48, 500)sibling destination cell). The destination-cell set is kingdom-universal: on the resident Sebucus / Karisto captures every censused destination cell holds a 16-px-aligned window of that state's own strip park rows (world_map_ocean_clut_live.rs), so the copy family runs against per-kingdom strips with kingdom-invariant destination operands. Correction: the earlier “single0x1Agrass page, positional(col%3,row%3),+0x14unused” reading was a misread - grass cells use page0x1Awith+0x14in the atlas's top-left3×3block, so the mod-3 sequence was coincidental;+0x14IS the tile selector. Distinct from the top-view bulk continent (per-cell meshes viaFUN_80043390/ MAN0x7F-sentinel). - Portals + NPC talk-to -
auto_engage_world_map_portalsruns each tick (after locomotion, before the SM step) and drives anyPortalwhose placement tile matches the player's to its transition state, surfacing aWorldMapTransitionto its target map (Idle portals only, once per visit). NPCs are talk-to:tick_world_mapopens anNpc's inline dialogue (setsWorld::current_dialog+ emitsOpenDialog) on a confirm press within one tile, dismissed on the next press. Dialogue text is inline in the record's structural0x1F-lead segment pool (found byfirst_inline_dialog_offset, not by a dialog opcode), not the scene MES.OwnedDialogPanel::from_inline_dialogdecodes the first0x1Fsegment andplay-windowrenders the box (geometry-header layout + multi-segment menu rendering not yet pinned). Not the0x3Fop: that is the named scene-change (it carries a destination scene name and calls the scene-change packetFUN_8001FD44), and the overworld's reachable towns/dungeons are listed as a table of0x3Fops in the controller script -man_field_scripts::scene_destinationsrecovers them (map01 →town01/0b/0c,dolk,rikuroa,cave01,vell,vozz, …, all real CDNAME scenes). engine-core::world_map::WorldMapController- models the camera globals (scroll_x,scroll_z,azimuth,zoom) and thetop_viewdebug toggle flag; exposestick(pad_input)matchingFUN_801E76D4's control logic. Attached toWorldasworld_map_ctrl: Option<WorldMapController>and ticked whenSceneMode::WorldMapis active.
The legaia-engine play-window --world-map flag activates WorldMap mode and shows camera state (scroll, azimuth, zoom) in the HUD overlay.
World-overview viewer
The /world-overview/ page in the static site renders each kingdom's landmark layer in real-time WebGL 3D from a disc image. The viewer-side reverse engineering (slot-1 layout heuristics, distance-cue fog approximation, sentinel placement resolver, ocean asset extraction, camera anchors) lives on its own page: world-overview viewer subsystem.
Full reference
The complete function-level reference with Ghidra provenance lives at docs/subsystems/world-map.md in the repo. Function dumps: ghidra/scripts/funcs/overlay_dialog_801e76d4.txt, overlay_dialog_801ead98.txt, and 801cfc40.txt.