Confidence

Confirmed (record shape, reader, install path) - Inferred (encoding within scripts). The reader (FUN_801DA51C body at 0x801DA620..0x801DA678) is fully decoded. The install path is the script-VM dispatcher's set of "arm encounter" opcodes (0x37/0x41, 0x38, 0x43, 0x47, 0x4C); Ghidra's C decomp of FUN_801de840 makes the install value explicit: pbVar43 = (byte *)(param_1 + param_2) - i.e. the current script-bytecode opcode pointer. So the encounter-record bytes (count at +0x3, ids at +0x4..) are the trailing operand bytes of the install opcode itself, inlined into the field-VM script for the scene that installs the record. There is no separate on-disc encounter-record array; the carriers are the per-scene field-VM script bundles (scene-v12-table sister pairs + scene-bundles scene_event_scripts). The exact opcode encoding (how target / sub-op bytes pack into +0x0..+0x2) varies per opcode and is decoded case-by-case in the dispatcher (see the script-VM subsystem).

Layout

+0x00  u8[3]  reserved             ; cleared to zero by the reader before the copy
+0x03  u8     monster_count         ; 0..4 inclusive
+0x04  u8[N]  monster_ids           ; N == monster_count, each id indexes the
                                    ; monster catalog (the per-scene battle_data
                                    ; group)
[possibly more after - fields not consumed by the formation copy]

The reader copies monster_ids[0..count] into the global formation cell at 0x8007BD0C..0x8007BD0F (a 4-byte array, one byte per slot). Slots beyond count stay zeroed. monster_count == 0 clears the formation cell entirely (no monsters spawn this round).

Reader

FUN_801DA51C (the world-map / field entity tick, see the world-map subsystem) at offsets 0x801DA620..0x801DA678:

801da620  lui v0,0x8008
801da624  addiu s0,v0,-0x42f4   ; s0 = formation_cell_base = 0x8007BD0C
801da628  sb zero,0x3(s0)        ; clear monster slot 3 (0x8007BD0F)
801da62c  sb zero,0x2(s0)        ; clear monster slot 2 (0x8007BD0E)
801da630  sb zero,0x1(s0)        ; clear monster slot 1 (0x8007BD0D)
801da634  jal 0x801de190         ; helper (effect / sound trigger)
801da638  _sb zero,-0x42f4(v0)   ; clear monster slot 0 (0x8007BD0C)
801da63c  lw v0,0x94(s1)         ; v0 = encounter_record_ptr = actor[+0x94]
801da640  nop
801da644  lbu a1,0x3(v0)         ; a1 = monster_count = record[+0x3]
801da648  nop
801da64c  beq a1,zero,0x801da67c  ; nothing to copy: skip loop
801da650  _clear a0
801da654  move a2,s0             ; a2 = formation cell base
801da658  lw v0,0x94(s1)         ; re-read record pointer (volatile)
801da65c  addu v1,a0,a2          ; v1 = &formation[a0]
801da660  addu v0,a0,v0          ; v0 = record + a0
801da664  lbu v0,0x4(v0)         ; v0 = record[+0x4 + a0] = monster_ids[a0]
801da668  addiu a0,a0,0x1
801da66c  sb v0,0x0(v1)          ; formation[a0-1] = monster_ids[a0-1]
801da670  slt v0,a0,a1
801da674  bne v0,zero,0x801da658  ; loop until a0 == monster_count
801da678  _nop

The s1 register is the actor record (caller's a0 in FUN_801DA51C); +0x94 is the encounter-record pointer slot. The clear-then-copy ordering means a monster_count < 4 record correctly leaves trailing slots zeroed. After the copy the reader clears entity[+0x94] and advances the entity's 5-state SM (entity[+0x8A]++), so the formation copy fires exactly once per arm. Just before the copy (0x801DA5F8..0x801DA61C) it also reads record[+0] (the opcode byte the record overlays) and ORs bit 0x80 into a battle-setup flag when it is non-zero - always true for a scripted arm, since the install opcodes are themselves non-zero. That byte is the first of the record's three "reserved" bytes (+0x00..+0x02 = the install opcode + its two operand bytes).

Discriminator (relevant to wiring this in an engine). There is no dedicated "encounter" opcode: the install opcodes are the field VM's generic halt-acquire family (0x37/0x41/0x38/0x43/0x47/0x4C), the same ones used for ordinary script yields. What turns a halt into an encounter is the consumer: only world-map / field entities ticked by FUN_801DA51C (those carrying the 5-state entity[+0x8A] SM) ever read their +0x94 as a formation record, and only once the SM reaches the encounter-confirm state. The random path enters that state via the FUN_801D9E1C roll (state 0); a scripted arm relies on the scene bytecode having authored [count @ +3][ids @ +4..] after the halt opcode on such an entity's context. The general-purpose scripted arm is the interact op 0x3E with op0 = 0xFF (3E FF <row>): the retail case-0x3E handler sets entity[+0x8A] = 1 and installs entity[+0x94] = ctrl[+0x20] + row * ctrl[+0x5D] + 1 — pointing the record slot at MAN formation-table row row directly (full arm + disc sites in battle.html: garmel rows 8/9 = Songi/Zeto, rikuroa row 17 = Caruban; boss rows sit outside every region's rollable [base, base+count) slice). Other per-scene shapes (an inline [count][ids] authored after a halt op) still occur; which shape a fight uses remains a per-scene bytecode fact.

Engine port. The clean-room field VM mirrors this split: the bare arm-encounter op (0x37/0x41) asks the host is_scripted_encounter_armed() and, only when armed, forwards install_scripted_encounter() the bounded record window overlaying the opcode ([opcode][op1][op2][count][≤4 ids]). The engine consumer (World) parses it as an encounter record, registers the formation, and forces the next field-step roll; a successful install disarms (fire-once, matching the retail entity[+0x94] clear). The armed flag stands in for "the active entity's FUN_801DA51C SM reached the confirm state" until the per-scene carrier identity is pinned.

Writer (record-pointer install)

The script-VM dispatcher (FUN_801DE840, see the script-VM subsystem) installs the encounter record on an actor with the pattern at 0x801DEEDC..0x801DEEEC:

801deedc  lw v0,0x10(s5)
801deee0  sw s0,0x94(s5)         ; actor[+0x94] = s0 (encounter record pointer)
801deee4  sh zero,0x54(s5)       ; reset actor sub-state
801deee8  ori v0,v0,0x400        ; raise "encounter armed" flag (state[0x400])
801deeec  sw v0,0x10(s5)

s0 is set once at the dispatcher prologue (addu s0, a0, s8 at 0x801DE858, i.e. s0 = param_1 + param_2 = bytecode_buffer + pc_offset) and is the current opcode pointer in the field-VM script bytecode. The Ghidra C decomp surfaces this as pbVar43 = (byte *)(param_1 + param_2). s5 is the resolved target actor - frequently the player context (_DAT_8007C364); when bit 7 of the opcode byte is set, byte +1 routes through the system-channel resolver FUN_8003C83C.

Multiple opcodes install the same pointer; each pairs the install with its own pre-install gate (target-actor selector, sub-op switch, etc.) and each advances the PC by a different amount past the opcode:

OpcodeInstall linePC advanceNotes
0x37 / 0x41 (shared case)0x801DEEDC / 0x801DEF08+3Bare arm-encounter. Second install on param_3 if iVar18 == _DAT_8007C364.
0x380x801DEFA0 / 0x801DF038+3Falls through to the same install clause; first branch reads a halfword table at 0x80073F04 into actor[+0x26] when low-7-bits of byte +1 are zero.
0x43 (sub-op 0/1/A/B)0x801DF3FC (decomp line 5223)+3Movement-target setup follows the install (actor[+0x14..+0x1A] from operand bytes); the encounter arms when the actor reaches the target.
0x470x801E1C38 (decomp line 5610)+3
0x4C0x801E1F44 / 0x801E21C0 / ... (decomp lines 6341 / 6460 / 6556)+3Three internal install sites in the same case body - one per inner sub-op.

All install paths share the same pre-install gate:

if (actor[+0x94] != 0  ||  actor == _DAT_8007C364) &&
   ((actor[+0x10] & 0x400) == 0  ||  *_DAT_801C6EA4[+8] != 0)

- the actor already has a record installed (re-arm), OR it's the player context (always allowed); AND the armed flag is clear OR the scene explicitly allows re-arm.

Two non-encounter writes to actor[+0x94] also live in the dispatcher (case 0x34: pbVar47 + 0xe and pbVar47 + 3). These do not raise the 0x400 flag and pair with actor[+0x9c]/actor[+0x9e] zero-writes; they're a separate "callback" pattern. Only the install sites listed in the table above are encounter-record arms.

Formation cell + battle-data variant selector

Adjacent to the formation cell:

AddressSizeRole
0x8007BD0Cu8[4]Active formation: monster ids per slot, populated by the reader above.
0x8007BD11u8Battle-data PROT-id selector. FUN_800520F0 case-4 path reads this byte and chooses PROT entry 0x367 (raw TOC index; = extraction entry 0869) when it equals the case-1 character index, otherwise 0x36D (raw TOC index; = extraction entry 0875). The selected entry is loaded as a kind-2 streaming asset for the battle scene.

Snapshots of the formation cell across captures (see the scripts/scenarios.toml manifest):

Save0x8007BD0C..0FInterpretation
mc1 (pre-encounter, map01)01 00 00 00One-monster record from the previous battle; selector reset to 0x01.
mc2 (in-battle, map01)04 04 00 00Two-slot encounter, both slots monster id 0x04.
mc3 (post-battle, suimon)0A 0D 00 00Two-slot encounter, monsters 0x0A and 0x0D.

Worked example: the Rim Elm training fight

The game's opening battle - the training fight in Rim Elm (town01) - is a scripted single-monster encounter against monster archive id 0x4F ("Tetsu", the 999/999 tutorial sparring partner). Reading the formation cell across the training-fight capture corpus shows the install boundary cleanly:

Capture phase (town01)0x8007BD0C..0FInterpretation
Pre-battle field (free movement, before the fight)00 00 00 00No formation installed - the cell is clear.
Battle loading (game_mode 0x15, graphics not yet drawn)4F 00 00 00One-monster formation: id 0x4F in slot 0.
Battle running (graphics / command menu / submenu)4F 00 00 00Same lone-monster formation.
Post-battle field (back to game_mode 0x03)4F 00 00 00Cell retains the last formation until the next install (cleared at the next encounter, not on victory).

The formation copy happens at battle entry: the cell is empty in the field and carries the lone id 0x4F from battle-load onward.

The id 0x4F is not an inline script literal - it is a per-scene formation index. Two independent surveys of town01's bytecode find no [count=1][0x4F] install operand anywhere: (1) the scene_event_scripts prescript at PROT entry 3 (the 0x4F bytes in the bulk payload are high-entropy asset data, not bytecode); and (2) the scene's MAN partition-1 field-VM scripts walked opcode-aware with the field-VM disassembler. The walk lands on every 0x37/0x41 yield byte and decodes the trailing [count][ids] window at each - across 53 records and 71 yield sites, zero carry the [1][0x4F] Tetsu signature. Every window that decodes is a count=0 artifact from the walker stepping into embedded MES dialog text (the windows are plain ASCII: 1F 64 6F 20 = "do ", 1F 56 61 = "Va", …). That is precisely the false positive a naive byte-scan produces; the opcode-aware walk distinguishes a real arm boundary from a dialog byte. See the disc-gated test town01_p1_arm_sites.rs and the legaia-engine man-scripts survey CLI. The lone-0x4F formation is instead town01 MAN formation index 4. The per-scene formations load from the scene's MAN asset into a contiguous 8-byte-stride table ([3 reserved][count: 0..4][≤4 ids]); in the live "talk to Tetsu / Come at me!" save state that table reads [04][07][0a][3f 3e 3e 3e][4f][0a 0a][3d 3d] - byte-identical to the engine's MAN parse (which yields exactly these 7 formations, formation_id 4 = [0x4F]). The scripted carrier selects this formation by index (it points actor[+0x94] at the row; FUN_801DA51C copies the count-1 record into the cell on the dialogue-accept), which is why the cell shows the lone 0x4F while no inline operand carries it.

The clean-room engine reaches this fight through the same indexed table: a cold boot loads town01's MAN formations (with the monster archive's real stats merged at scene entry), and World::install_man_formation(RIM_ELM_TRAINING_FORMATION_ID) (= 4) installs the existing row as the forced next encounter - no re-encoded record, the scene's merged stats stand (Tetsu's HP 999). EncounterRecord::rim_elm_training() remains for the hand-built [count=1][0x4F] window used by the arm-seam path.

The carrier entity

The MAN-placed actor that installs this fight is pinned: town01 partition-1 holds exactly one placement at tile (76, 65) with model byte 0x6A, whose interaction record carries a long multi-page dialog block - the sparring talk-menu and its battle-trigger branch. It is the only on-map placement whose inline dialog runs that long (the village's other NPCs are one- or two-line), and it sits adjacent to the town01 spawn. Mirrored as RIM_ELM_SPARRING_CARRIER_TILE / RIM_ELM_SPARRING_CARRIER_MODEL in encounter_record.rs and locked by rim_elm_sparring_carrier.rs.

The carrier is identified by its dialog block, not by an opcode-decoded selector: a field interaction record is dominated by embedded message text whose bytes alias field-VM opcodes (a literal > is 0x3E, the warp/interact opcode; ASCII punctuation hits the 0x37/0x41 yield bytes), so a linear disassembly desyncs inside the text and reports phantom interact / dialog ops with garbage operands. The dialog text itself is recovered structurally - as a run of 0x1F-lead / 0x00-terminated segments (man_field_scripts::first_inline_dialog_offset) - which is also how every town01 NPC's message renders. Confirming the dialogue-accept → actor[+0x94] install on this specific entity needs a mid-interaction RAM capture; the field-VM control flow from the menu branch to the formation-index install is the remaining open step.

The carrier set the field entity SM acts on is derived from the MAN itself rather than hand-built: man_field_scripts::derive_field_carriers walks the partition-1 placements and maps each interactable actor to a FieldCarrierConfig - the pinned sparring partner (is_rim_elm_sparring_carrier, by tile + model) becomes a ScriptedEncounter for formation 4, every other talk NPC a plain Npc keyed by its record index; decorative/warp placements carry no carrier. World::install_field_carriers_from_man installs that derived set and returns the sparring carrier's slot; enter_field_scene calls it on every field entry (the counterpart to the MAN encounter-table install), so the carriers are live from the scene's own data. They sit Idle until the carrier is engaged, which advances the actual MAN actor's FUN_801DA51C SM. Engagement is driven by the field-VM dialogue-accept, not a manual API: a field-interact op (0x3E, op0 < 100) on the sparring carrier's placement opens its inline dialogue and arms the engage, and accepting the prompt (the dialog-advance dismiss, 0x4C n5 sub-4) engages it - so the field-VM bytecode drives the fight. (engage_field_carrier remains the direct entry point the auto-arm and tests call.) Because the sparring dialogue's Yes/No box-selection logic is still undecoded, the engine treats the accept as the dialog dismiss; the tutorial fight is forced, so there is no decline path to gate. The formation index (4) is a pinned constant - the interaction record selects its formation by index, not via an inline [count][ids] literal - but which actor is the carrier, and where it stands, come from the scene data.

Scripted-battle id path (FUN_8005567c)

The actor[+0x94] record path is one of two ways the formation cell is populated. The other is a global battle-id at DAT_8007b7fc consumed at battle-init by FUN_80055b6c, which calls FUN_8005567c (SCUS_942.54) to expand the id into the cell: a plain id fills slots 0/1/2, while id ranges 0x07..0x09 / 0x49..0x4d / 0x88..0x8b / 0xa2..0xff get bespoke multi-monster / boss expansions. DAT_8007b7fc is a transient parameter (0 in every captured Tetsu frame). The distinguishing signature is the cell shape - FUN_8005567c writes slots 0/1/2 for a plain id ([0x4F,0x4F,0x4F,0]), whereas the Tetsu cell is [0x4F,0,0,0] (slot 0 only), which is the FUN_801DA51C count-1 record path. So the Rim Elm fight uses the indexed-record path; FUN_8005567c is the formation source for battles cued by a battle-id rather than an entity record.

Zeto is NOT on this path - it uses the record path in scene garmel. A live PCSX-Redux firehose capture (three reproducible runs forward-played from the chapter2_garmel_pre_zeto bracket) shows the Zeto fight installs its formation via FUN_801DA51C - the actor[+0x94] entity-record path (count=1, record[4]=0x4B), storing [0x4B,0,0,0] directly into DAT_8007bd0c at the battle-launch tick (writer ra inside FUN_801DA51C body 0x801DA620..78). A write-watch armed on 0x8007b7fc stayed silent across the entire fight (field → 0x08 load → 0x15 orbit → back to field), and a mid-battle save-state reads 0x8007b7fc = 0. Zeto is monster id 0x4B (byte-confirmed from the PROT 867 name table: Zeto, HP 5000, gold 8000, exp 9000). Although 0x4B falls in FUN_8005567c's bespoke 0x49..=0x4d boss-expansion band, the retail encounter carries the boss id in a count-1 entity record, not the DAT_8007b7fc global - so "boss id in-band ⇒ battle-id path" does not hold. The mid-battle cell [0x4B,0,0,0] matches the earlier zeto_*_mid_cast states byte-for-byte; those captures pinned the id, not the mechanism, and their jou scene attribution is superseded by the live forward-play (runtime scene buffer 0x8007050C reads garmel through the whole fight). The clean-room engine enters the fight through the MAN formation row selected by the beat record's 3E FF <row> op (World::trigger_scripted_battle), producing the same [0x4B] cell from the disc bytes.

Mt. Rikuroa's Caruban (id 73 = 0x49, same band) is a separate boss and uses the same record path: its carrier is the rikuroa P1[3] boss-stager placement (park-gated on first-visit flag 0x142, 3E FF 11 → formation row 17 = lone 0x49), which the engine runs on approach (World::run_boss_stager_record). Both bosses are 3E FF record-path fights.

Open - does any retail battle write DAT_8007b7fc? Four exhaustive Ghidra sweeps across all 47 loaded programs (SCUS + 46 overlays) - lui+addiu absolute stores, lui+ori, gp-relative (gp = 0x8007B318, validated), and pointer-table occurrences - find only readers (FUN_8005567c ×4, battle-init FUN_80055b6c, and the per-frame post-battle mode gate FUN_80046a20), zero writers by any addressing mode. A write-watch at the Zeto trigger cannot catch a writer - that trigger uses the record path and never writes the global. Every capture (Tetsu frames, the pre_zeto bracket, the full Zeto fight) reads DAT_8007b7fc = 0, and battle-init FUN_80055b6c takes its "== 0 ⇒ preserve the record formation" branch, so the global may be always-0 / vestigial in retail with all battles resolving through FUN_801DA51C. Whether any retail encounter exercises the FUN_8005567c battle-id path at all is the open question; the writer (if one exists) sits outside the static corpus.

Random-encounter trigger path

The script-VM install opcodes above describe scripted encounter arms. Random encounters use a separate path.

Roll function

FUN_801D9E1C (in the world_map overlay; also paged in by dance / fishing / slot-machine / cutscene_mapview / dialog_typing / debug_menu overlays - same code each time) runs once per movement update. It walks the per-scene region table at *(_DAT_801C6EA4 + 0x28) + 1 and matches the player's (x, y) against each region's AABB at pbVar9[0..3]. The matching region descriptor supplies:

  • pbVar9[4]: per-step rate increment.
  • pbVar9[6], pbVar9[7]: base + count of the formation slice the region rolls into.

The rate is then scaled by the user-config setting at _DAT_8007B5F8 (0 off, 1 low, 2 normal → << 2, 3 high → >> 2; the world-map debug menu ENCOUNT row cycles this byte) and by four sequential accessory / status modifiers, statically pinned in the same dump: the High Encounter passive (bit 0x3B, Bad Luck Bell / Nemesis Gem) shifts the rate << 2, the Low Encounter passive (0x3C, Good Luck Bell / Evil Talisman) >> 1, and system flags 0x1D / 0x1E shift << 1 / >> 1. The scaled rate is subtracted from the step counter at _DAT_8007B5FC. When the counter goes ≤ 0, two RNG draws pick a formation id in [pbVar9[6], pbVar9[6] + pbVar9[7]) and the roll function installs:

*(short *)(actor + 0x88) = formation_id;
*(short *)(actor + 0x8a) += 1;
*(uint  *)(actor + 0x94) = formation_table_base + 1 + formation_id * stride;
*(uint  *)(_DAT_8007c364 + 0x10) |= 0x80000;
_DAT_8007b5fc = (RNG % 0x1e7) - ((RNG % 0x1e7) - 0x3ce);

Note the install at +0x94 uses the same slot the scripted-encounter path uses, but raises a different flag bit (0x80000, not 0x400). FUN_801DA51C reads actor[+0x94] without checking either flag, so a single reader serves both paths.

Engine port (region-keyed roll). The roll is ported clean-room as region_encounter (PORT: FUN_801D9E1C). RegionEncounterTable preserves each region's tile-AABB + rate increment + formation slice (built from the MAN via region_encounter_table_from_man, the position-routed companion to the aggregated encounter_man::encounter_table_from_man). RegionEncounterTracker::on_step(world_x, world_z, rng) reduces the position to a 128-unit tile (coord >> 7), selects the first region whose AABB contains it, subtracts the setting-scaled rate increment from the step counter, and on a <= 0 counter rolls a formation uniformly from [base, base + count) with the one-step anti-repeat and the 0x3ce + rng%0x1e7 - rng%0x1e7 counter reset. The no-trigger path consumes zero RNG, matching retail (replay-safe).

Both scene modes route the tracker off the same MAN section. The overworld installs it via World::set_world_map_regions (rolled per tile in live_world_map_tick); a field scene installs it at scene entry via World::set_field_regions, and World::on_field_step rolls the active region and drives a trigger through the aggregated EncounterSession's transition / grace SM (EncounterSession::trigger_with) - so the field path varies the rate + formation pick by region instead of using the single mean-rate table, while keeping the same transition bracketing. A field scene whose MAN has no encounter-region section (towns) installs no tracker and falls back to the mean-rate session.

Encounter control block (_DAT_801C6EA4)

A 100-byte block allocated by FUN_8003A024 and populated per-scene by FUN_8003A110 ("Mesworks set encount group table"). After scene load it carries:

OffsetField
+0x20Formation table base. Records of stride +0x5d; record at index i lives at base + 1 + i * stride (the +1 skips a leading count byte).
+0x24Condition table base. Records of stride +0x5e.
+0x28Region table base. Records of stride +0x5f (AABB + rate + formation range).
+0x5dFormation record stride.
+0x5eCondition record stride.
+0x5fRegion record stride.

Per-scene MAN file (asset type 0x03)

The encounter data lives as the Man asset in each scene's scene_asset_table 7-asset bundle, descriptor index 2. The asset dispatcher (FUN_8001F05C) LZS-decompresses the Man payload into a heap buffer addressed by _DAT_8007B898; FUN_8003AEB0 (the per-scene MAN walker, called from FUN_801D6704 / family scene loaders) then walks the MAN's multi-section header and finally writes the encounter-section pointer into ctrl[+0x20] before calling FUN_8003A110.

The MAN multi-section header is byte-exact across all 80 retail scene_asset_table bundles and lives at MAN offset 0:

+0x00..+0x02   u16 LE  status_flags     ; return value; bit 0x400 hints world-map
                                         ; bulk terrain (set on map01/map02/map03)
+0x01          u8      low_bit_DAT_8007B6A8         ; secondary flag
+0x02..+0x22   16 x s16 LE  depth_lut    ; written negated to the GTE scratchpad
                                         ; (0x1F800314+0x48) for per-scene fog /
                                         ; depth-of-field
+0x22..+0x24   s16 LE  N0                ; partition-0 record count (open)
+0x24..+0x26   s16 LE  N1                ; partition-1 record count (consumed by
                                         ; FUN_8003A1E4 as the per-scene NPC /
                                         ; actor placement list)
+0x26..+0x28   s16 LE  N2                ; partition-2 (open)
+0x28..+0x2B   u24 LE  u24_28            ; in-table byte offset of section 0's
                                         ; length prefix within the data region
                                         ; (relative to records-end)
+0x2B..+0x2B+3*(N0+N1+N2)  3-byte records   ; concatenated [P0..P1..P2] partitions;
                                         ; each record is a u24 LE byte offset
                                         ; into the data region
+0x2B + 3*(N0+N1+N2)     data region     ; encounter section, actor-placement
                                         ; payloads, etc.

Section 0 (the encounter section) lives at records_end + u24_28. Sections 1..=5 chain via a 3-byte length prefix: each section is [u24 LE length][length payload bytes] and the next section starts at current + 3 + length. Section 5 is universally a zero-length terminator across the retail corpus.

The six sections install into different globals (per FUN_8003AEB0):

IndexInstall targetRole
0_DAT_801C6EA4[+0x20]Encounter / formation tables (consumed by FUN_8003A110; see below).
1_DAT_801C6EA4[+0x00](Open) - referenced by the field-script context dispatcher; the pointer is advanced past its length prefix immediately after the walk.
2_DAT_801C6EA0(Open) - same advance-by-3 treatment.
3_DAT_801C6EA4[+0x04]Camera-region table - count-prefixed 18-byte records queried by player tile (FUN_801DBA20); the camera-parameter payload split is decoded below.
4DAT_80073ED8(Open) - advances by 4 (skipping length + 1 byte); the byte at +3 is copied into DAT_80073EDC, and a zero terminator there detaches the pointer (DAT_80073ED8 = NULL).
5DAT_80073EE0Universally a zero-length terminator. Reserved-but-unused sentinel.

The encounter-section header (consumed by FUN_8003A110) is 4 bytes followed by three count-prefixed record arrays:

+0x00          u8      formation_stride
+0x01          u8      condition_stride
+0x02          u8      region_stride
+0x03          u8      formation_count
+0x04          formation_count x formation_stride bytes   ; encounter records
                   record_i[+0..+2] = reserved (other-path scratch)
                   record_i[+3]     = monster_count
                   record_i[+4..]   = monster_ids
+next          u8 condition_count + condition_count x condition_stride bytes
+next          u8 region_count + region_count x region_stride bytes
                   region_j[+0..+3] = (x_min, y_min, x_max, y_max)
                   region_j[+4]     = rate increment
                   region_j[+6]     = formation-range base index
                   region_j[+7]     = formation-range count
                   region_j[+8..]   = battle-bg variant flags + extras

For 0086_map01 (Drake's kingdom field-scene bundle), the MAN descriptor sits at file offset 0x3B238 (LZS in 6537 → out 11274 bytes). The decoded layout is:

status_flags=0x01B2, N0=12, N1=9, N2=42, u24_28=0x21D8,
data_region @ 0xE8, section_0 (encounter) @ 0x22C0 len 0x43E
section_1 @ 0x2701 len 0x15
section_2 @ 0x2719 len 0x0E
section_3 @ 0x272A len 0xC7
section_4 @ 0x27F4 len 0x6F
section_5 @ 0x2866 (terminator)

encounter: formation_stride=8, condition_stride=4, region_stride=12;
37 formations, 4 conditions, 64 regions.

Formation 3 = [00 00 00 02 04 04 00 00] matches mc2's in-RAM formation cell 04 04 00 00 byte-for-byte. The legaia_asset::man_section crate exposes this parser plus per-record decoders for formation + region rows.

MAN section 3: the camera-region table

Section 3 (installed at _DAT_801C6EA4[+0x04]) is the per-scene camera-region table: [u8 count] then count × 18-byte records. The per-tile query FUN_801DBA20 walks it (tile = (player_pos - 0x40) >> 7, first match wins). The field camera arrival handler FUN_801DBE9C hands the hit to the config loader FUN_801DBC20, which splits bytes[5..17] into the resident camera-parameter globals at 0x8007B607..0x8007B627 (a query miss loads fixed defaults). The camera-param builder FUN_801DAB90 then folds those globals into the same staging-struct slots the script-VM Camera Configure op 0x45 writes (pitch +0x02, yaw +0x06, eye-space (dx, dy, depth) +0x0E/+0x12/+0x16, GTE H +0x26; see cutscene op-0x45) - so a zone record is a per-region camera preset in the op-0x45 parameter space.

Provenance: loader ghidra/scripts/funcs/overlay_fishing_801dbc20.txt (byte-identical across the fishing / dance / debug_menu / slot_machine captures; the cutscene captures differ only in Ghidra label prefixes - the function sits in the co-resident camera cluster, while the overlay_0897 and overlay_baka_fighter captures alias different bytes at this VA). Consumers: overlay_cutscene_dialogue_801dab90.txt (builder) + overlay_cutscene_dialogue_801dbe9c.txt (arrival handler + defaults).

Record header (bytes[0..5]): +0x00 u8 kind (0 = anchor (rec[1], rec[2]) and the player tile both inside the scratch attribute box, 1 = inclusive tile-bbox match, 2..=0x1F = region-type mask bit vs _DAT_8007B8F4); +0x01..+0x04 = tile-space bbox [minX, minZ, maxX, maxZ] (kind 1) / anchor (kind 0). For mask kinds with byte[1] != 0, the loader side-copies bytes[3],[4],[1],[2] to scratchpad 0x1F8003E8..EB + mirrors 0x801F2778/80/7C/84 - the writes are Confirmed, the consumer is Unknown.

Camera payload bytes[5..17] - byte[5] is stored whole to the camera mode byte DAT_8007B607; its high nibble selects both the loader split and the FUN_801DAB90 build branch (low nibble = mode-specific strength). Mode 6 = keep current camera (the loader writes nothing). s16 = signed 16-bit LE via FUN_8003CE9C.

offsetsweep split (every mode but 3/5/6)mode 3 split (look-at anchor)mode 5 split (fixed shot)
+5u8 → B607 mode bytesamesame
+6u8 → B608 pitch sweeps16 +6..7B61C anchor tile Xu8 → B61C focus tile X
+7u8 → B609 depth sweep(high byte of anchor X)u8 → B624 focus tile Z
+8u8 → B60A heading-pitch couplingu8 → B60As8 → B620 height offset
+9u8 → B60B dy + ease dampingu8 → B60Bu8 → B60B
+10..11s16 → B610 base yaws16 → B620 anchor heights16 → B610 yaw
+12..13s16 → B60C base pitchs16 → B624 anchor tile Zs16 → B60C pitch
+14..15s16 → B614 eye depths16 → B614 eye-depth biass16 → B614 eye depth
+16..17s16 → B618 GTE Hs16 → B618 GTE Hs16 → B618 GTE H

(B6xx = 0x8007B6xx. Mode-5 B620 is an s8: the loader ORs 0xFFFFFF00 in when byte[8] >= 0x80.)

Global roles (consumer FUN_801DAB90 unless noted; angles in the 4096 = 360° space, one tile = 0x80 world units):

  • B607 - mode byte. High nibble 1/2: anchor-follow with a position-proportional yaw sweep - yaw = B610 ± (B607 & 0xF) · lerp(player X across the zone attribute box at 0x1F800384/386), sign by nibble. 3 = look-at anchor, 4 = pad-analog yaw (pitch B60C, depth B614), 5 = fixed scripted shot, 6 = keep current.
  • B608 - pitch sweep: high nibble 1/2 = sweep sign about the box Z midpoint (pitch = B60C ± (B608 & 0xF) · lerp(player Z)); other values pin pitch to 0x1B8.
  • B609 - depth sweep: high nibble n ∈ 1..=5 anchors the sweep (n−1)/4 of the Z span from the box max (depth = B614 + (B609 & 0xF) · lerp(player Z)); else depth = B614.
  • B60A - heading-coupled pitch adjust: high nibble 1..=4pitch += (B60A & 0xF) · heading / 4; 5/ 8 plus sin/cos-LUT (dy, depth) compensation from the player facing +0x16. Heading = func_0x80019278(anchor).
  • B60B - high nibble 1..=0xB: eye-space dy = 0x200 − heading · (B60B & 0xF); else dy = 0x200. The same high nibble is the per-frame ease-damping shift in the follow-ease FUN_801DB510 (srav by B60B >> 4).
  • B610 / B60C - base yaw / base pitch (staging struct +0x06 / +0x02).
  • B614 - eye-space depth (tr_eye.z, staging struct +0x16). Mode 3 treats it as a bias: depth = (dist3D(anchor, player) · ((B607 & 0xF) + 1) · 6 >> 10) + B614 − 0x4000.
  • B618 - GTE H (projection focal length, staging struct +0x26) in every mode.
  • B61C / B624 - camera anchor/focus tile X / Z. Mode 3: the look-at target is world (B61C·0x80+0x40, B620·0x20, B624·0x80+0x40) - yaw and pitch are aimed from it at the player. Mode 5: the focus tile - FUN_801DB8EC writes focus −(tile << 7) − 0x40 and FUN_801DB510 smooth-scrolls toward it.
  • B620 - mode 3: anchor height (× 0x20 world units). Mode 5: s8 height offset rotated by pitch into (dy, depth) via the sin/cos LUTs (_DAT_8007B7F8 / _DAT_8007B81C).

Query-miss defaults (FUN_801DBE9C): B607 = 0x10 (anchor-follow, sweep strength 0), B608 = 0x10, B609 = 0x30, B60A = 0x51, B60B = 0x20, B610 = 0, B60C = 0x1B8, B614 = 0x4000, B618 = 0x300.

Confidence: the three byte splits and the global routing are Confirmed (direct disassembly + the builder's consumption); the mask-kind scratchpad side-write consumer is Unknown.

What this doesn't tell us

  • Per-opcode encoding of the trailing operand bytes. Each install opcode (0x37/0x41, 0x38, 0x43, 0x47, 0x4C) packs its first 3 bytes differently (target selector / sub-op / flag bits); the count + ids layout at +0x3.. is fixed by the reader but the opcode-header bytes need a per-case decode in the dispatcher to interpret as "encounter trigger from script X at PC Y".
  • Sibling section roles (sections 1, 2, 4). The MAN multi-section walker pins exact offsets and lengths for every section across all 80 retail scene bundles, and section 3 is decoded (the camera-region table), but the interior layout of sections 1, 2, and 4 (the pointers _DAT_801C6EA4 + 0, _DAT_801C6EA0, and DAT_80073ED8 install onto) is still open. The lengths cluster small (often 1 byte, occasionally a few hundred), suggesting per-scene callbacks / inline state rather than record arrays.
  • The pre-encounter live-pointer state. No save state in the scenario corpus captures an actor with +0x94 mid-armed - the corpus's mc0 carries a stale value and every other slot is zero or 0xFFFFFFFF. Byte-level verification of "the encounter record bytes at the live actor[+0x94] match the parsed Man's formation record" needs a fresh save-state capture taken in the one-frame window between roll and FUN_801DA51C consumption.

Files referencing this format

  • crates/engine-vm - the field VM dispatcher port reads the operand and writes the actor pointer slot.
  • crates/engine-core::encounter - the runtime engine's EncounterRecord parser exposes monster_count / monster_ids from a candidate byte slice.
  • subsystems / world-map - world-map controller integration.
  • subsystems / script-vm - the dispatcher op-handler family that installs the pointer.

See also