World map internals
The register-level companion to the world map tour: how the carrier installs a formation, the camera's globals and axis convention, how the port seeds portals and gates from the disc, the overlay terrain renderers, the palette-walk table, the debug tooling's globals and the routine-by-routine provenance.
Formation install and who arms the carrier
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.
Walk camera and axis convention
| 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 an ease between regions. 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, so the projection and the pad-to-axis mapping have to change together.
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.
Portals, gates and NPCs 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. - Talk-to. The overworld runs the field overlay's pad controller (
FUN_801D01B0), so its action button is the field's interaction probeFUN_801CF9F4: one point 64 units ahead of the player, a ±72 box against every placed actor, the touched placement's record resumed past its spawn section by the dialog SMFUN_80039B7C.tick_world_mapruns that path (world_map_record_talk_slot→trigger_field_interact→drive_inline_dialogue) against the placements' live seats. The fishing signboards (map02P1[7],map03P1[19]) are talk-to through it: frozen-lake flag0x378(map03), rod and lure holdings (4C DC), a yes / no picker, a white fade (34 01) and 48-frame wait (4A 30), then the record's own3E 64warp. The simplified-dialogue mode keeps the older path: an NPC entity's inline text, opened on confirm within one tile of its static spawn. - 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 through the boss-stager record.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 (capture 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.
Why the sidecar doors read as sealed
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.
Overlay terrain renderers
The per-primitive dispatcher picks its emit function from one of two tables, chosen by a scratchpad flag; with the world-map overlay paged in, the overlay table's eight depth-cued slots point at overlay copies of the executable's renderers.
| Flag | Table | Banks | Lives in |
|---|---|---|---|
| clear | 0x8007657C | 4 (alpha offset 0 / 50 / A0 / F0) | SCUS |
| set | 0x801F8968 | 1 (the alpha offset is skipped) | world-map overlay |
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.
Each overlay leaf also bends every corner's screen Y by the curvature table entry its own depth indexes, after the back-face test and the ordering-table depth; the executable's four lit handlers never bend, and no overworld mesh reaches them. mednafen-state prim-dispatch-table decodes both tables from a save state.
Per-frame dispatch, horizon and 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.
Sky band FUN_801F73E4 (PROT 0901): the terrain sweep FUN_801F69D8 calls it first, unconditionally, so every overworld frame draws it into the farthest ordering-table bucket and the terrain draws over it. It zeroes the yaw word, rebuilds the view, and projects (0, 0, 10000) from the raw eye position; the result’s screen Y is the camera’s horizon row, and the band’s top edge sits 16 pixels below it. Five 128×128 SPRTs alternate the two tiles of the 8bpp page at (512, 256) (CLUT 0x7A80, the kingdom bundle’s 256-colour TIM), scrolled by ((sx + playerX / 64 + yaw) & 0xFF) - 0xFF and darkened from 0x808080 to 0x404040 once system flag 0x14C is set. At the walk camera’s pitch the terrain hides most of it; it shows where the visible-tile window or a coast leaves the top of the frame open. Both resident overworld states’ packets reproduce exactly; the port draws it on both hosts, depth-tested at the far plane.
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.
Ground heightfield in the port
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. The rule matches the object record on every captured cell. - 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 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.
The palette-walk table
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) 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.
Debug tooling globals
| 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-field::world_map::WorldMapController models the four camera globals and the toggle (tick(pad_input)); engine-dev-menu::dev_menu_leaves 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). The overworld opens the pause menu through the ordinary field arm. See save screen.
In the port
| Module | Ports | Notes |
|---|---|---|
engine-field::world_map_entity | 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-field::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_scene_formats::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 |