World Map
The overworld: the kingdom-sized map you walk between towns and dungeons, where random battles interrupt the trip. Under the hood it is the same field engine as a town - same walk controller, same collision grid, same script VM - plus a per-entity state machine that turns a step into a battle, a dedicated ground renderer, and the developers' own fly-over camera and debug menu, both shipped in the retail disc.
At a glance
- Where
- Three kingdom scenes
map01/map02/map03(Drake / Sebucus / Karisto). Kingdom bundles PROT 0086 / 0245 / 0392; the walk.MAPis the first entry of each scene block. Code: the world-map overlay, loaded into the shared0x801C0000+window in two builds (walk, top-view debug). - Game mode
0x03- the ordinary field-run mode a town uses. The overworld is classified by scene name, not by mode.- Walking
- Byte-for-byte the field locomotion against the same walkability grid; the camera is a pitched GTE view at a 6x world scale.
- Encounters
- A region-keyed roll on every 128-unit tile crossing feeds a 5-state carrier entity that installs the formation and launches the battle in one tick. Record format: encounter.
- Entrances
- Walk-on tile triggers in the
.MAPspawn partition-2 records of the scene MAN (its script-and-data bundle); each names its destination with a0x3Fscene change and carries its own story-flag gates. - Ground
- A heightfield from the
.MAP's floor nibbles, textured per cell from a multi-page atlas; landmarks are the kingdom bundle's slot-1 mesh pack; the sea shimmers by palette animation from slot 5. - Port
engine-vm::world_map(entity SM),engine-core::world_map(top-view controller),build_walk_heightfield(ground),engine-vm::prim_dispatch(top-view terrain). Browser twin: world overview.- Confidence
- Confirmed from disassembly and RAM captures for everything above, the walk camera included: it is the field zone camera, and on all three resident overworld states its live words equal the zone composer's staging descriptor.
How a step becomes a battle
Random encounters are not a separate subsystem. One MAN-placed carrier entity - distinct from the player - runs a five-state machine every frame. The idle state is where the encounter roll lands; the moment the carrier reaches state 1 it installs the monster formation and, in the same tick, falls through to the battle hand-off.
What each state does, in order:
| State | Action |
|---|---|
| 0 Idle | Polls the pending-BGM resolver and the pad for interaction. The encounter roll (region roll) or a script advance pushes the carrier to state 1 and points it at an encounter record. |
| 1 Activating | Clears the 4-slot formation cell, copies count monster ids out of the record, raises the player's "encounter active" flag, clears the record pointer, moves to state 2 - and keeps executing. |
| 2 / 3 Transitioning | Writes game mode 8 (battle), sets state 4, clears the encounter-active flag. |
| 4 Terminal | Parked; the battle's return path re-seats the overworld. |
The formation cell is the input to the battle-scene loader (battle). Scene and town transitions are not in this machine - they are the field VM's named scene-change op, covered under entrances.
Register-level detail: encounter-record installation and who arms it
The install body at 0x801DA620..0x801DA678 (inside the entity tick, jump table 0x801CEC28 on entity[+0x8A]):
- Clear the formation array
0x8007BD0C..0x8007BD0F(slots 3, 2, 1, then 0 - slot 0 in the delay slot ofJAL 0x801DE190). monster_count = entity[+0x94][+3].- Copy
entity[+0x94][+4 ..+4+count]into the cell byte-for-byte; clearentity[+0x94], setentity[+0x88] = 0,entity[+0x8A] = 2. - Fall through the case-2/3 arm:
_DAT_8007B83C = 8,entity[+0x8A] = 4, clear bit0x80000of_DAT_8007C364[+0x10](the player context, corpus-stable at0x80083794).
The adjacent byte 0x8007BD11 selects the battle-data PROT entry (0x367 or 0x36D). The random roll FUN_801D9E1C sets +0x88 / +0x8A / +0x94 from the rolled formation index. Scripted arming comes from a family of field-VM op handlers inside the script dispatcher (0x801DEEDC, 0x801DEF08, 0x801DEFA0, 0x801DF038, 0x801DF3FC, 0x801E1C38, 0x801E1F44, 0x801E21C0), each a "trigger encounter on actor X" op sharing one clause:
sw <record_ptr>, 0x94(<actor>)
sh $zero, 0x54(<actor>)
ori $tmp, $tmp, 0x400 ; "encounter armed" in actor[+0x10]
sw $tmp, 0x10(<actor>)
The carrier is reached through the per-entity update-function-pointer dispatch, never by a direct jal, which is what shows it is a MAN-placed entity and not the player. State 0's resolver call (FUN_800243F0) is the per-frame BGM/asset poller, not a location lookup.
Walking the overworld
Everything on field locomotion applies: the same held-pad remap, the same 2-unit stepping, the same per-axis collision against the walkability grid (the kingdom maps carry thousands of wall sub-cells). Three things are specific to the overworld:
- Region-keyed encounters. Each 128-unit tile crossing selects the first region whose bounding box contains the player; that region's rate depletes a step counter, and a counter at zero rolls a formation from the region's slice and hands it to the carrier. A camera-only map with no region table never rolls.
- The camera. The same zone camera a town has: the kingdom MAN's camera-region records, composed per region into a pitch-only GTE view with a 6.0x uniform world scale (battle uses 4x), focused on the player's X/Z and eased between regions. During the opening's Rim Elm fly-in the cutscene timeline owns the camera instead: a snap to the aerial shot, then a quadratic-ease-out descent.
- Camera-relative d-pad. The remap is not trigonometry: the held direction is looked up in an eight-entry compass ring and rotated by a whole number of 45-degree octants, so "screen up" always walks toward the top of the screen.
The port keeps what the player sees identical while placing its camera on a different world axis than retail; the compensating rotation lives in the remap. Rotating only one half of that pair breaks the screen contract, so a projection test and a pad-to-axis test pin both halves.
Deep dive: camera model, axis convention and pinned globals
| Symbol | Meaning | Value / global |
|---|---|---|
| H | projection distance | 368 (_DAT_8007B6F4) |
| S | base world scale matrix | DAT_8007BF10 = 24576 x I (6.0x) |
| R | pitch-only rotation trio | _DAT_8007B790 |
| focus | player world X/Z (negated) | _DAT_80089118 / _DAT_80089120 |
| TR | eye trio, composed per region | _DAT_800840B8: (-69, 776, 8875) on map01 near Rim Elm, (-71, 536, 9139) Sebucus, (-86, 406, 11041) Karisto |
screen = H * (R * (S * (v - player)) + TR) / Ze. The pose is the field zone camera's: the region record's pitch and eye depth, eye X = -(depth >> 7), eye Y compensated for the terrain height under the player, and the ease between regions the community poll probe's wmcam rows capture. An earlier model treated the Sebucus and Karisto values as two ends of one zoom axis; they are two region records, and that model framed map01 from well above retail's camera.
Axis convention. FUN_800467E8 looks the direction nibble (pad & 0xF000) up in the ring at DAT_800766FC (1000, 3000, 2000, 6000, 4000, C000, 8000, 9000), adds the octant count at gp + 0x2D8, masks & 7, and writes the entry back over the pad bits. The step arms of FUN_801D01B0 then fix the axes: 0x1000 = Z+, 0x4000 = Z-, 0x2000 = X+, 0x8000 = X-. Retail's yaw-0 camera looks down +Z; the port's world_map_camera_mvp puts the eye on +X, so its azimuth-0 Up walks -X. A single-axis sign flip turns the frame from a rotation into a reflection and is caught by the tests named above.
Port symbols. tick_world_map, step_world_map_locomotion, live_world_map_tick, set_world_map_regions (the region roll), World::advance_with_collision, World::world_map_camera_relative_bits.
Where the towns are: entrances and story gates
How does stepping on a tile mean "enter Rim Elm"? The kingdom .MAP holds walk-on tile triggers. Each points at a partition-2 record in the scene MAN, and that record is a small script: it may carry a named scene change (op 0x3F), a one-shot cutscene beat, or both, and it carries its own two story-flag lists - C1 blocks the record if any listed flag is set, C2 requires all of them.
.MAP.MAP triggers
2Recordthe trigger names a partition-2 record in the scene MANMAN P2[n]
3GateC1 / C2 story-flag lists decide whether the record runs at allflag test
4Beatoptional cutscene (the mist-wall force-walks are records with no destination)timeline
5Destinationop 0x3F names the CDNAME scene and the arrival tilescene change
Drake's hub, map01, as the disc lays it out:
| Destination | Record | Gate | Note |
|---|---|---|---|
town01 (Rim Elm), cave01, vell, vozz, suimon, jou | one P2 record each | none | reachable on a fresh arrival |
dolk / dolk2 | P2[1] / P2[2] | selector on flag 0x142 | same trigger and arrival tile; clear = pre-boss, set = post-boss interior |
keikoku (the Ravine) | P2 portals | C1 = [0x193] | drops out of the portal set once vozz sets the flag |
| mist-wall bands | P2[34..36] | C1 = [0x482] | force-walk beats, no destination; dispatched as a cutscene timeline |
The interior legs (what each town's exits lead to, which deeper rooms are gated) are tabulated on the chapter-1 hub sweep page. NPCs on the map are partition-1 placement records whose dialogue text is inline in the record; talking to one opens that text directly.
The exits that live in the other trigger table
Every scene has two walk-on trigger tables, not one. The first is inside the scene's .MAP; the second is a separate one-sector sidecar file, .PCH, with the same header shape, staged just past the map in memory. The per-tile lookup searches the map's table first and falls through to the sidecar. So a scene can put every one of its doors in a table that is not in its map at all.
Five scenes do exactly that, and it made them look one-way: Uru Mais (uru) and its three dream rooms (urudre1..3), plus jouine, the deepest Drake Castle room. Every one of them has a walk-on exit; it is only ever in the sidecar.
| Scene | Exit record | Walk-on band | Tail op | Leads to |
|---|---|---|---|---|
uru | P2[42] | tiles (36..39, 5) | 0x3F named scene change | MAP03 (Karisto overworld) |
uru | P2[37] | tiles (37..39, 44) | FMV trigger, movie 7 | uru2, after MV5.STR |
urudre1 | P2[2] | tiles (35..37, 22..24) | 0x3F | back to uru |
urudre2 | P2[9] | tiles (26, 14) and (24, 13) | 0x3F | map01 |
urudre3 | P2[0] | tile (51, 90) | 0x3F | back to uru |
jouine | P2[16] | tiles (17, 17..19) | FMV trigger, movie 8 | town0e, after MV6.STR |
uru is the chain's hub and carries six doors in total - the two above plus one dream entrance per room, at tiles (110..112, 35), (10..12, 98) and (80..82, 99). All six live in the sidecar. jouine is the only one of the five with no named scene change anywhere in its script: its record ends by firing a movie, and it is the movie's dispatcher that picks the next scene.
Three separate decoders read these five as sealed, and each answer was a property of the instrument. A script walk that reads a partition as one stream desyncs inside the first long block of dialogue text and never reaches ops sitting several kilobytes deeper. A tile sweep with a cap stops before the sidecar's rows, which sort after the map's - uru's exit band is the 63rd of its 118 walk-on tiles. And a short "did anything happen?" budget after stepping on a tile cannot outlast records that spend hundreds of frames in explicit waits, or that run a boss fight first.
Confirmed on hardware behaviour: walking four tiles north from Uru Mais's own arrival tile under an emulator probe spawns record 42 and calls the scene-change routine with MAP03, returning into the field VM's 0x3F arm. Since that tile exists only in the sidecar, the same capture is the live proof that the fall-through search is real and not just present in the code.
Deep dive: how the port seeds entities, portals and gates from the disc
- Placements. Partition-1 records (
ManFile::actor_placements):[u8 N][N x 2 locals][model][anim_id][tile_x][tile_z][script].classify_placementsreads the kind off the script: a genuine door warp (base0x3E,op0in100..=106, not extended) is a Portal to scene-type overlayop0 - 100; an inline0x1Ftext block or a scripted-battle install (op0 < 100) is an NPC; anything else is Plain. Eleven genuine portals exist in the whole corpus;map01has none, so its entrances come from the bridge below. - Bridge.
man_field_scripts::overworld_portal_sitesjoins.MAPtile trigger to P2 record to0x3Fdestination + arrival tile, decoding the op-0x70conditional pair asConditionalDest.enter_world_map_sceneruns each record throughpartition2_record_gates/World::p2_record_gates_pass(retailFUN_8003BDE0) and installs anOverworldPortalonly when the gate passes. - Drain.
auto_engage_world_map_portalsdrives a matching Idle portal to its transition state once per visit;SceneHost::tickdrains theWorldMapTransition- anOverworldPortalloads its scene and seats the arrival tile; a door portal resolves throughMapIdResolver.SceneHost::dispatch_walk_on_triggerruns in world-map mode too, leaving portal records to the entity SM and spawning beat records as timelines so their C1 one-shot latch holds. - NPC talk.
tick_world_mapopens an NPC's inline dialogue on confirm within one tile (first_inline_dialog_offset,OwnedDialogPanel::from_inline_dialog); the text is the record's structural0x1Fsegment pool, never the scene MES and never the0x3Fop. - Flag writers.
0x142is set by plain51 42script bytes inrikuroa's streaming-carrier MAN - the self-latching post-victory record P2[50] - and re-asserted on entry bydolk2P1[0]. Four further records carry the same op as rows of a developer flag menu rather than as the beat, which is why an opcode count credits six writers where two exist; the engine reaches it organically through the boss-stager record (organic_beat_records_disc.rs).0x482(mist walls) and0x225(the town01 opening one-shot) have no script writer in either the field-VM census or the motion-VM census; they are direct code paths in overlays and remain capture targets (runbook).
Two falsified readings, kept so they are not re-walked: "dolk2 is reached from a dungeon interior" (it is the same hub entrance selected by 0x142), and "0x482 is set in the other7 block" (desynced-walker noise over SJIS text). More on do-not-re-walk.
What you see: the render pipeline
The ground, the landmarks, the rotating horizon and the shimmering sea are drawn by four different mechanisms. The frame starts in the always-resident executable (SCUS) and calls out into the overlay only for the horizon plane and, in the top view, for the terrain renderers.
| Layer | Source | Mechanism |
|---|---|---|
| Continent ground (walk view) | .MAP floor nibbles at +0x4000 | A smooth heightfield: one quad per object-grid cell, corner heights through the floor LUT (the bilinear ground-height sampler's math). |
| Ground texture | object record +0x14..+0x18 | Per cell: 8x8 atlas tile index, PSX texture page (grass / mountain / water / forest are different pages), CLUT word. Matches the record 100% in captures. |
| Landmarks | kingdom bundle slot 1 (Drake 40 / Sebucus 36 / Karisto 56 meshes) | Placed by flags & 0x4 records; rendered as ordinary case-5 TMD actors. |
| Horizon / sky plane | cos table + three globals | A one-shot emitter behind a self-clearing gate: 224 iterations, about 670 prims, rotating with the camera angle. |
| Bulk terrain (top view) | same slot-1 pack, per cell | Ordinary TMD rendering whose per-primitive dispatch is mode-switched to eight overlay-resident renderers that add a distance-cue fog pass (below). |
| Sea shimmer | kingdom bundle slot 5 | Palette animation - see ambient effects. |
Cells with no object record have no ground quad in retail either - their surface is an environment mesh placed over them. A clear-colour hole over such a cell means a missing mesh, not a texturing bug. Slot 4 of the kingdom bundle is not a terrain source and holds no geometry at all; it is the scene's actor animation bank (slot-4 records).
Top-view bulk terrain: the overlay-replaced renderers
The top view (the developers' fly-over) draws the continent as per-cell meshes. The SCUS per-primitive TMD renderer picks its emit function from one of two tables, chosen by a scratchpad flag; with the top-view overlay paged in, the overlay table's eight high-mode slots point at overlay copies of the SCUS renderers with a fog post-process spliced in between the GTE projection and the packet write.
| Flag | Table | Rows | Lives in |
|---|---|---|---|
| clear | 0x8007657C | 4 (alpha 0 / 50 / A0 / F0) | SCUS |
| set | 0x801F8968 | 1 (alpha 0 only; the alpha offset is skipped) | world-map overlay |
The engine mirrors the dispatch in engine-vm::prim_dispatch; SceneLoadKind::WorldMap selects the overlay variant. mednafen-state prim-dispatch-table decodes both tables from a save state.
Per-slot delta: overlay leaf vs SCUS sibling
Every overlay leaf is the SCUS body plus a per-vertex distance-cue fog block gated on (u8*)(t2-0x2D1) & 0x10: clamp max-Z, shift by (u8*)(t2+0x90), mix the far-Z reference at t2-0x2E0 into the prim cmd, run GTE.dpcs (or dpct + dpcs for textured quads), then index the per-Z RGB LUT at t2-0x2BC three times to add fog tints into packet offsets 8 / 0xC / 0x10. Each leaf ends with j 0x80043580; addiu $a1, $a1, 0xc. Slots 8..11 share the SCUS low-mode dispatchers.
| Slot | SCUS sibling | Overlay leaf | Extra GTE ops |
|---|---|---|---|
| 12 | 0x80043658 | 0x801F7644 | dpcs |
| 13 | 0x80043768 | 0x801F7838 | dpcs |
| 14 | 0x80043B58 | 0x801F7F78 | dpcs |
| 15 | 0x80043C6C | 0x801F8198 | dpct + dpcs (FT4 / GT4) |
| 16 | 0x800438B8 | 0x801F7AA4 | dpcs |
| 17 | 0x800439E4 | 0x801F7CCC | dpcs |
| 18 | 0x80043DD4 | 0x801F8454 | dpcs |
| 19 | 0x80043F10 | 0x801F8690 | dpct + dpcs (FT4 / GT4) |
Three properties hide these from static hunts: the GP0 command byte comes from a descriptor table, not an immediate; an overlay capture that stops at 0x801F0000 clips every leaf; and the horizon emitter is called by a direct jal from SCUS that a sweep of the overlay alone never sees. Capture the overlay with the wide window 0x801C0000..0x801F9000.
Deep dive: per-frame dispatch, the horizon emitter and the pass iterator
Dispatch. Two handlers from the 28-mode table at 0x8007078C reach the render tick: the default handler FUN_80025EEC (FUN_8001698C → FUN_80016444(1)) and the mode-13 handler FUN_80025F2C (FUN_8001698C → overlay entry 0x801CE850 → FUN_80016444(0)). The argument only decides whether the early FUN_8005FB84 block runs. The horizon call is gated on the submode register _DAT_8007BC3C == 2:
80016750 lui v1, 0x8008
80016754 lw v1, -0x43c4(v1) ; _DAT_8007BC3C
80016758 li v0, 0x2
8001675c bne v1, v0, 0x8001676c
80016764 jal 0x801d7ea0 ; horizon emitter
Horizon emitter FUN_801D7EA0: if _DAT_801F351C is set, clear it, advance the angle _DAT_801F3518 += DAT_1F800393 * _DAT_801F3524, and loop 224 times emitting two POLY_FT4 (cmd 0x2C808080, chain tag 0x9000000) plus one small prim per step, vertices projected through the sine LUT at 0x8007B81C with _DAT_801F3520 and its 4/5 as scale moduli. The gate is armed by FUN_801D1344 (reads _DAT_8007BCD0/D4/D8) → FUN_801D8258 (writes the gate + _DAT_801F3520/24/28). The gate lives above 0x801F0000 and survives overlay swaps. 0x801C2B2C and 0x801C9688 are not copies of the wrapper and the emitter: they are those two bodies printed 0xE818 low, and PSX overlays are not relocated.
Pass iterator FUN_8002519C: five linked-list heads at _DAT_8007C34C..+0x20; per node, bit 0x8 of +0x10 takes an early return, otherwise jalr the tick at +0x0C.
| Offset | Type | Role |
|---|---|---|
| +0x00 | actor* | next (NULL terminates) |
| +0x0C | fn* | tick function |
| +0x10 | u32 | flags: 0x8 early return, 0x200 already-emitted guard, 0x800 free the chain at +0x44 |
| +0x44 | chain* | mesh / prim chain head (count, then pointers) |
| +0x48 | u8* | move-VM bytecode base |
| +0x56 | u8 | render mode 1..0xB for FUN_8001ADA4 |
| +0x70 | u16 | move-VM PC in halfwords |
Ticks seen on the overworld: FUN_80021DF4 (move VM step, 8 actors), FUN_8003BC08 (motion VM + the visibility cull FUN_801D79E8, 14 actors), FUN_801E76D4, FUN_801DA51C, FUN_801D1344. The render dispatcher's case 4 splits on actor[+0x9E] (0x4000 → FUN_8002A5A4, 0x2000 → overlay FUN_801CFA48, else FUN_80028158); case 5 walks +0x44 calling FUN_8002735C when the actor's +0x42 is non-zero, FUN_80029888 when that is zero and +0x7A is not, else FUN_80043390 - and across four save states in three game modes every one of 5089 gate hits took the last arm; case 1 re-poses each piece from the scene's animation bank before drawing it (slot-4 records); cases 2/3/6/7/8/B are LOD, particle, billboard and palette-cycle branches. The walk pool DAT_8007C018 settles at 45 entries on map01: five party meshes then the 40-mesh landmark pack, so pack mesh i is pool slot i + 5.
Walk .MAP source. The field-file loader FUN_8001F7C0 resolves the .MAP by PROT entry index (FUN_8003E8A8 over the in-RAM TOC at 0x801C70F0, index from 0x80084540); a live Drake walk reads index 85, the raw uncompressed 0x10000 region at PROT.DAT 0x655800.
Engine port: heightfield, texturing and the slot-4 inspection overlay
Scene::walk_heightfield→build_walk_heightfieldsweeps the0x1000cells, emits one quad per cell with corner Y =-lut[nibble], and bakes per-cell UV +[clut, tpage](WalkHeightfield::uvs/::cba_tsb). The heightfield is drawn without the Y-flip the pack meshes get - flipping it sinks every raised cell under its own buildings.- Texture rule:
u = (id % 8) * 32,v = (id / 8) * 32from+0x14; page from+0x15(0x1Agrass,0x0Cmountain,0x1B/0x1Cwater,0x0Bforest); CLUT from+0x16. Verified withanalyze-walk-ground-tiles.py --verify-rule; disc-gatedfield_ground_surface_disc.rs. - Landmarks:
Scene::walk_object_placements. Entities draw as kind-coded markers (portals cyan, NPCs green, encounter zones red) and the player as a marker with a facing tick. LEGAIA_WORLDMAP_SLOT4=1overlays the decoded slot-4 animation bank: one polyline per (clip, part) through that part’s decoded 12-bit translations across its clip (world_map_overlay::translation_path_segments). Slot 4 is an ANM container, not a mesh library - the draw used to callwireframe_segments_3d, which plots raw 16-bit slices straddling the entries’ packed nibble boundaries and is a byte-diffing aid, never geometry. The paths are object-local, so they draw about the world origin until an actor owns them.
History: "single grass page, positional (col%3,row%3), +0x14 unused" was a misread - grass tiles happen to occupy the atlas's top-left 3x3 block; +0x14 is the selector.
Ambient effects: why the ocean shimmers
Nothing moves on the overworld but you, yet the sea glitters. The effect is palette animation: a table in the kingdom bundle's slot 5 tells a walker to copy 16-pixel palette strips onto the water and shoreline palette cells on a fixed cadence, so the same texels cycle through a ring of colours. Two families are easy to confuse:
| Family | Source | Animates | Engine |
|---|---|---|---|
| Slot-5 CLUT-walk table | kingdom bundle slot 5 (type 0x06), 516 bytes, identical across the three kingdoms | the ocean head plus seven shoreline / terrain cells | legaia_asset::clut_walk, WaterAnim::Walk |
| Script-driven CLUT-cell ops | MAN 4C 61 ops (one-shot copy, cross-fade) | only the row-498 park-cell one-shots on map01 | engine-core::clut_fx |
The same type-6 table drives ambient walks in twelve carriers, not only kingdoms; towns add an effect tree and the HSV cycler behind jou's pulsating flesh - see field-ambient-fx.md.
Deep dive: table format, cadence arithmetic and the parked strips
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 x [u8 0][u8 hold_vsyncs][u16 0][u16 src_x][u16 src_y]. The asset-type dispatcher (case 6) installs it at DAT_8007B7C8; field init spawns one actor per entry with the accumulator seeded to 100 so all eight fire on the first tick; the SCUS walker (render case 0xB) adds the frame-skip byte DAT_1F800393 (overworld 3, towns 2) each tick and on acc >= hold emits a 16x1 MoveImage, resets to zero, and advances with wrap. Interval = ceil(hold/dt) * dt: the ocean head at fb (0, 506) (18 steps, hold 8) fires every 9 vsyncs, full cycle 162 vsyncs, about 2.7 s.
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; clut_walk::park_strips). map02 / map03 ship only rows {501, 503, 505} and inherit the rest as VRAM residue from the Drake upload, so the engine parks Drake's byte-identical records for the missing rows. The VRAM parity oracle excludes exactly the destination cells (WORLD_MAP_CLUT_CYCLE_CELLS); clut_walk_real and world_map_ocean_clut_live.rs pin it on all three kingdoms. The script family: FUN_801E4C58 (field-VM 0x4C n6 sub-0x61 one-shot copy / fill) and FUN_801E4794 (multi-frame cross-fade); map01 holds exactly eight such ops, all on row 498, frames denominated in vsyncs through the same skip byte.
The developers' tooling: top view and debug menu
The shipped game carries the tooling the designers used to fly over the map: a top-down camera behind a pad combo, and a scrolling dev menu with entries such as MAP CHANGE, ENCOUNT and BGM CALL. Both need a debug flag retail leaves clear, so no player ever reaches them - but the code is all there, and it is the source of the top-view terrain renderer above.
- The top-view controller is only the debug camera: with the view flag clear it returns immediately. The overworld's per-frame update is the ordinary field chain (gate-arm wrapper → locomotion), the same pair a town runs.
- Toggle: debug flag set + pad mask
0x4A+ held mask0x40flips the view flag, snapshots the camera and resets the panel cursor. - Top-view controls: d-pad scrolls X/Z by 8 per frame; Circle/Square turn the azimuth by
0x14; R1/L1 change zoom by 4. - Dev menu: at least 24 rows;
MAP CHANGEandCARD OPTIONreadCLOSEDon a retail flag;CAMERA,ENCOUNTandBGM CALLshow live readouts.CAMERAis the follow-camera switch0x8007B606(the row is its only writer after new game), drawnOFF, orONplus the walk-region box’s centre tile; the port’s dev menu carries it on both hosts.
Globals and engine port of the debug tooling
| Global | Role |
|---|---|
_DAT_8007B98C | debug flag (clear on retail) |
DAT_801F2B94 | view flag (walk / top view); DAT_801F2B95 bits 1 / 2 enable the screen-dim pass and a second animation |
_DAT_8007B850 / _DAT_8007B874 | pad mask / held mask |
_DAT_80089120 / _DAT_80089118 | Z scroll (Up / Down) / X scroll (Right / Left) |
_DAT_8007B794 / _DAT_8007B6F4 | azimuth / zoom-height |
_DAT_801F35A8..AC | camera snapshot taken on toggle |
0x801CF344 | dev-menu string table; DAT_8007B5F8 encounter-rate readout; _DAT_801F2E90 BGM readout |
Port: engine-core::world_map::WorldMapController models the four camera globals and the toggle (tick(pad_input)); engine-vm::world_map_overlay holds the 24-row dev-menu model (DevMenuRow, is_closed, cursor_step, format_fixed_decimal, decode_camera_readout), the escape-timer scheduler, the battle-records model and the equipment stat-comparison preview; engine-ui owns the draw halves (records_screen_draws_for, dev_menu_list_draws_for). legaia-engine play-window --world-map shows the camera state in the HUD.
History: an earlier reading of the top-view controller as "the world-map controller with a normal-walk path" is falsified by its own branch target (the walk branch is the epilogue). It mattered - it implied the overworld needed its own menu-open arm, which left the pause menu unreachable on the overworld in the port. See save screen.
Engine port
| Module | Ports | Notes |
|---|---|---|
engine-vm::world_map | carrier entity SM | EntityState (Idle / Activating / Transitioning / Terminal), WorldMapEntityHost, step(entity_idx, ctx, host); per-entity roles EncounterZone / Portal / Npc. Serves field-resident carriers too. |
engine-core::World | walk, regions, portals, NPC talk | tick_world_map, set_world_map_regions, enter_world_map_scene, auto_engage_world_map_portals; battles return via battle_return_mode. |
engine-core::world_map | top-view controller | camera globals + toggle. |
build_walk_heightfield / prim_dispatch | ground, top-view terrain | heightfield with baked per-cell texturing; overlay-variant prim table. |
legaia_asset::clut_walk | sea shimmer | slot-5 table parser; WaterAnim::Walk in play-window. |
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| entity tick | FUN_801DA51C | the 5-state carrier SM; formation install + mode-8 write in one tick | overlay_world_map_801da51c |
| region roll | FUN_801D9E1C | region-keyed encounter roll per tile crossing | world-map overlay |
| top-view controller | FUN_801E76D4 | toggle combo, scroll / azimuth / zoom globals; walk branch is the epilogue | overlay_dialog_801e76d4.txt |
| dev-menu renderer | FUN_801EAD98 | row labels, CLOSED gating, readouts | overlay_dialog_801ead98.txt |
| gate-arm wrapper | FUN_801D1344 | the overworld's real per-frame chain; arms the horizon gate | 801cfc40.txt window |
| horizon emitter | FUN_801D7EA0 | 224-step cos-projected sky plane | world-map overlay |
| render tick | FUN_80016444 | submode-2 gated direct call into the overlay | SCUS |
| prim dispatcher | FUN_80043390 | flag-selected table; eight overlay leaves at 0x801F7644..0x801F8690 | dump_world_map_top_prim_leaves.py |
| ground-height sampler | FUN_80019278 | the continent is a heightfield from +0x4000 nibbles | SCUS |
| compass remap | FUN_800467E8 | octant table lookup, not trigonometry | SCUS |
| P2 gate test | FUN_8003BDE0 | C1 any-blocks / C2 all-required | SCUS |
| CLUT walker | FUN_8001ADA4 case 0xB | slot-5 cadence; installed by FUN_8001F05C case 6 | SCUS |
| captures | - | walk pool of 45 entries; walk camera anchors; ocean cadence; texture rule 100% | mednafen overworld save states |
Full function-level reference: docs/subsystems/world-map.md.