Field event scripts
Every object on a field location (people, doors, the party members themselves) is driven by a
byte-code script in the location's event data. The interpreter, the opcode table
(Evt_OpcodeTable, $C0:5D6E, 256 words) and every opcode handler are in bank $C0 and matched;
what is left is outside the interpreter (see What is not matched yet).
See also: Frame cycle and NMI, Engine core, Sprites & objects.
Objects, functions and levels
LocLoad_Unk56D4 ($C0:56D4), the last location-load step, unpacks the event data;
LocLoad_CheckEvtData checks it and stops the game on a colour (Sys_HaltWithColor) if it is
bad. The data starts with a function table: 16 function offsets per object (32 bytes per
object at Evt_Data + slot × 16, the slot being the object number × 2).
Evt_InitObjects ($C0:5709) resets the per-object tables for the location's Evt_ObjCount
objects and runs each object's function 0 (its init function) up to opcode $00.
Evt_RunObjInit ($C0:597D) does the same for one object; Party_ReinitIfChanged uses it for
the character objects after a bank-$C2 round trip.
Each object runs at a level (Obj_Unk1C00), 7 after its init function. The function-call
banner reads lower levels as more urgent, "probably": a call only interrupts an object running at a
higher level. A call saves the interrupted script position in a per-level table
(ObjX_Unk7F0580), and Evt_Op00_Return ($C0:5F74) resumes the first saved position above
the level that ends. The action button (Field_ActionButton → Evt_StartTargetFunc1) starts
function 1 of the object found in front of the leader; Evt_StartTargetFunc2 is the touch
counterpart (function 2).
The scheduler
Scripts run from Field_EndOfFrame, once per frame, in two steps ($C0:59D9–$C0:5AC4):
Vblank_Unk59D9counts down every object's script timer (Obj_Unk1001, reloaded fromObj_Unk1000, 4 at load) and queues the objects whose timer ran out (kind-0 objects at the head of the list, others at the tail).Vblank_ReadScanlineCountersruns the queued objects while the frame still has time: before each object it reads the PPU's V counter and stops once it is atObjQ_Unk69($48fromScene_PostLoadInit) or more but below line 240.Evt_RunObjScriptSteps($C0:5A93) runs one object: up toField_Unk68opcodes (5 from load) throughEvt_OpcodeTable.
Every handler is entered with Y = its opcode's offset in the event data and returns X = where the
script goes on, with C=1 to run the next opcode in this run or C=0 to stop the object
until its next run. A handler that returns its own position with C=0 runs again next time: that is
how waits and moves in progress work. The two scheduler routines keep their Vblank_ names because
verified callers use them; their headers suggest Obj_QueueScripts / Obj_RunQueuedScripts.
The opcodes
Evt_OpcodeTable ($C0:5D6E–$C0:5F6D) holds one bank-$C0 handler address per opcode. Four
places read it with JSR (Evt_OpcodeTable,X): Evt_InitObjects, Evt_RunObj0Func1,
Evt_RunObjInit and Evt_RunObjScriptSteps. Its first two bytes are also read by the 32-byte
copies of LocLoad_InitUnk7F3700 (see Party walks).
The handlers, by group (each group has its own banner in bankC0.asm):
| Opcodes | Group | Range | Examples |
|---|---|---|---|
$00, $02–$07 |
Return and function calls | $C0:5F74–$C0:623F |
Evt_Op00_Return, Evt_Op02_CallObjFunc, Evt_Op04_CallObjFuncWait, Evt_Op05_CallPcFunc |
$08–$0E |
Object control | $C0:6240–$C0:62B4 |
Evt_Op0A_RemoveObj, Evt_Op0B_StopObjScript, Evt_Op0C_StartObjScript |
$10–$16, $18–$1A, $1C |
Jumps and conditions | $C0:62B5–$C0:658E |
Evt_Op10_JumpFwd, Evt_Op12_IfVarByte, Evt_Op16_IfMemByte, the 18 compare routines of Evt_CmpTable8/16 |
$20–$24, $27, $28 |
Party and object queries | $C0:658F–$C0:66A4 |
Evt_Op21_GetObjTile, Evt_Op28_IfObjInsideScreen |
$2D, $30, $31, $34–$39, $3B, $3C, $3F–$44 |
Button tests (and $47, which sets the frame-build scanline limit) |
$C0:66A5–$C0:6791 |
Evt_Op34_IfHeldA, Evt_Op3F_IfPressedA, Evt_Op47_SetScanlineLimit |
$48–$56, $58–$5A |
Loads, stores and copies | $C0:6792–$C0:6A1E |
Evt_Op48_GetLongByte, Evt_Op4E_CopyData, Evt_Op56_SetMemByte |
$5B, $5D–$61, $63–$67, $69, $6B, $6F, $71–$73, $75–$77, $7F |
Arithmetic and bits | $C0:6A1F–$C0:6D2E |
Evt_Op5B_AddVarByte, Evt_Op63_SetVarBit, Evt_Op7F_RandomVarByte |
$0F, $17, $1B, $1D–$1F, $25, $26, $7A, $7B, $8F, $92, $94–$9A, $9C–$A1, $A6–$A9 |
Object movement and facing | $C0:4D06–$C0:56A5 |
Evt_Op7A_ArcToTile, Evt_Op92_WalkDir, Evt_Op96_WalkToTile, Evt_Op8F_FollowPc, Evt_OpA8_FaceObj |
$AA–$AE, $B3, $B4, $B7, $B9, $BA, $BC, $BD |
Animation and waits | $C0:2E67–$C0:2FFC |
Evt_OpAB_PlayAnimOnce, Evt_OpAD_WaitRuns, Evt_OpB9_Wait4 |
$AF, $B0 |
Party control | $C0:2FFD–$C0:305C |
Evt_OpB0_PartyControl |
$B1, $B2, $B5, $B6, $B8, $BB, $C0–$C4 |
Yields, endless follows and messages | $C0:353F–$C0:3710 |
Evt_OpB1_Yield, Evt_OpB5_FollowObj, Evt_OpBB_Msg, Evt_OpC0_MsgChoice |
$D9, $DA |
Party walks and gathers | $C0:326C–$C0:353E |
Evt_OpD9_PartyWalkToTiles, Evt_OpDA_PartyGather |
$33, $C7–$D8 |
Palette, items, gold, characters, battle | $C0:3711–$C0:3AB6 |
Evt_Op33_SetPalette, Evt_OpC9_IfHasItem, Evt_OpCD_AddGold, Evt_OpD3_AddCharToParty, Evt_OpD8_StartBattle |
$DC–$E8, $EA–$EE, $F0–$F4, $F8–$FA |
Locations, control, sound, fades, map effects | $C0:3AB7–$C0:3E83 |
Evt_OpE0_Warp, Evt_OpE3_SetControl, Evt_OpE4_CopyMapRegion, Evt_OpEA_PlayMusic, Evt_OpF0_FadeBrightness, Evt_OpF4_SetShake |
$29–$2C, $2F, $32, $FE, $FF |
Credits, layer 3, window effects and scene exits | $C0:3E84–$C0:41E3 |
Evt_Op29_StartCredits, Evt_Op32_CreditsLine, Evt_OpFE_WinQuad, Evt_OpFF_Misc |
$57, $5C, $62, $68, $6A, $6C, $6D, $80–$83 |
Character and sprite objects | $C0:41E4–$C0:4866 |
Evt_Op57_InitChar0, Evt_Op80_InitCharObj, Evt_Op81_InitSprite, Evt_Op83_InitEnemySprite |
$2E, $7C–$7E, $84, $87–$8E, $90, $91 |
Object settings and palette animations | $C0:4867–$C0:4D05 |
Evt_Op84_SetUnk1B01, Evt_Op87_SetScriptPeriod, Evt_Op88_ObjPalAnim, Evt_Op8B_PlaceAtTile |
The unused opcodes ($01, $3A, $3D, $3E, $45, $46, $6E, $70, $74, $78, $79,
$85, $86, $93, $9B, $A2–$A5, $BE, $BF, $C5, $C6, $DB, $E9, $EF,
$F5–$F7, $FB–$FD) all point at Evt_UnusedOpcode ($C0:5F6E), which stops the game on
the colour Halt_ColorBadOpcode.
Operands follow common conventions: "slot" is an object × 2; "p" is 0, 2 or 4 for a party
member; "a"/"b" name event words at Evt_Unk7F0200 + a × 2 (a byte stored into one goes to
its low byte only); "m" is an address in bank $7F (Evt_Mem7F + m); "ptr" is a 3-byte long
address. Jump distances are unsigned bytes counted from the operand itself, forward for every jump
but $11. A condition opcode goes on with the next opcode when its condition holds and jumps when
it does not.
Movement
The movement opcodes set up a timed move and then return their own position with C=0 until it is
done, keeping Obj_Unk1A80 nonzero meanwhile. They lean on the object motion code:
Obj_CalcDirection ($C0:ABA2, an angle with 256 steps per turn from a 32×32 table),
Obj_SetVelocity and Obj_SetVelocityChecked (steps from the angle and speed with the hardware
multiplier), and Obj_MoveStep / Obj_MoveStepFree, which the per-frame object pass
Vblank_UnkA810 runs. Evt_Op7A_ArcToTile moves an object along an arc to a tile ("probably a
jump", per its header), with a gravity operand.
Messages
The message opcodes ($B8, $BB, $C0–$C4) wait until Field_Unk29 is 0, start
Field_Unk1F87's sequence with a number, the object and a pointer set by Evt_OpB8_SetMsgPtr,
then wait for it to finish. A treasure (Field_CheckTileInFront) starts the same sequence.
Field_Unk1F87 ($C0:1F87, run every frame from GameLoop_FrameBody) is the message window's
frame step; its banner suggests the name Field_MessageStep. It works by state in Field_Unk29:
clear the 1 KB text buffer at $7E:F000 (Field_MsgClearBuf) and let the NMI send it to the
four text lines; open the window a step a frame, placing it in the upper or lower half of the
screen ("away from the object, probably") unless Field_Unk30 fixes the place; draw the text one glyph
a step with bank $C2's text window (TextWin_Init / TextWin_Step, through Field_MsgStart and
Field_MsgStep); wait; then close the window step by step. The status codes the string's control codes
set (TextWin_Status) drive the waits: the end, a timed wait (its argument × 15 frames), a new
line or a new page, some after a button press. A choice cursor (Field_Unk62, 64, 65)
runs in the wait state, "probably". The text-line reading of Field_Unk31, and the reading of the
status codes, come from what the code does with them.
Items, gold and characters
The item and gold opcodes ($C7, $C9–$CE) call the bank-$C1 services behind
BankC1_Entry8003 (BankC1_ServiceTable, $C1:D126): 0 finds an item in the inventory, 1 adds
one, 2 removes one, 3 tests the 24-bit gold against an amount, 4 adds gold, 5 takes it off when
there is that much. The character opcodes ($CF–$D3), the item count ($D7) and $D5,
$F8–$FA go through BankC2_Entry8004 to the menu's command handler Menu_Unk8C36 (not
matched); its character commands work on the 9-byte list Menu_PartyOrder (three party places, then six reserve places).
Party walks
Evt_OpD9_PartyWalkToTiles walks the leader and the two other members to three tiles, and
Evt_OpDA_PartyGather walks members 2 and 3 to the leader. Both send the members off on small
scripts that LocLoad_InitUnk7F3700 seeds at $7F:3700 (each member runs at level 0) and wait in
the calling object until every member is back.
Quirks kept
The handlers keep the original's oddities, each documented in its header. A few:
Evt_Op00_Return's level search has no upper bound; Evt_Op6F_ShrVarByte treats a count of 0
as 256 shifts; the comparison numbers of opcodes $12–$16 and the bit numbers of $63–$66
are not masked; Evt_OpB6_FollowPc is missing a CLC; Evt_InitObjects fills three bank-$7F
tables and then clears them again in its next step.
What is not matched yet
Every opcode handler is matched. Outside the interpreter, the code the handlers lean on that is not matched yet:
- Where
$FFwith a second byte below$90goes: bank$C3(BankC3_Entry0000) and the bank-$FDvectorsFdVec_FFF1,FdVec_FFEB,FdVec_FFE8,FdVec_FFE5. These leave the field for good. - The menu's command handler
Menu_Unk8C36, behind the character opcodes. FdVec_FFF7's routine (EngFD_UnkE39C), which works each frame on the palette-animation records that$2Eand$88fill.