Kajar notes of ChronoRET, rebuilding Chrono Trigger one matched routine at a time

Last updated: October 9, 2026

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):

  1. Vblank_Unk59D9 counts down every object's script timer (Obj_Unk1001, reloaded from Obj_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).
  2. Vblank_ReadScanlineCounters runs the queued objects while the frame still has time: before each object it reads the PPU's V counter and stops once it is at ObjQ_Unk69 ($48 from Scene_PostLoadInit) or more but below line 240.
  3. Evt_RunObjScriptSteps ($C0:5A93) runs one object: up to Field_Unk68 opcodes (5 from load) through Evt_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 $FF with a second byte below $90 goes: bank $C3 (BankC3_Entry0000) and the bank-$FD vectors FdVec_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 $2E and $88 fill.