Event scripts
- Source
docs/executable/event-scripts.md- SHA-256
8eb122c3…a5e9ea73
How the executable runs the event script modules: the script VM, its opcodes, and the natives (game commands) that scripts call. The two tables it runs from are exported to data/exe/ as views exe.script_opcodes and exe.script_natives (The indexes). It's the same VM as the PSP release's, which Gibbed documented (reference_tools/Gibbed.LetUsClingTogether/docs/cutscene_scripting/), with more natives. Terms are as in the glossary.
| Based on | The unpacked executable, SHA-256 7b0230a11498f40396c9048345e7cfe0ef442cb7ca8faffc6f197920d2669599; every script module in event/ (event_script.md); Gibbed's opcodes.md and psp_natives.md (checked 2026-10-11). |
| Checked | 2026-10-11 |
| Method | Static analysis with Ghidra 12.1.4: the decompiled code in local/exe/decomp/, objdump -d, and ScriptTablesScript (Our Ghidra scripts); single-file C# programs counting opcode and native uses over the scripts; the R31 review of names, which read every interpreter case (2026-10-11). Every fact here is at most likely. |
The VM
- likely: there are 4 VM contexts of
0x148bytes at0x140F2CA90;0x14034e780sets one up for a module (its tables, variable storage, callbacks) and0x14034e9d0finishes loading it (variable initial values, autostart functions). The scene's module runs in context 0 (0x1402d0600).0x14034dd20resets all four and registers the natives. - likely:
0x14034a060runs a script thread:0x14034a1b0, the interpreter, executes instructions until one blocks, or for at most 65,536 steps a call, then the thread resumes next time from where it stopped. - likely: values live on one stack at
0x140F2CFB0: a depth, then up to 63 entries, each tagged int (1) or float (2). Popping an int from a float entry truncates it, and from an empty stack gives 0 (0x14034e220); popping a float from an empty stack ends the game (0x14034e1c0callsexit). Pushing onto a full stack is ignored (0x14034e2b0int,0x14034e280float). - likely: each thread's frame has three registers that opcodes push and pop, named here by their frame offsets:
r08,r0candr10(Gibbed's X, Y and A). Source:0x14034a1b0cases0x1B–0x20.
Opcodes
The interpreter is one switch on the opcode byte (0x14034a1b0), with cases 0–99, whose numbers match Gibbed's opcode list. After each instruction it advances by the instruction's size, read from a table of 0x18-byte records at 0x140C9E300, one per opcode; when an instruction blocks (a native that isn't done, a wait), it steps back by the same size and the thread stops there.
exe.script_opcodes has one row per record, 100 rows (ScriptTablesScript): opcode, record_address, size (bytes, 0, 1 or 3), operand_kind, unk_0x04, name_address and name (the record's name pointer), unk_0x10 and unk_0x14.
operand_kind |
Opcodes | The u16 operand is |
|---|---|---|
| 0 | the rest | none (size 1; nop_0, 0, has size 0), or unused (98, size 3) |
| 1 | 79 push_int_immediate |
a signed value |
| 2 | 76 push_tag, 77 push_act, 78 push_int_table |
an index into the unk_0x2C_offset table or the int table |
| 3 | 72–75 (push_variable, pop_variable, …) |
an index into the variable table |
| 4 | 88 call, 89 call_pop_r08, 93 call_result_r10, 94 call_pop_r08_result_r10 |
a native's number (Natives) |
| 5 | 80 push_float_table |
an index into the float table |
| 6 | 95 request_all, 97 request_wait_all, 99 request_i_all |
an index into the unk_0x30_offset lists |
| 7 | 2 tag (size 0) |
— |
| 8 | 1 label (size 0), jumps 81–87, 90–92, 96 |
an index into the script's jump table |
- likely: the records' names are stripped: opcodes 0–98 point at an empty string. Record 99 points at
mn_battle_action_headerand its last two fields are −1 and0x7FFFFFFF, so the table probably ends at 98 and opcode 99 reads its size (3) from the next data. No script uses opcode 99. Source:exe.script_opcodes; the C# use count. - likely: the interpreter reads only
size(0x14034a1b0);operand_kind,unk_0x10andunk_0x14are read by no code found, probably left over from a disassembler or debugger.
Opcode names
Each opcode's name, from its case in 0x14034a1b0 (R31 review). The guess names keep Gibbed's or FF12's label (in the last column) where the code fits it but doesn't show it; opcodes with no meaning shown are named by number. "Pops 2" takes the top as the right-hand operand.
| Opcode | Name | Confidence | What the case does | Gibbed |
|---|---|---|---|---|
| 0 | nop_0 |
likely | Nothing (size 0); the same case as 42, 43, 44, 51 | Invalid |
| 1 | label |
guess | Not a case: the default ends the run; record size 0, operand_kind 8 |
Label |
| 2 | tag |
guess | Default case; size 0, operand_kind 7 |
Tag |
| 3 | halt |
likely | Sets thread +0xD bit 0x40, which makes the interpreter return at entry, and stays on the instruction | Halt |
| 4 | discard |
likely | Pops one int and drops it | System |
| 5 | logical_or |
likely | Pops 2, pushes 1 unless both 0 | LogicalOr |
| 6 | logical_and |
likely | Pops 2, pushes 1 when both non-zero | LogicalAnd |
| 7 | bit_or |
likely | | |
Or |
| 8 | bit_xor |
likely | ^ |
ExclusiveOr |
| 9 | bit_and |
likely | & |
And |
| 10 | equal |
likely | 0x140349d70 returns sign(second − top); pushes == 0 |
Equal |
| 11 | not_equal |
likely | ≠ 0 | NotEqual |
| 12 | greater_than |
likely | > 0 | GreaterThan |
| 13 | less_than |
likely | < 0 | LessThan |
| 14 | greater_or_equal |
likely | ≥ 0 | GreaterThanOrEqual |
| 15 | less_or_equal |
likely | ≤ 0 | LessThanOrEqual |
| 16 | shift_left |
likely | << |
ShiftLeft |
| 17 | shift_right |
likely | >>, arithmetic |
ShiftRight |
| 18 | add |
likely | Int or float | Add |
| 19 | subtract |
likely | Subtract |
|
| 20 | multiply |
likely | Multiply |
|
| 21 | divide |
likely | ÷ 0 gives 0 | Divide |
| 22 | modulo |
likely | Ints only | Modulo |
| 23 | logical_not |
likely | Pushes value == 0 | Not |
| 24 | bit_not |
likely | Pushes ~value, not the negation |
Negate |
| 25 | op_25 |
— | Default case | Undefined25 |
| 26 | op_26 |
— | Default case | Undefined26 |
| 27 | push_r10 |
likely | Pushes frame +0x10 | PushA |
| 28 | pop_r10 |
likely | Pops into frame +0x10 | PopA |
| 29 | push_r08 |
likely | Frame +0x08 | PushX |
| 30 | push_r0c |
likely | Frame +0x0C | PushY |
| 31 | pop_r08 |
likely | PopX |
|
| 32 | pop_r0c |
likely | PopY |
|
| 33 | request |
guess | Pops 3, starts a function on another script (0x14046c390, 0x14046b780), mode 0 to 0x14034e2e0 |
Unknown33; FF12 REQ |
| 34 | request_f |
guess | As 33, mode 2 | Unknown34; FF12 FREQ |
| 35 | request_t |
guess | As 33, mode 4 | Unknown35; FF12 TREQ |
| 36 | request_sw |
guess | The request family's second path (LAB_14034b977) |
Unknown36; FF12 REQSW |
| 37 | request_f_sw |
guess | As 36 | Unknown37; FF12 FREQSW |
| 38 | request_t_sw |
guess | As 36 | Unknown38; FF12 TREQSW |
| 39 | request_ew |
guess | 0x14034e2e0(…, 0, 3) |
Unknown39; FF12 REQEW |
| 40 | request_f_ew |
guess | (…, 2, 3) |
Unknown40; FF12 FREQEW |
| 41 | request_t_ew |
guess | (…, 4, 3) |
Unknown41; FF12 TREQEW |
| 42 | nop_42 |
likely | Case 0's group | NoOperation42 |
| 43 | nop_43 |
likely | Case 0's group | NoOperation43 |
| 44 | nop_44 |
likely | Case 0's group | NoOperation44 |
| 45 | return |
likely | Pops the call frame (0x14046b3e0, 0x14046b830), resumes the caller or ends the thread (0x140349be0) |
Return |
| 46 | return_n |
likely | Pops n, 0x14046b560(thread, n + 1, 0) |
ReturnN |
| 47 | return_t |
guess | As 45, then 0x14046c020(thread, 0x14046b690(…)), unread |
ReturnT; FF12 RETT |
| 48 | return_t_n |
guess | Pops n, then as 47 | ReturnTN; FF12 RETTN |
| 49 | op_49 |
— | Default case | Undefined49 |
| 50 | request_wait |
guess | Pops 2, 0x14046c100(script, fn, −1), waits (state 3) |
Unknown50; FF12 REQWAIT |
| 51 | nop_51 |
likely | Case 0's group | NoOperation51 |
| 52 | request_change |
guess | Pops 3; writes a u16 into slot (< 8) +0xA8 of another thread | Unknown52; FF12 REQCHG |
| 53 | request_cancel |
guess | Pops n, 0x14046b560(thread, n + 1, 1) |
Unknown53; FF12 REQCANCEL |
| 54 | pop_int_0 |
likely | Pops an int into thread +0x54 | PopInt0 |
| 55 | pop_int_1 |
likely | Into thread +0x58 | PopInt1 |
| 56 | pop_int_2 |
likely | Into thread +0x5C | PopInt2 |
| 57 | pop_int_3 |
likely | Into thread +0x60 | PopInt3 |
| 58 | pop_float_0 |
likely | Pops a float into thread +0x64 | PopFloat0 |
| 59 | pop_float_1 |
likely | Into thread +0x68 | PopFloat1 |
| 60 | pop_float_2 |
likely | Into thread +0x6C | PopFloat2 |
| 61 | pop_float_3 |
likely | Into thread +0x70 | PopFloat3 |
| 62 | push_int_0 |
likely | Pushes thread +0x54 back | PushInt0 |
| 63 | push_int_1 |
likely | +0x58 | PushInt1 |
| 64 | push_int_2 |
likely | +0x5C | PushInt2 |
| 65 | push_int_3 |
likely | +0x60 | PushInt3 |
| 66 | push_float_0 |
likely | Pushes thread +0x64 back | PushFloat0 |
| 67 | push_float_1 |
likely | +0x68 | PushFloat1 |
| 68 | push_float_2 |
likely | +0x6C | PushFloat2 |
| 69 | push_float_3 |
likely | +0x70 | PushFloat3 |
| 70 | op_70 |
— | Pops one int, then a guard-check stub | Undefined70 |
| 71 | op_71 |
— | Default case | Undefined71 |
| 72 | push_variable |
likely | Through 0x14034d500, by type |
PushVariable |
| 73 | pop_variable |
likely | Clamped to the type | PopVariable |
| 74 | op_74 |
— | Resolves into 0x140F2D240, pushes nothing |
PushVariableDebug |
| 75 | push_variable_ref |
likely | Pushes offset | scope | 0x70000000 |
PushVariablePointer |
| 76 | push_tag |
guess | Reads the table at context +0x70 (unk_0x2C_offset, empty everywhere) |
PushTag |
| 77 | push_act |
guess | As 76 | PushAct |
| 78 | push_int_table |
likely | Pushes ints[operand] |
PushIntFromTable |
| 79 | push_int_immediate |
likely | Pushes the signed operand | PushIntImmediate |
| 80 | push_float_table |
likely | Pushes floats[operand] |
PushFloatFromTable |
| 81 | jump |
likely | Jump |
|
| 82 | jump_if_r0c_eq |
likely | Pops v; jumps if v == r0c | JumpIfEqual |
| 83 | jump_if_r0c_lt |
likely | r0c < v | JumpIfGreaterThan, named from v's side |
| 84 | jump_if_r0c_le |
likely | r0c ≤ v | JumpIfGreaterThanOrEqual |
| 85 | jump_if_r0c_gt |
likely | v < r0c | JumpIfLessThan |
| 86 | jump_if_r0c_ge |
likely | v ≤ r0c | JumpIfLessThanOrEqual |
| 87 | jump_if_r0c_between |
likely | Pops 2 bounds | JumpIfBetween |
| 88 | call |
likely | r08 = thread ID, 0x14034d330 |
Call |
| 89 | call_pop_r08 |
likely | Pops r08 first | CallAct |
| 90 | pop_r08_jump |
likely | PopXAndJump |
|
| 91 | pop_r08_jump_if_nonzero |
likely | PopXAndJumpIfNotZero |
|
| 92 | pop_r08_jump_if_zero |
likely | PopXAndJumpIfZero |
|
| 93 | call_result_r10 |
likely | r08 = thread ID, call, pops the result into r10 | CallAndPopA |
| 94 | call_pop_r08_result_r10 |
likely | Pops r08 first, then as 93 | CallActAndPopA |
| 95 | request_all |
guess | Starts list operand of unk_0x30_offset (0x14034de80), then 0x14034e4e0(…, 0) |
Unknown95; FF12 REQALL |
| 96 | jump_internal |
guess | The same case as 81 | JumpInternal |
| 97 | request_wait_all |
guess | 0x14034df10, waits (state 5) |
Unknown97; FF12 REQWAITALL |
| 98 | inc_init_tag |
guess | Peeks 3, sets bit 1 << top on a thread (0x14034d9a0) |
Unknown98; FF12 INCINITTAG |
| 99 | request_i_all |
guess | As 95 with last argument 1 | Unknown99; FF12 REQIALL |
Natives
A call opcode's u16 operand is a native's number: bits 12–15 pick one of 16 native tables, bits 0–11 the entry (0x14034d330). Only table 0 is registered: 328 entries of 0x20 bytes at 0x140C9FF80, with the count, 328, at 0x140C9FF70 (0x14046c4a0). Each entry holds up to three handlers, called with the script, the thread, a per-call work area and the stack:
| Offset | Handler | When |
|---|---|---|
| 0x00 | start |
Once, on the first try: pops the arguments. 98 natives. |
| 0x08 | update |
Every try, start's included: returns bit 0 set while the command is still running, and the VM blocks and retries the instruction later. 99 natives. |
| 0x10 | slot_0x10 |
Never set, and never read. |
| 0x18 | finish |
When done: runs the command, or sets its result. 229 natives. |
After finish, the VM pushes the result: the work area's value as an int or a float when the handler set its state byte to 2 or 3, else 0. call_result_r10 then pops it into r10; call_pop_r08 and call_pop_r08_result_r10 first pop an actor into r08. Source: 0x14034d330, 0x14046c430 (start), 0x14046c3f0 (update), 0x14046c460 (finish), 0x14034a1b0 (cases 0x58–0x5E).
exe.script_natives has one row per entry (ScriptTablesScript): native, record_address, start, update, slot_0x10 and finish (addresses, null when empty) each with its function's name, and pop_calls: the direct calls to the two pop functions in start and finish. pop_calls is the argument count only for natives that pop their arguments directly and always; it misses pops through a helper (natives 27–38) and counts optional ones (15, 16). Handlers that weren't functions in the Ghidra project (50) were made functions by the script. The same script writes the task opcode table, view exe.task_opcodes: opcode, record_address, operand_size, unk_0x04, handler, handler_name, one row per record of the 114 at 0x140694BD0.
Numbering against the PSP release
- likely: natives 0–200 keep the PSP numbering, Reborn inserted 201 and 202 (they set a value pair in a unit's record), and Reborn 203–282 are PSP 201–280. So Gibbed's names and argument counts in
psp_natives.mdapply to Rebornn= PSPnup to 200, and to Rebornn= PSPn − 2from 203 to 282. Natives 283–327 are Reborn's own. Source: for 0–200,pop_callsequals Gibbed's argument count wherever both are known, except the natives that pop through a helper or take an optional argument; shifted by 2 from 203 on, argument counts match again and use counts line up, many exactly (203 and PSP 201, 237 and 235, 249 and 247); each native's uses in Reborn's scripts are close to Gibbed's PSP counts. - likely: some natives take an optional last argument: the handler pops it only when the stack holds the full count (the talk natives 15, 16, 27–38 and 288–299, and 220–223), so a script may leave it off. Source: the handlers.
What the natives do
Each native's name, arguments (in the order a script passes them), result and what it does are in the names list, names.jsonl (../../data/exe/names.jsonl), which the toolchain reads for _native in the decoded event scripts: one row per handler function, named Native<number>_<name> (with _Start, _Update or _Finish when the native has more than one), its comment holding the rest; exe.script_natives shows the names in its *_name columns. A handler shared by several natives is named after the first, and its comment lists the others. Found by reading each native's handlers and their callees (static analysis, 2026-10-11). The words actor, current actor, bind and talk are Gibbed's, kept as guess names.
The natives come in groups (likely):
| Natives | Group |
|---|---|
| 0–12 | Waits, the end of the event (1), screen fades and the map's sepia tint (2–8), message speed |
| 13–46 | The two message windows at 0x140D518B0: talk from an actor or from the actor bound to a script thread, waiting or not |
| 47–94 | Actors: direction, animations, portraits, position, class; reading game variables and game flags (90, 91; 92 and 93, which would set them, do nothing) |
| 95–122 | Movement over the map's tiles (speed, waypoints, tiles to avoid, waits) |
| 123–166 | Binding actors to script threads, showing and hiding them, fades |
| 167–182 | The camera |
| 183–193 | Music and sound |
| 194–238 | Units in battle (AI value, wait turn, animations, falling, vanishing, status effects), the letterbox bars (203, 204), turning event skipping on and off (205, 206), map animations, markers and tile colours |
| 239–282 | Vfx in 9 slots at 0x140D51EC0, projectiles, a unit using an ability at a tile, weather (guess), a system message (268), the battle result screen; 272–274 do nothing on PC |
| 283–327 | Reborn's: voiced talk and voice playback (283–315, voice bank by the voice-language setting), music, settings queries, an achievement call (317, unused) |
- likely: each context has a skip byte (
+0xC1, read by0x14034de60): while an event is skipped, waits end at once, moves finish, and fade, camera and other durations become 0. Source: the wait, move, fade and camera handlers.
Leads
- The
guessnatives: most say what the code sets, not what it means in game; check them in game or against the scripts that call them. - Where scope 0 (
context) variable storage comes from: context+0x38, which0x14034e780clears. - What
menu_data#12holds:0x1402d0600, which starts a scene's module, picks a row of it by scene ID and game variables. - Actor, talk, message: about 250 native names use Gibbed's PSP words. The game's own words nearby are
EventObject/EventUnit(classes,Event\EventObject.cpp;0x1403224d0builds 16EventUnitof 2000 bytes and 32EventObjectof 0x130) andMessageWindow. Natives resolve an actor ID through0x140322af0on0x140D5C9B0, inside theevent/eventobject.cppregion; check that0x140D5C9B0is the object0x1403224d0builds before renaming them. - Not re-read in the R31 review: the kept native names rest on the earlier handler reading (names.jsonl
how_found); only natives 0, 47, 48, 90–93 and the interpreter were re-read. The request family (opcodes 33–41, 50, 52, 53, 95, 97–99) and returns 47/48 stayguessuntil0x14034e2e0,0x14034de80,0x14034df10,0x14046b560and0x14046c020are read. - Inconsistent native names: 33–38 (
talk_thread_*) and 294–299 (thread_actor_talk*) for the same speaker kind; 288–293 (talk,talk_2,talk_keep_open) and 27–32 (talk_window0_wait_close…); 183 and 300 bothbgm_playwith different arguments. Natives 89 and 316 disagree on what0x140CE1A34is (input state or settings). - Weather: natives 264–267 are named
weather_*as guesses; seepgrsLeads. - Opcodes 1, 2, 25, 26, 49 and 71 fall to the interpreter's default case (stop the run); 0 has size 0, so executing it would spin until the 65,536-step limit. No script uses any of them: the shipped scripts use only 29 opcodes (DuckDB over
data/game/event/**/code.jsonl, R19, 2026-10-12).