World Map
The overworld traversal mode - the view where you walk the party across a kingdom between towns and dungeons, and where random encounters fire. This page traces how the retail code drives it: the walk controller, the per-entity state machine that turns a step into a battle, the render pipeline, and the developers' own top-down debug camera and dev menu, both still present in the shipped game.
How it works
The world-map code is not in the game's main executable - it lives in overlays, chunks of MIPS code the game streams from the disc into RAM on demand. We traced them with a disassembler (Ghidra) over live RAM captures; the FUN_80xxxxxx names below are the RAM addresses of functions in that trace. 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.
Two things worth knowing up front. First, the shipped game still carries the developers' tooling: a top-down debug camera (a pad-combo toggle, gated on a debug flag) and a scrolling dev menu with entries like MAP_CHANGE and ENCOUNT - both documented below. Second, encounters are not a separate subsystem: a 5-state per-entity state machine drives them, and the monster-formation install and the battle launch both happen in the same tick the carrier entity reaches its activating state.
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 - captures use a wide window (0x801C0000..0x801F9000, 228 KB) so the prim-mode dispatch table at 0x801F8968 and its eight overlay-resident emit leaves at 0x801F7644..0x801F8690 are included.
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 (TMD is the game's 3D-mesh format) at PROT entry 0086 slot 1, which the dispatcher walks once per frame through FUN_8002735C (the Legaia TMD renderer - 60 ops on the GTE, the PS1's geometry coprocessor). 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.
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 a runtime library of object-local 3D meshes (see the slot-4 records format doc) - independent of the continent terrain emit mechanism.
Why static hunts miss the terrain emitters (negative-result audit)
Three properties of the dispatch hide the terrain emitters from the usual static hunts:
- The GP0 cmd byte is loaded from a per-mode descriptor table (
DAT_8007326Con the SCUS side,0x801F8968in the overlay), not built withlui/liimmediates - soaddprimimmediate-scanners see neitherFUN_8002735C(the landmark TMD renderer) nor the eight overlay leaves. Static addprim hits reduce to the horizon emitter plus five non-terrain SCUS emitters (HUD sprite batches, screen-tint, digit batchers). - An overlay capture that stops at
0x801F0000clips every leaf address out of the analysed binary. The dynamic prim-pool-writers probe confirms top-of-list PC hits at0x801F7344..0x801F8DBC- exactly the eight high-mode renderer addresses. - The horizon emitter is called by direct
jalfrom SCUS; Ghidra's reference manager misses the cross-program call when sweeping the overlay alone (sweep SCUS to surface the caller).
An audit of FUN_80016444's direct jal targets (12 unique targets across 60 call sites) confirms none is a bulk-terrain emitter on its own - the bulk prims come from many small case-5 TMD renders going through the overlay-replaced per-mode renderers:
| 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). |
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.
The landmark prims originate in FUN_8002735C, whose cmd byte comes from the per-mode descriptor table at DAT_8007326C; 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). Neither shows up in immediate-scanning addprim hunts - see the negative-result audit disclosure above.
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. While a cutscene timeline that staged op-0x45camera params owns the overworld - the New-Game opening's map01 leg - the walk/top-view cameras stand down and the shell renders the same cutscene GTE camera the field prologue uses: retail's Rim Elm aerial fly-in is three camera beats in map01's opening record (a snap to the high aerial shot, then a45 0B .. apply 900mode-2 quadratic-ease-out descent), confirmed against a per-frame RAM capture of the live camera globals; the marker overlay hides while the timeline runs. The walk-view camera itself is the retail GTE model, RAM-pinned from the overworld resident savestates:screen = H·(R·(S·(v − player)) + TR)/ZewithH = 368(_DAT_8007B6F4),S= the base matrixDAT_8007BF10=24576·I(a 6.0× uniform world scale; the battle sibling holds 4×), a pitch-only rotation trio at_DAT_8007B790, focus = the player's world X/Z (_DAT_80089118/20hold its negation), andTRfrom_DAT_800840B8- two pinned zoom states,(0,536,9139)/(0,406,11041). The retail pitch/TR path between the anchors is not yet pinned; the community poll probe streams the full tuple on the overworld (wmcamrows: rotation trio + H + the TR low halves + view mode, on change), so any captured zoom/rotate session yields the trajectory. - 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. - The overworld axis convention - retail pins it twice, both in the instruction stream.
FUN_800467E8is not a trigonometric rotation: it looks the held direction nibble (pad & 0xF000) up in the eight-entry compass ring atDAT_800766FC(1000, 3000, 2000, 6000, 4000, C000, 8000, 9000), adds the integer octant count atgp + 0x2D8, masks& 7, and writes the entry back over the pad bits - so the remap is a whole number of 45° steps turning from+Ztoward+X, and a count of0is the identity.FUN_801D01B0's four step arms then fix the axes:0x1000→actor[+0x18] += 2(Z+),0x4000→ Z−,0x2000→actor[+0x14] += 2(X+),0x8000→ X−. Those are the raw PSX d-pad bit positions, so retail's unrotated overworld d-pad walks Up = world Z+, Right = world X+. The port's frame is offset from that on purpose: retail's yaw-0 walk camera looks down+Z, whileworld_map_camera_mvpputs the eye on+X, so the port's remap carries a compensating rotation and its azimuth-0 Up walks−X. What the player sees matches; the world-axis assignment does not - and rotating only one half of the remap/camera pair breaks the screen contract. A single-axis sign flip is a different animal (it turns the frame from a reflection into a rotation) and is caught by the projection test plus a pad→move_stateend-to-end test of the retail bit-to-axis table. - 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: pool slots 0..4 hold the five party meshes, so pack meshilands at pool sloti + 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][anim_id][tile_x][tile_z][script], giving spawn position, model index, animation clip (anim_id= scene-bundle ANM record + 1, installed into actor+0x5C;0= none - see the placement-header resolution), 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. 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 (walk view)
Heightfield ground
The continent ground renders as a heightfield surface: Scene::walk_heightfield → build_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). The baked corner height -lut[nibble] is already the world height the placement / actor transforms carry in their un-flipped translation, so the heightfield draws without the mesh Y-flip the pack meshes get - flipping it re-negates the elevation and sinks every raised cell below its own buildings (in town scenes that hid the whole cliff-top core, including Rim Elm's spawn plaza). 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). Distinct from the top-view bulk continent (per-cell meshes via FUN_80043390 / MAN 0x7F-sentinel).
Per-cell ground texturing (multi-page atlas)
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.
The gate is the object grid, in towns too. The ground quads share corner vertices, so a capture's ground lattice rebuilds camera-independently and the fitted projection re-projects every cell to sub-pixel residual. Asking per cell whether retail emitted a quad gives a clean split on a Rim Elm field capture: every on-screen 0x1000 cell has one, no on-screen objcell == 0 cell has one, and every recovered quad carries its record's +0x14/+0x15/+0x16. A floor cell with no object record therefore has no ground quad in retail either - its surface is an env mesh placed over it by a +0x10 record. Widening the gate to the collision grid is wrong twice over: it emits quads retail never draws, and - having no record - they sample empty atlas space, decode to 0x0000, and are discarded. A clear-colour hole over such cells means a missing mesh, not a texturing bug. Disc-gated coverage: crates/engine-core/tests/field_ground_surface_disc.rs.
Ocean / water CLUT animation (kingdom-bundle slot 5)
The water tile's CLUT row at fb (0, 506) (CBA 0x7E80) - plus seven more shoreline/terrain shimmer cells - is rewritten by a table-driven walker whose operand source is the kingdom bundle's slot 5 (the type-0x06 slot of PROT 0086/0245/0392): an LZS-compressed 516-byte CLUT-walk animation table, byte-identical across the three kingdoms (parser legaia_asset::clut_walk). Format: [u32 count=8][u32 entry_offsets[8]], then per entry [u8 kind=1][u8 nframes][u16 cumulative_size][u16 dest_x][u16 dest_y] + nframes × 8-byte frames [u8 0][u8 hold_vsyncs][u16 0][u16 src_x][u16 src_y]. The asset-type dispatcher FUN_8001f05c case 6 installs the decoded table at DAT_8007B7C8; field init FUN_801d6704 spawns one actor per entry (FUN_80024cfc, accumulator seeded to 100 so all eight fire on the first game tick); the SCUS walker FUN_8001ada4 case 0xB steps each actor - acc += dt per game tick (DAT_1F800393; overworld 3, towns 2), and on acc >= hold_vsyncs emits a 16×1 MoveImage from the parked strip onto the destination cell, resets acc to zero (live traces show strictly constant intervals), and advances the frame with wrap. Real interval = ceil(hold/dt)*dt vsyncs: the ocean head ((0, 506), 18 steps hold 8 over the row-505 strip with a 128/144 ping-pong ×3 mid-cycle) fires every 9 vsyncs on the overworld, full cycle 162 vsyncs ≈ 2.7 s. play-window runs the same model (WaterAnim::Walk / advance_ocean_animation: per-entry accumulators, CPU-VRAM move_image, re-upload on fire), with the legacy 13-frame ocean-head cycle (legaia_asset::ocean) kept only as a fallback for a bundle without slot 5.
The walk sources park at VRAM rows 498/499/501..505 as raw CLUT-block records in the bundle's slot-0 TIM_LIST (no TIM magic, so plain TIM walkers skip them; clut_walk::park_strips). map01 ships the full six-record set; map02/map03 ship only rows {501, 503, 505} and inherit the kingdom-invariant rest (rows 498/499/502/504) as VRAM residue from the Drake upload - map01 is always the first world map, and resident Sebucus / Karisto captures hold map01's record bytes on those rows byte-exact - so the engine parks the byte-identical Drake records for rows the scene's own bundle doesn't carry. The earlier map01 capture census stays as corroboration: every censused animating column falls inside a slot-5 destination cell (row 506 cols 0..48 incl. the STP near-copies + pure-channel tail; row 508 cols 0..48 incl. the map01-only [32..47] == [0..15] mirror - strip content, not a second writer; row 509's entries 42..43 inside (32, 509); row 500 cols 62..63 inside (48, 500)), and row 507 - which no entry targets - is fully static. The VRAM parity oracle excludes exactly the destination-cell fold (vram_oracle::WORLD_MAP_CLUT_CYCLE_CELLS); the disc-gated clut_walk_real test pins the full entry set against all three bundles, and world_map_ocean_clut_live.rs verifies the cycle live on all three kingdoms.
The row-498 park-cell fades are a separate, script-driven family - event-triggered MAN 4C 61 ops, not part of the slot-5 table: 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). map01's field MAN holds exactly eight 4C 61 ops, all on the row-498 strip park row (four frames=0 one-shots copying (112, 499) onto (0/16/32/48, 498) + four 128-vsync cross-fades back; scanner scene_clut_cell_fx, disc-gated map01_clut_fx_disc). The fade's per-tick advance multiplies by the same adaptive frame-skip byte 0x1F800393, so the frames operand is denominated in vsyncs; engine mirror engine-core::clut_fx + World::step_clut_fx, fed by the op4c_n6_sub_61_emitter host hook.
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: it draws raw object-local coordinates + group-polyline topology, whereas retail consumes the pool in place through the cluster-A handler chain - the per-prim GTE emit family rooted at the TMD prim dispatcher FUN_80043390, entered from the world-map renderer at ra 0x801F78D4 - which supplies the per-object placement transform and triangle emission (documented in the world-map overlay format doc); the viewer does not replay that handler chain. Off by default.
Misreadings to avoid
- "Single
0x1Agrass page, positional(col%3,row%3),+0x14unused" is a misread: grass cells use page0x1Awith+0x14in the atlas's top-left3×3block, so a mod-3 sequence is coincidental;+0x14IS the tile selector. - A placement classifier without the genuine-warp gate mis-classifies
geremi(op0=200) and the leftover-JPother7(op0=175/179) as portals to non-existent maps; the gate (!extended && op0 in 100..=106) rejects these text-desync phantoms.
- 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). The real overworld hop is theOverworldPortalconfig:map01has no partition-1Portalplacements, so its town/dungeon entrances are seeded (enter_world_map_scene) from the.MAPwalk-on tile-trigger → partition-2 record →0x3Fbridge (man_field_scripts::overworld_portal_sites), each carrying its exact CDNAME destination + arrival tile.WorldMapTransitionis emitted by the entity SM and drained bySceneHost::tick(sibling of the named-0x3Fdrain): anOverworldPortalloads itsscene_nameand seats the arrival tile; a doorPortalresolves itstarget_mapthrough theMapIdResolver. Story-gating is the entrance record's own C1/C2 gate:enter_world_map_sceneruns each bridge site's partition-2 record throughpartition2_record_gates+World::p2_record_gates_pass(FUN_8003BDE0: C1 blocks if ANY listed flag is set, C2 requires ALL) and installs anOverworldPortalonly when it passes. Most Drake entrances carry empty gates (unconditional); the Ravine (keikoku) portals carryC1=[0x193], so a fresh continent arrival keeps them reachable and setting0x193drops them from the per-visit portal set. The other half is walk-on beat records: a gate-1 tile trigger whose record carries no0x3F(the Drake mist-wall force-walk bands,map01P2[34..36],C1=[0x482]) is spawned bySceneHost::dispatch_walk_on_trigger- which runs in world-map mode too, leaving portal records (tested byp2_record_is_portal) to the entity SM and spawning only beats as a cutscene timeline, so their C1 one-shot latch is honored (band force-walks while0x482is clear).step_world_map_locomotionstands the player down while such a timeline runs. A finer mechanism lives inside an entrance record: a story-conditional destination where an op-0x70SysFlag.Testswitches the0x3Ftarget after a beat.map01's dungeon entrance (P2[1]/P2[2]) branches on flag0x142- clear →dolk(pre-boss), set → a second0x3F→dolk2(post-boss), same trigger + arrival tile;overworld_portal_sitesdecodes the conditional pair (ConditionalDest) and the seeder resolves it viaWorld::system_flag_test. (This falsifies the earlier "dolk2 is reached from a dungeon interior" reading - no interior listsdolk2; it is the same hub entrance asdolk, chosen by flag0x142.) Gate-flag setters beyond a scene's bundle MAN:man-scripts --system-flag-censuswalks the MAN field-VM ops0x50/0x60/0x70across EVERY carrier per scene (bundle + the streaming variant MANs); the other disc-resident bytecode writing the same bank is the motion VMFUN_80038158(op-7/op-8, carrier MAN tail-section 1, swept by--motion-flag-census). A flag absent from BOTH censuses is set by a direct code path (FUN_8003CE08-class call in an overlay). Where the chapter-1 spine flags landed:549/0x225(town01 opening one-shot) - writer still unrecovered; the RAM-diff pin stands but neither census finds a disc stream carrying it (the earlier "written by op-7 from its own script bytecode" reading is falsified), so the setter is a direct code path and the spine flag-writer capture harness is the closer.0x142(the Caruban beat / dolk-dolk2 switch) - writer pinned: the SETs are plain field-VM51 42script bytes in the rikuroa streaming-carrier MAN (extraction 157) -P1[10..12]plus the post-victory recordP2[50](C1 =0x142itself, the self-latching one-shot) - re-asserted by dolk2's carrierP1[0..1]and cleared by dolk's bundleP1[26]; the firehose capture caught the write live (ra 0x801E3598, the dispatcher's own0x5xSET arm) and the resident script heap byte-matches the carrier (the old corpus-negative stood because no census walked the streaming variant MANs). Engine: the whole chain is organic record execution - approaching the rikuroa boss-stager placementP1[3]runs the record through the field VM (World::install_boss_stagers_from_man/run_boss_stager_record; park gate =0x142, read from the record's own head test), whose52 89SETs the transient staged marker0x289and whose3E FF 11enters the fight; the post-battle field return re-runs the scene-entry scriptP1[0], whose72 89test arm spawnsP2[50]through the C1-gated record dispatch - the record's own51 42script bytes SET0x142(and62 89clears the marker), flipping the dolk→dolk2 entrance organically (disc-gated oracle:organic_beat_records_disc.rs).0x482(mist walls) - no script writer exists: the earlier "SETs in theother7block, clears in theedbalden/eddomanepilogue carriers" reading was desynced-walker text noise (full-width SJIS digits / anEXITlabel table aliasing the54 82/64 82op bytes; falsified per-site by hand disasm, pinned by the census decode-coherence flag inman_variant_carrier_census_disc.rs). The writer is a direct code path - a capture target (write-watch across the post-Zeto Drake-revival beat), like flag 549; the engine leaves the gate as-is meanwhile. - Chapter-1 Drake hub sweep - past the covered spine (
town01 → map01 → keikokuand the boss legmap01 → rikuroa / dolk → dolk2) the remainingmap01interior legs arecave01,vell,vozz,suimon,jou, each decoded + driven straight from disc bytes (no capture) bychapter1_hub_sweep_oracle.rs: drivingtown01 → map01and stepping onto the leg's portal tile loads it inFieldmode with its MAN present. Portals / entrance records / MAN partitions:cave01tile(37,110)P2[5][1,13,18];vell(77,97)P2[6][8,17,13];vozz(107,91)P2[8][4,24,20];suimon(57,61)P2[19][10,7,3];jou(95,23)P2[37][15,8,7]. Every one lists a single onward0x3Fback tomap01(joualso warps to its interior variantjouina).dolk2(post-boss variant; its real MAN is the streaming carrier extraction 70, partitions[29,73,17]) is a terminal interior: its only0x3Fleads back tomap01. Gate census: all five swept entrance records install unconditionally (empty C1/C2); the only C1-gatedmap01entrance iskeikoku(C1=[0x193]), the only op-0x70branch is thedolk/dolk2destination switch on flag0x142(a selector in the ungateddolkrecord), and0x482is a separate no-0x3Fband record - so no C1/C2 progression gate stands between a fresh Drake arrival and any of the five legs. The “suimonanddolk2share a MAN” identity - dissolved: both halves were the CDNAME-shifted scene window -dolk2's unshifted window bled two entries intosuimon's block and picked up suimon's sidecar copy of its own 2345-byte[10,7,3]MAN (rikuroa/geremialiased the same way; byte comparison + position law in scene-v12-table § over-read). In the retail framesuimonkeeps that MAN (its scripted bundle) anddolk2resolves its own streaming carrier (extraction 70,[29,73,17]).Scene::loadconverts CDNAME raw-TOC block ranges to the extraction frame (raw - 2), which also dissolves the historical “a scene's.MAPis two entries below its block” rule - the.MAPis simply the retail block's FIRST entry. - Chapter-1 hub depth (vozz + jou → jouina) - one level deeper on the two story-load-bearing legs, decoded + driven
by
chapter1_hub_depth_oracle.rs.vozzis the Ravine unlock: its0x3Fset is{map01}only (exitP2[10], tiles(60..62, 2)), but the ONLY0x193SET on the disc isvozzP1[7](51 93at MAN0xDA6, one-shot-guarded on0x2AC); itsP2[11..=13]are self-latching one-shots (each record's C1 is the very flag its own script SETs - the canonical one-shot beat idiom). Thetown01 → map01 → vozz → map01round trip drives clean in-engine.jou's castle door is chapter-gated: thejouinawarpP2[5](tiles(93..95, 97)) sits behindC2=[0x44D], whose only setter isjou's own opener chainP2[2] → P2[3] → P2[4]gated upstream on0x44B(set byizumiP1[15] /noaruP2[31] - the Noa beat). In-engine the gate behaves: fresh walk-on installs nothing; with0x44Dset the same walk-on spawns the door record.jouinais not terminal (destinations{jou, jouinb}, both ungated). Player-channel handshake (resolved):jouP2[5]'s door cutscene drives the PLAYER channel (A2 F8 06ExecMove +C3 F8 ...HaltAcquire). The timeline stepper models the handshake (ExecMove arms an in-flight countdown, HaltAcquire parks then steps past by encoded width; see cutscene), so the record reaches its trailing0x3Fand the oracle drives thejou → jouinahop toSceneEntered. - Chapter-1 hub breadth (cave01 / vell / suimon + the Drake Castle chain) - the remaining hub legs, one
level deep, decoded + driven by the disc-gated
chapter1_hub_breadth_oracle.rs.cave01is a two-mouth pass-through - one named0x3Fdestination (map01) carried by TWO exit records (P2[0]gate-0 trigger(8,89),P2[1]gate-1 band(93..94,96)). Nine of 18P2records are gated: six self-latch one-shots plus the longest ordered beat chain decoded on a hub leg -P2[13](C10x15E/ C20x15D) →P2[14](C10x169/ C20x15E) →P2[15](C1[0x13, 0x142]/ C20x169; the final beat stops replaying once the Zeto flag sets). UngatedP2[16]SETs the0x15Dentry key. The0x15Ebeat is read cross-scene byurudre1P2[0](the Uru Mais dream tests the cave beat).vell- single exitP2[10](band(88..92,7)).P2[11]self-latch C1=[0x2AF] (strictly vell-local Set/Test pair);P2[7]carriesC1=[0x63A, 0x7]byte-identical to vozzP2[7]'s gate, and0x63Ahas ZERO script sites disc-wide (no writer anywhere in the MAN corpus - open thread). Also carries a gate-4 trigger family (record 53, five scattered tiles) not seen on the other legs.suimonis a pure corridor - all threeP2records are ungated0x3Fexits tomap01; story variation only via the shared controllerP1[0]testing0x142. The Drake Castle interior is FOUR scenes deep:jou → jouina → jouinb → jouinc → jouind.jouinb([19,7,13]) is fully ungated (no jouina-styleC1=[0xF]busy-latch):P2[9]back tojouina,P2[10]deeper tojouinc([43,18,60], lists{jouinb, jouind}). Oncejou's0x44Ddoor is passed the deep castle is open. The oracle's part F drivesjou → jouina → jouinb → jouincend-to-end in one session (the door cutscene completes through the player-channel model) - the deepest driven interior chain in the engine.jouinc/jouinddecode is an open thread. Decoder asymmetry (pinned): the partition-1 destination-table scan under-reports doors carried only byP2records (jouinb'sjouinareturn door) - the reverse of thejouP2[5]blind spot; theP2walker and the strict portal-site join both see them. 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.