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.

VariantFirst prologueLoaded when
Normal walk (overlay_world_map)0x801CFC40Standard world-map mode
Top-view debug (overlay_world_map_top)0x801CE850Debug 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 == 0x4A AND held mask _DAT_8007B874 == 0x40. On trigger: DAT_801F2B94 ^= 1, captures current actor camera position into _DAT_801F35A8/AA/AC, clears ctx[+0x54] and ctx[+0x50], calls FUN_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)
  • 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: calls FUN_800243F0 (BGM/asset resolver) to look up the scene associated with the entity's location; checks _DAT_8007BB38 pad 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]:

  1. Clear the 4-slot formation array at 0x8007BD0C..0x8007BD0F (slots 3, 2, 1, then 0 - slot 0 is cleared in the delay slot of JAL 0x801DE190).
  2. Read monster_count = entity[+0x94][+0x3].
  3. 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>)

Debug menu renderer - FUN_801EAD98

Entry: (ctx_ptr, x, y, scroll_idx, max_visible). Renders a vertically scrolling menu list for the world map developer menu. String table at 0x801CF344; at least 24 entries (bounds check local_40 > 0x17). Known entries: MAP_CHANGE, CARD_OPTION (both show as CLOSED when _DAT_8007B868 != 0), PLAYER_STATUS, CAMERA, ENCOUNT, OTHER_SETTINGS, BGM_CALL, DEBUG.

Called by FUN_801ECA08 when the debug panel is active (ctx[+0x54] mod-6 dispatch, cases 1 or 3).

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:

AddressMode-table roleTick call
FUN_80025EECDefault per-mode handler (used by 13 of 28 modes - not world-map-specific).FUN_8001698CFUN_80016444(1)FUN_80016B6C.
FUN_80025F2CMode 13 (MAPDSIP MODE) - field/world-map display per-frame handler.FUN_8001698Cfunc_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:

FlagTable baseRowsWhere it lives
clear0x8007657C4 (alpha 0/50/A0/F0)SCUS_942.54
set0x801F89681 (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.

SlotSCUS siblingOverlay leafOverlay-only GTE ops
120x80043658 (68 instr)0x801F7644 (125 instr)dpcs
130x80043768 (84 instr)0x801F7838 (155 instr)dpcs
140x80043B58 (69 instr)0x801F7F78 (136 instr)dpcs
150x80043C6C (90 instr)0x801F8198 (175 instr)dpct + dpcs
160x800438B8 (75 instr)0x801F7AA4 (138 instr)dpcs
170x800439E4 (93 instr)0x801F7CCC (171 instr)dpcs
180x80043DD4 (79 instr)0x801F8454 (143 instr)dpcs
190x80043F10 (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):

TargetCallsRole
0x8001a06819Actor-list pass dispatcher (8 of the 19 hit at the function head).
0x8005fb8418Early-return block; skipped when submode == 2.
0x8002519c6Per-frame render-pass iterator (5 lists per frame).
0x8001d1406Stack-swap wrapper → FUN_8001ADA4 (per-actor render dispatcher).
0x800172c01GTE matrix setup helper (FUN_80026988(&local_18, 0x1F8003A8)).
0x800179c01Small helper.
0x800188c81Debug-HUD text renderer (PSX_TEST_PROGRAM string).
0x8001d058248-byte scratchpad / GPU register flush.
0x8002b688/7902 / 2Tiny accessors (60-80 bytes each).
0x800469781Screen-tint fade emitter (gated on gp+0x9d4).
0x801d7ea01Horizon 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:

OffsetTypeRole
+0x00actor *Next pointer (singly linked list, NULL terminates).
+0x0Cvoid (*)(actor *)Tick function (the entry point jalr calls).
+0x10u32Flags; bit 0x8 selects the early-return path, bit 0x200 is the "already-emitted this frame" guard.
+0x14u32Saved next-pc copy used by the early-return path.
+0x18u16Halfword count exposed at +0x20 for the early-return path.
+0x44chain *Optional prim-chain head; freed via FUN_80017b94 when bit 0x800 is set.
+0x48u8 *Move-VM bytecode base (for actors whose tick is FUN_80021df4).
+0x70u16Move-VM PC in halfword units; the actual byte offset is 2 * actor[+0x70].

Standard tick functions observed in the world-map render passes:

Tick functionWhereRole
FUN_80021DF4 (SCUS)per-frame actor tickSteps the move VM via FUN_80023070(actor). The eight actors in list _DAT_8007C350 use this tick.
FUN_8003BC08 (SCUS)per-actor tickCalls 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 controllerTop-view debug toggle + camera scroll/azimuth/zoom + dev-menu render.
FUN_801DA51C (world_map overlay)per-entity tick5-state SM on entity[+0x8A].
FUN_801D1344 (world_map overlay)horizon gate-arm wrapperSee 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: bit 0x4000FUN_8002A5A4 (SCUS); bit 0x2000FUN_801CFA48 (overlay-resident); else → FUN_80028158 (SCUS - distinct from the 6692-byte motion bytecode VM FUN_80038158).
  • case 5 (full TMD). Iterates the mesh chain at actor[+0x44] (puVar5[0] = count, puVar5[1..n] = mesh pointers) and per entry calls FUN_80043390 (textured TMD), FUN_80029888 (environment-mapped when actor[+0x7a] != 0), or FUN_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_801D1344FUN_801D8258

The one-shot gate _DAT_801F351C is armed by a 40-byte trigger function called from a 1332-byte parameter-prep wrapper:

AddressRole
FUN_801D1344World-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_801D825840-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_801C2B2CCode-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 of FUN_801DA51C: EntityState enum (Idle / Activating / Transitioning / Terminal), WorldMapEntityHost trait (encounter / interact / scene-transition callbacks), step(entity_idx, ctx, host). World drives one WorldMapEntityCtx per installed overworld entity each SceneMode::WorldMap tick: the Idle-state encounter latches the configured formation, which the world resolves into a battle through the same formation_table machinery as a field encounter - tagged via battle_return_mode to return to the overworld - while interactions surface as a FieldInteract event. Each entity carries an optional per-entity role (WorldMapEntityConfig): an EncounterZone spawns its own formation, a Portal surfaces a WorldMapTransition with its target map when engaged, and an Npc carries its interaction id plus the inline dialog-text bytes from its placement record's structural 0x1F-lead segment pool (the actual message - not from a 0x3F op, which is the named scene-change).
  • Overworld player movement + region encounters - tick_world_map walks 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 field live_field_tick), rolls the scene's region-keyed encounter table (set_world_map_regions, the clean-room port of FUN_801D9E1C): the player's tile selects the first region whose AABB contains it, the region's rate depletes a step counter, and a <= 0 counter rolls a formation from the region's slice and flips WorldMap → Battle (returning to the overworld). A camera-only world map (no region table) is unchanged. In walk mode the native play-window camera 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_801cfe4c against the same _DAT_1f8003ec + 0x4000 walkability grid (the kingdom maps carry thousands of wall sub-cells there), stepped through the shared World::advance_with_collision. World::world_map_camera_relative_bits rotates the held d-pad through the camera azimuth so "screen up" walks toward the top of the screen for any framing (the retail func_0x800467e8 remap). The kingdom pack is drawn Y-flipped (PSX Y-down → renderer Y-up); the world→screen axes come from the real world_map_camera_mvp matrix and are pinned by a disc-free projection test.
  • Geometry: the kingdom landmark pack (slot 1) - a map\d\d scene 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. The DAT_8007C018 pool the tile dispatcher reads is filled by one descriptor-walk (FUN_801D6704FUN_80020118 party meshes [0..4] + FUN_80020224, dispatching via FUN_8001f05c; only cases 0x02/0x09 install). A real map01 walk 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-grid 0x1000 cell bit and interpolates the floor height from the 2×2 block of +0x4000 nibbles (each & 0xf → the 0x1f80035c LUT, weighted by sub-tile position). So +0x4000 is terrain elevation and the 0x1000 continent is a smooth heightfield surface; the .MAP +0x10 field feeds only the sparse placed-landmark layer (FUN_8003A55C, flags & 0x4; +0x10 == 0 for bulk ground cells). The only per-cell terrain emitter is FUN_801F69D8 (the top-view overview renderer, gate 0x2000), which draws per-cell meshes via +0x10 through FUN_80043390 - FUN_80019278 (the height math) is the reliable anchor for the walk-view heightfield geometry.
  • Walk .MAP source (pinned) - the field-file loader FUN_8001f7c0 is dual-mode; on retail (_DAT_8007b868 == 0 && _DAT_8007b8c2 != 0) it resolves the .MAP by PROT entry index via FUN_8003e8a8 (indexing the in-RAM PROT TOC at 0x801c70f0), the index read from the global at 0x80084540. A live Drake walk reads 0x80084540 = 85, so the records+grid is the raw 0x10000 region at PROT.DAT 0x655800 (= toc[87], no compression; 99.7% byte-identical to the live buffer). The per-entry extractor mis-slices it (its 0085_map01.BIN count=46 pack at 0x668000 is the field object/script pack; the real .MAP is under the overlapping manifest entry 83). Parsing the raw region with the walk rules (0x1000 gate, 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-VM scene_transition resolving to a mapNN label routes SceneHost::tick through enter_world_map_scene (region table + world-map mode). On-map entities are the MAN partition-1 records (FUN_8003A1E4, decoded by ManFile::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_placements reads the kind off the script: a genuine warp (the base 0x3E with op0 in 100..=106) → Portal (target map id op0 - 100, i.e. one of the 7 door-warp scene-type overlays); an inline 0x1F dialog-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-JP other7 (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; koin1 maps 3/4/5, koin3 map 6, balden map 3, plus one overworld fishing-spot warp each on map02/map03). enter_world_map_scene installs the typed Portal/NPC entities with their spawn positions from disc.
  • Rendering - the continent ground renders as a heightfield surface: Scene::walk_heightfieldbuild_walk_heightfield sweeps the 0x1000 cells and emits one quad per cell, each corner's Y from the +0x4000 floor-nibble grid via the floor LUT (the FUN_80019278 math), verified vs the real disc (map01/02/03 build >10k-quad heightfields with genuine elevation). play-window draws it as the ground with the placed landmarks (Scene::walk_object_placements, the flags & 0x4 slot-1 pack meshes via record[+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_locomotion records the heading into the actor's render_26). Slot-4 vertex-pool inspection overlay: the per-kingdom object-mesh library (kingdom-bundle slot 4) decodes onto SceneResources::world_map_slot4 for every SceneLoadKind::WorldMap scene (and only those); with LEGAIA_WORLDMAP_SLOT4=1, play-window builds a colour-by-kind LineList from world_map_overlay::wireframe_segments_3d and 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-cell POLY_FT4 (cmd 0x2C) quads, one 32×32 quad 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..+0x18 run: +0x14 = 8×8 atlas tile index (u=(id%8)×32, v=(id/8)×32), +0x15 = PSX tpage (the terrain page/type: 0x1A grass, 0x0C mountain, 0x1B/0x1C water, 0x0B forest), +0x16..+0x18 = PSX clut word. Verified by aligning each quad run's UV→tile sequence to the .MAP's +0x14 grid (scripts/ghidra-analysis/analyze-walk-ground-tiles.py --verify-rule): tile/page/clut match the record 100% across mountain + coast captures. build_walk_heightfield bakes 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) (CBA 0x7E80) 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 CBA 0x7E80, so play-window animates 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-VM 0x4C n6 sub-0x61, a one-shot 16×1 MoveImage cell copy / flat-colour fill whose coordinates are script operands) and FUN_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 “single 0x1A grass page, positional (col%3,row%3), +0x14 unused” reading was a misread - grass cells use page 0x1A with +0x14 in the atlas's top-left 3×3 block, so the mod-3 sequence was coincidental; +0x14 IS the tile selector. Distinct from the top-view bulk continent (per-cell meshes via FUN_80043390 / MAN 0x7F-sentinel).
  • Portals + NPC talk-to - auto_engage_world_map_portals runs each tick (after locomotion, before the SM step) and drives any Portal whose placement tile matches the player's to its transition state, surfacing a WorldMapTransition to its target map (Idle portals only, once per visit). NPCs are talk-to: tick_world_map opens an Npc's inline dialogue (sets World::current_dialog + emits OpenDialog) on a confirm press within one tile, dismissed on the next press. Dialogue text is inline in the record's structural 0x1F-lead segment pool (found by first_inline_dialog_offset, not by a dialog opcode), not the scene MES. OwnedDialogPanel::from_inline_dialog decodes the first 0x1F segment and play-window renders the box (geometry-header layout + multi-segment menu rendering not yet pinned). Not the 0x3F op: that is the named scene-change (it carries a destination scene name and calls the scene-change packet FUN_8001FD44), and the overworld's reachable towns/dungeons are listed as a table of 0x3F ops in the controller script - man_field_scripts::scene_destinations recovers 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 the top_view debug toggle flag; exposes tick(pad_input) matching FUN_801E76D4's control logic. Attached to World as world_map_ctrl: Option<WorldMapController> and ticked when SceneMode::WorldMap is 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.

See also