At a glance

Controller
One routine in the field overlay (PROT 0897), shared by towns, dungeons and the overworld walk mode. Not the tile-board puzzle mode that shares the overlay.
Collision grid
Per-scene, one byte per 128-unit tile: high nibble = four sub-cell wall bits, low nibble = floor tier. Streamed from the scene's .MAP file at load; scripts paint story-conditional changes on top.
Speed
480 units (3.75 tiles) per second walking, 720 running - counted in vsyncs, so the same on a busy frame as an empty one.
Height
A second controller after the walk: floor sampled from the grid's low nibble, or from per-tile ramp records; ledges trigger a scripted hop.
Engine
World::step_field_locomotion (engine-core); opt-in enhancements --edge-collision, --solid-npcs, precise movement (R).
Confidence
Confirmed - runtime write-watchpoints on the player position, wall-press captures, disassembly (how we know).

One frame of walking

1Gateskip everything if movement is disabled (encounter queued, cutscene owns the player) or the action button was just pressedplayer +0x10 2Camera remaprotate the held d-pad by the camera's eighth-turn so "up" means "away from the camera"pad remap 3Wall slideif the wanted direction is blocked, probe along the wall and add a perpendicular slideslide resolver 4Speedbase step (8 walk / 12 run) × actor multiplier × frame scalar; halve on slow terrain, trim diagonals by a quarterframe scalar 5Step loopmove 2 units per iteration, Z then X, each iteration checked against the grid and nearby actorscollision 6Settlere-sample the floor height under the new position; hop a ledge if the floor ahead rises a stepvertical settle 7Triggersentering a new tile checks the trigger block: teleports, door records, story beats, encounterswalk-on dispatch
  • Direction. The remapped mask names axes: 0x1000 Z+, 0x4000 Z−, 0x2000 X+, 0x8000 X−. Diagonals are two axes resolved independently. The heading (+0x26) snaps to one of eight angles.
  • Wall slide. When the pad's direction is blocked, three probes 62 units ahead (centre, ±33 lateral) sweep an offset table across the travel axis and add the slide direction the sweep's sign picks - so the player skids along walls instead of sticking. Pure diagonals and symmetric dead ends add nothing.
  • Step loop. speed = ((base × mult) >> 12) × frame_scalar units are spent 2 at a time; each axis is committed only if its probe is clear. The loop plays no sound - walking and wall contact are silent.
  • Animation follows motion. A committed step selects the walk (or run) clip; no step selects idle. The pad is never consulted for the clip.

How fast is a frame?

Retail runs two clocks: the vsync (60/s) and the game tick, one pass of the master frame driver. A scratchpad byte, the frame scalar, says how many vsyncs the last tick spanned (1..4, floored at 2 in field scenes and 3 on the overworld). Every duration and velocity inside the tick is multiplied by it, so a cadence change moves the sample rate, never the speed.

Base stepSelectorUnits / secondTiles / second
5forced slow3002.34
8plain walk4803.75
0xCrun7205.63
0x18debug turbo144011.25

The port ticks once per vsync with the scalar at 1 - 60 steps of 8 instead of 30 of 16 - and both hosts drive that tick at 60 Hz from a wall clock. Reading a per-frame number in the wrong clock is where "the player moves 1.67× too fast" comes from.

The field buffer: where walls come from

Nothing in a town's script says "this is a wall". The scene loader streams the scene's .MAP file into a per-scene buffer; the collision grid is one region of it.

+0x0000 +0x4000 +0x8000 +0x10000 +0x12000 objects0x20-byte records collision + floor1 byte per tile object mapu16 per tile trigger block4 kinds field pack+ efect.dat base pointer in scratchpad 0x1F8003EC - regions not to scale
The .MAP file is the first 0x12000 bytes of this buffer, streamed by CD-DMA at scene load; the walkability grid is its +0x4000 region.
RegionContentFilled by
+0x0000Object records, 0x20 bytes each: the buildings, fences and props (anchor tile, mesh index, rotation)the .MAP file
+0x4000Collision + floor grid, 0x80-byte rows: high nibble four sub-cell wall bits, low nibble floor tier.MAP; story deltas painted by op 4C 7s
+0x8000Per-tile word: low 9 bits object index, 0x400 object footprint, 0x800 elevation override.MAP; footprint bits stamped at scene init
+0x10000Trigger block: kind 0 teleports, kind 1 record triggers, kind 2 ramp heights, kind 3 region boxes.MAP
+0x12000Field pack and efect.dat; also the trigger lookup's fallback windowseparate files

Collision: how a step is tested

Each 2-unit sub-step asks one function whether the tile ahead is a wall and whether an actor is in the way. The world position becomes a tile by (coord + bias) >> 6; the byte's high nibble is masked by the quadrant the point falls in. Three points along the player's leading edge are probed (48 units ahead, ±16 lateral), so the footprint is tested, not the centre. The Z bias is authored into the wall bits: the floor sampler reads the same bytes with plain indexing, so one byte's two nibbles live under two different mappings.

BlockerTestResult bit
WallSub-cell bit under any of the three edge probes2
Static object±80-unit box around the record's footprint centre1 / 4
Moving NPC±40-unit box around its live position; a hit links the pair at +0x981 / 4

Pressing the action button runs a third probe: a radius-64 compass point in the facing sector against a ±72 NPC box; a hit posts the touch event (engaged flag, facing saved) and turns the player toward the NPC. Props fire the same event on contact, no button needed.

Height: two floor models

The walk controller only writes X and Z. After it commits, a second controller re-samples the floor under the new position and glides Y toward it; a third probes ahead along the last step and starts a scripted hop when the floor rises a ledge.

Tile word bit 0x800ModelUsed for
clearBilinear nibble surface: the four corner tiles' tiers through a 16-entry height LUT, weighted by sub-tile positionflat ground, gentle terrain
setElevation override: flat mean of the four corner tiers plus the tile's kind-2 record - −32 per whole-tile step, −16 per 64-unit sub-cell step. No interpolationramps and staircases

The kind-2 record replaces the bilinear branch, it is not a fast path on top of it. Rim Elm's shore ramps sit on sea-level nibbles and hold their whole elevation in kind-2 records; interpolating their nibbles walks the player under the drawn stairs. Engine: World::sample_field_floor_height carries both branches (--flat-y opts out).

Triggers: doors, beats and teleports

Crossing onto a new tile (tile = (world − 0x40) >> 7, compared against last frame's) looks the tile up in the trigger block. Scene entry marks the compare stale, so the arrival tile fires on the first tick.

KindRecordWhat it does
0[x][z][dest_half_x][dest_half_z]Intra-scene teleport - most house exits
1, gate 1[x][z][p2_record][1]Spawn a MAN partition-2 record if its story-flag gates pass: town exits (the record runs the named scene change), walk-on story beats
1, gate 0[x][z][p0_record][0]At scene init, bind a partition-0 record to the object on that tile as its touch script and idle clip - house doors, cupboards
2[x][z][coarse:i8][quads:u8]Ramp / staircase height (above)
3[x0][z0][x1][z1][type], 8 bytesRegion boxes: camera zones, encounter regions

A beat that should play once self-latches: its script sets the very flag its C1 gate lists, so it runs on the crossing that finds the flag clear and blocks itself afterwards. Rim Elm's dinner chain is the canonical example. The gate bitmap is the field VM's own story-flag bank, so a set is visible to the next record immediately.

Walking into a house

A town house is not a scene change: the interior is a sub-area of the same 128×128 grid parked in an unused corner, and the door repositions the player. Two door mechanisms exist and one house can use one of each.

ClassMechanismLives in
Script doorA gate-0 bind whose record teleports the player channel 0xF8 on contact - by instant teleport (A3 F8), teleport + move clip (CC F8 51) or animated walk-to-tile (C7 F8)MAN partition 0
Map doorA kind-0 teleport tile: no object, no script, no name.MAP trigger block

Map doors are the larger class; a MAN-only census cannot see them. Vahn's house is the mixed case: script door in, map door out.

NPCs: facing and glide speed

Initial facing

The placement record has no facing byte. At spawn, the placement installer pre-runs the record's leading ops (a 0x24/0x25 marker up to the first 0x21), and two of them write the heading from the 8-direction LUT at SCUS 0x80073F04: the NPC move-to-tile op 4C 51 (operand +3, low nibble) and CAM_CFG's simple path. Town prologues route that leg through story-flag tests, so the fall-through branch is the fresh-game facing. Rim Elm's side-by-side villagers at tiles (29,22) / (30,22) derive indices 6 and 2 - they face each other.

Heading space: retail 0 = Z−, 0x400 = X−, 0x800 = Z+, 0xC00 = X+; the engine's convention is the same rotated a half-turn. After spawn, walking snaps the heading and the rotate ops ramp it; the last writer in the frame draws (motion VM).

Glide speed

An NPC's pace is not the player's walk step - the walk kernel never reads the speed multiplier. Both NPC walk kernels carry the base step in the walk op's own operands, on the ladder numerator >> (2 + bits) units per vsync.

OpWhere bits livesNumerator
0x37 glide(op0 >> 5 & 4) | (op1 >> 6)0x80
0x41 glidesame0x40 - half speed
0x47 walk-to-tileb2 & 7; high nibble = approach mode0x80
motion-stream steps 03 / 19 / 20operand byte 1, low nibble0x80
motion-stream 06 / 184-bit selector over the operands' high bits0x80

Base steps run 32 / 16 / 8 / 4 / 2 / 1 for bits 0..5. There is no synthesised motion bytecode: the field VM parks the op's address on the actor and the walk kernel reads the record bytes in place each frame. Engine: placement_glide_speed decodes the placement's bound motion stream first, then its own yield ops, with a base-step-8 stand-in when neither decodes.

The port

  • Faithful path. step_field_locomotion rotates the pad by the camera azimuth (quantised to 90°, which agrees with the 45° remap for every axis-aligned retail camera) and steps through the same per-axis 2-unit collision. Wall-press captures rest byte-exact.
  • Opt-in fidelity. --edge-collision enables the three-probe leading edge; --solid-npcs enables the actor boxes (static props and moving NPCs). The slide resolver is ported standalone (resolve_field_slide), not yet in the live pad path.
  • Enhancements. Precise movement (R, default off): continuous azimuth, true 45° diagonals at unit speed, analog stick angle, sub-step remainder carried across frames. Camera distance presets (T) never touch the simulation.
  • Scene entry. .MAP resolved as the slot two below the scene's CDNAME block start (its 0x12000 footprint identifies it); the MAN's entry script runs on channel 0xFB; the MAN's encounter table is installed; input is locked while an opening-cutscene timeline drives the player.
  • Spawn. Cold entry (New Game, Rim Elm) creates the player at (0xA40, 0, 0xA40), the centre of the camera window; warp entry lands on the saved door coordinates.

How we know

Function / dataAddressWhat it provesDump
Locomotion controllerFUN_801D01B0Gate, remap, speed, 2-unit step loop; found by a write-watchpoint on the player X/Z (stores at 0x801D0684 / 06E4 / 0744 / 07B4)autorun_player_pos_watch.lua, field overlay dump
Wall collisionFUN_801CFE4CBiased tile mapping, high-nibble quadrant mask, probe tables DAT_801F21B4 / 2214field overlay dump
Actor collisionFUN_801CFC40Static ±80 / moving ±40 boxes, 0x20000 class bit, +0x98 linkfield overlay dump
Wall slideFUN_80046494Three-point probe, perpendicular sweep, slide bitfuncs/80046494.txt
Pad remapFUN_800467E8Eighth-turn rotation over the 8-direction ring DAT_800766FCfuncs/800467e8.txt
Frame scalarFUN_80016B6CDAT_1F800393 picked from a VSync(1) measurement with per-mode floorsfuncs/80016b6c.txt
Floor samplerFUN_80019278Bilinear vs override on cell bit 0x800; LUT at scratchpad 0x1F80035Cfuncs/80019278.txt
Settle / hopFUN_801D1BA0 / FUN_801D1878Step-delta globals 0x8007BDE0 / E4, hop arcfield overlay dump
Walk-on dispatchFUN_801D1EC4 → FUN_801D5630 → FUN_8003BDE0Tile compare, trigger scan with the +0x12000 fallback, C1/C2 gate checkfield overlay dump, funcs/8003bde0.txt
Object spawnFUN_8003A55C / FUN_8003AEB0Object record fields, bind lookup, footprint bit, LUT fillfuncs/8003a55c.txt
Grid sourceFUN_8001F7C0 via CD-DMA FUN_8005D9A0Write-watchpoint on the live grid caught only the DMA writer: the base grid streams, scripts paint deltasfuncs/8001f7c0.txt
Wall paint0x801E1C64 (op 4C 7s)Rect [col0, col1+1) × [row0+1, row1+2); subs 0/1 six bytes, 2/3 sevenoverlay_0897_801de840.txt
Wall-press capturesPCSX-Redux statesX− press pins the 47-unit edge; Z− press pins the authored Z bias; engine rests byte-exact on bothfield_wall_press_*.rs
NPC facing / glidetown01 placementsFacing pair (29,22)/(30,22); glide steps decoded from real operandsfield_npc_initial_facing_disc.rs, field_npc_glide_speed_disc.rs

Full write-up, including the hop-arc controller, the door-swing clip binding and the scripted-scene actor programs: docs/subsystems/field-locomotion.md.

Details

Player actor fields

The player actor pointer is _DAT_8007C364. Read +0x14/16/18 as 16-bit: a 32-bit read of +0x14 folds the angle word into X and looks like position drift.

OffsetMeaning
+0x10flags: 0x80000 movement disabled, 0x1000000 interact requested
+0x14 / 18world X / Z (s16, 128 units per tile)
+0x16terrain-conform angle - not the yaw
+0x26heading, eight angles
+0x5Crun state (> 0 selects the run clip)
+0x72speed multiplier, 0x1000 = 1.0
+0x94encounter-record pointer
+0x98interaction-target actor
Object records and the two spawners
OffsetTypeMeaning
+0x00u16X sub-tile offset: world_x = col × 128 + this + 0x40
+0x02u16Y offset added to the tile's LUT floor height
+0x04u16Z sub-tile offset: world_z = row × 128 − (this − 0x40)
+0x06 / 07i8Footprint column / row delta to the anchor tile
+0x08..0x0Cu16 × 3Rotation triple, 4096 per turn (+0x0A = yaw)
+0x10u16Mesh index into the scene TMD pack - for every id; the id selects the record, never the mesh
+0x12u16flags; 0x4 = placed

A placed record becomes an actor through exactly one of two sweeps. The scene-init sweep resolves the record's bind (the gate-0 trigger on its footprint anchor) and skips unbound records; the sub-area window rebuild spawns from the tile words with no bind lookup, gated only by the footprint bit. The sets are complementary (Rim Elm: 37 bound + 9 unbound = 46), so every placed record draws. A bound record with a clip id is posed by that clip's frame 0 - the cupboard's hinge doors hang through the floor without it. Parser legaia_asset::field_objects.

Scene entry: which .MAP, which script, which encounters
  • The .MAP is the PROT slot at define − 2 from the scene's CDNAME block; the first 0x12000 entry inside a block belongs to the next scene. The TOC-indexed payload is 0x4000 bytes; the grid lives in the entry's trailing sectors, so the loader reads the extended footprint. town01's grid matches the live RAM grid with zero diffs.
  • The entry script is the MAN partition-1 record 0 on channel 0xFB, sliced from the script start so 16-bit-wrapping jumps anchor at the slice base. Its wall paints sit behind story-flag tests: map03 paints tile (66, 102) solid behind flag 0x6C2 and clears three rects behind 0x378; on a fresh boot both are clear.
  • The same MAN carries the random-encounter table (section 0, installed at scene + 0x20); towns have real tables too - Rim Elm declares 7 formations at a 6/256 mean rate, gated by its region records.
  • Standalone towns use the count = 6 asset-table form in their block's second PROT entry; kingdom-bundle scenes use count = 7. Both resolve through find_bundle.
The scripted-scene actor

Some field actors are small programs rather than NPCs - the hop-arc controller, door swings, one-shot props. FUN_801D4A60 is a 38-state jump table on the actor's +0x54 whose entry state computes +0x54 = +0x54 + 1 + (+0x50 × 10): +0x50 is a program selector and the table is four short programs of 5, 5, 6 and 7 states entered at 1 / 11 / 21 / 31. Spawner FUN_801D5A24(program); port engine-core::field_actor_program.

Open
  • The full touch-event handler state beyond its decoded entry kernel.
  • Per-actor field-VM channel execution with story-flag branches: the engine loops decoded waypoint lists and takes the fall-through facing leg rather than evaluating the prologue's flag tests against live flags.
History: readings this page replaced

Walls were once thought to be authored entirely by scripts (a static search for CPU stores cannot see the DMA load). The +0x8000 region was read as a terrain-flag grid (it is the object map). The object table was read as NPC spawns rather than buildings (the near-zero +0x08..0x0C fields are the rotation, not the mesh). A positional "mesh band" rule for object ids was falsified by the GPU prim pool. The 4C 51 operand was read as a glide speed (it is a facing nibble). See do-not-re-walk.

See also