Skip to content

EVD container and encoding

EVD is the bytecode format that drives cutscenes, NPC behaviour and battle setup. These pages describe the container, the instruction encoding, the execution model and the operand conventions. They are a format reference. How the shipped event files are packed on the disc, and how one event leads to the next, is the Events section.

Two independent lines of evidence were used, and they agree.

Static analysis of the game code. The debug build ships a linker .MAP with C++ symbol names, so handlers can be located by name (CRadiScript::Command_1b) rather than guessed at. Every behavioural claim cites the function it came from. Addresses are virtual addresses in the debug build; the bulk of the script engine lives in the step0_00 overlay, based at 0x001B4280.

Exhaustive round-tripping. Every EVD the game ships — 7,462 files, covering the loose event files, the packed archives and the per-character animation archives — has been decoded to a textual representation and reassembled, and the result must equal the original bytes. It does, for all 7,462, through three independent representations. 7,321 of those files are distinct by content; the rest are duplicates across archives.

That is the strongest claim here and the one everything else rests on: the container and instruction encoding are not inferred, they are proven. Any error in them would surface as a byte mismatch.

Field meanings are weaker and are marked as such. Where a trace establishes where a value is written but not what consumes it, this document says so rather than inventing a purpose. What is not established lists the gaps.

An EVD file is a flat little-endian structure. All multi-byte values are little-endian, and all offsets stored in the file are word offsets — multiply by 4 for a byte offset.

+0x00 4 bytes magic "EVD\x01" (45 56 44 01)
+0x04 u32 header flags
+0x08 u32 marker table offset, in words; 0 = no marker table
+0x0C ... command region, runs to end of file

SetScriptNumber checks only the low 24 bits of the first word against "EVD" (0x445645, at 0x002E3FC4–0x002E3FD8), so the \x01 is not part of the test.

Execution begins at +0x0C, and there is no length field and no code/data section table: reachable paths from that address are decoded as commands.

That is not the same as saying every byte after +0x0C is part of one linear command stream. Some files embed complete EVD blobs and other unreachable regions inside the command region — inline script data belonging to the stacked-start commands — so a decoder that walks linearly from +0x0C to end-of-file will desynchronise on them and invent commands that never execute.

The word at +0x04 is read as a byte by CRadiScript::SetScriptNumber when a script is started, and all four bits that occur in shipped files are consumed there confirmed:

Bit Meaning Evidence
0 Run the first StepProcess slice synchronously inside the start call, before it returns to the caller. The slot’s status byte 0 carries bit 4 while that slice runs. lbu v0, 4(evd) / andi v0, v0, 1 at 0x002E40D8; the StepProcess call at 0x002E4144
1–2 Duplicate-start scope. 0 = no check. 1 = refuse if a script with the same mode and script number is already running. 2 = refuse if one with the same character and variant is. 3 = both. A refused start sets bit 3 of the running slot’s status byte 1 (readable as event value 1102) and returns −3. 0x002E3FE8–0x002E40C8, through SearchScriptSlot (0x002E44D0)
3 Mode-2 scripts only: suppress the per-frame rebind of the default character (status byte 1 bit 4). Clear means the character is re-resolved every frame. 0x002E41CC–0x002E4218

The bits are read separately rather than as one value:

002E3FE8 lbu v0, 0x04(evd_base)
002E3FEC dsll32 v0, v0, 0x1d
002E3FF0 dsrl32 v1, v0, 0x1e ; bits 1-2: duplicate-start scope
002E3FF4 beqz v1, no_duplicate_check
002E3FF8 addiu v0, zero, 3
002E3FFC beq v1, v0, scope_both
...
002E40D8 lbu v0, 0x04(evd_base)
002E40DC andi v0, v0, 1 ; bit 0: synchronous first step
...
002E41CC lbu v0, 0x04(evd_base)
002E41D0 dsll32 v0, v0, 0x1c
002E41D4 dsrl32 v0, v0, 0x1f ; bit 3: no per-frame rebind (mode 2 only)

Observed values across all 7,462 parsed files:

Value Files Bits set
0 2,693 none
1 745 synchronous first step
2 80 duplicate scope 1
3 2,719 synchronous, duplicate scope 1
4 1 duplicate scope 2
6 201 duplicate scope 3
7 1,022 synchronous, duplicate scope 3
9 1 synchronous, no rebind
Total 7,462

If the word at +0x08 is non-zero it is a word offset to a marker table:

+0x00 u16 entry count
+0x04 u32[n] word offset of each marker, relative to the file start

CRadiScript::GetMarkerAddress(const EVDFILE_HEAD*, int) at 0x002E57D0 bounds-checks the index against the count and returns file + entry * 4 confirmed:

lw v0, 0x08(a1) ; word offset to marker table, 0 means no table
beqz v0, return_null
sll v0, v0, 2
addu v1, a1, v0 ; marker_table = evd_base + header[0x08] * 4
lhu v0, 0x00(v1) ; marker count
sltu v0, marker_index, v0
...
addiu v0, marker_index, 1
sll v0, v0, 2
addu v1, v1, v0
lw v0, 0x00(v1) ; marker target word offset
sll v0, v0, 2
addu v0, a1, v0 ; return evd_base + target * 4

All 2,797 files with a non-zero table parse cleanly under this layout.

The table is a lookup structure, not a list of entry points. Four readers walk it and none of them is a script start in the ordinary sense: the marker-seek command (control flow §3), the stop handler and the schedule-percent walk (§4), and the animation frame trigger.

Every command begins with one 32-bit header word:

31 24 23 16 15 8 7 0
+----------------+----------------+----------------+----------------+
| flags | argument byte | extra words | opcode |
+----------------+----------------+----------------+----------------+
Field Bits Meaning
opcode 0–7 Which handler runs
extra words 8–15 Count of 32-bit operand words following the header
argument byte 16–23 Passed to the handler; almost always a bitfield of options
flags 24–31 Header flags; bit 7 (0x80) is the keep-going bit

A command therefore occupies 4 + extra_words * 4 bytes.

CRadiScript::StepProcess(SCR_DATA*, int) reads exactly this confirmed:

lw a3, 0x0c(scr)
addiu v1, a3, 4
sw v1, 0x0c(scr) ; cursor advances past command header first
lw s2, 0x00(a3) ; command word
srl v1, s2, 8
andi s1, s2, 0xff ; opcode
andi s0, v1, 0xff ; extra word count
srl v1, s2, 0x10
andi a3, v1, 0xff ; third command byte

and dispatches through CRadiScript::pfnTable, a table of 12-byte entries at 0x003B2D70:

lui v0, 0x003b
addiu v0, v0, 0x2d70 ; pfnTable
sll v1, opcode, 1
addu v1, v1, opcode
sll v1, v1, 2 ; opcode * 12
addu t9, v0, v1
jal 0x00149400 ; C++ member-function pointer call helper

The handler call shape is:

handler(this, step_process_arg, scr_data, argument_byte, extra_word_count)

After the handler returns, StepProcess acts on the result:

Return Effect
1 Stop processing this script for now.
2 Set bit 31 on the command word and continue — a branch was taken, and the handler has already rewritten the cursor.
<= 0 If the cursor is now 0 the handler ended the script: return. Otherwise, for opcodes below 0xF0, advance SCR_DATA+0x0C by extra_word_count * 4.

Individual handlers can still override or modify cursor behaviour, so command body decoding must be verified per opcode.

The loop condition proves what the ubiquitous 0x80 flag byte does confirmed:

002E3498 lui v0, 0x8000
002E349C and v0, s2, v0 ; bit 31 of the just-executed command word
002E34A0 bnez v0, 0x2e33e0 ; set: execute the next command immediately
002E34A4 move v0, zero ; clear: return, resume next game update

Bit 31 — bit 7 of the flags byte — is the keep-going bit:

  • set — run the next command immediately. This is the common case.
  • clear — stop after this command and resume on the next game update, i.e. a one-frame yield.

This is why nearly every shipped command carries 0x80, and why a taken branch (handler return 2) forces the bit on: execution must continue at the jump target. Two consequences follow, and both matter to an emulator:

  • A taken branch never yields, whatever its own flag byte says.
  • A branch that is not taken keeps its own yield bit.

Other bits of the flags byte are rare and no general meaning has been established.

For opcodes >= 0xF0 the extra-word count is ignored and the command is exactly 4 bytes. The stepper consumes only the header. This is what lets those opcodes act as in-stream markers whose remaining bits carry data rather than pointing at operands — see 0xF0 markers.

It is also why Command_ff consumes only its header despite a raw word of 0xFFFFFFFF appearing after it in some files.

A script runs in a script slot. There are 48 slots of 0xE0 bytes, laid out as an array at the start of the CRadiScript object: SetScriptNumber bounds the index with slti $at, $s4, 0x30 (0x002E3E30) and computes the record as slot * 0xE0 (0x002E3E90–0x002E3E9C). GetEmptySlot (0x002E45A0) allocates one when the caller passes a negative index, and starting into a slot whose cursor is non-zero is refused with −4.

CRadiScript::FuncRun (0x002E2E90) is one game frame of script execution: it visits the slots in index order, services deferred starts, runs each slot’s wait records and then its StepProcess. What the format needs from that loop is the slot record.

Each running script has a state block of 0xE0 bytes, referred to here as SCR_DATA confirmed:

Offset Contents Evidence
+0x00 Status. Low nibble = start mode, which container the script came from (§4.2). Bit 4 = inside the synchronous first step. Bit 5 = the stop marker has already run. Bit 6 = deferred start, waiting for an event file to load. Bit 7 = has a default object name. SetScriptNumber 0x002E4118–0x002E4130; Command_f0 0x002FD338; FuncRun 0x002E2F50
+0x01 Status. Bits 0–1 = waiting state. Bit 2 = trigger-check result. Bit 3 = a duplicate start was refused this frame. Bit 4 = re-resolve the default character every frame. Bits 0–1, 2 and 3 are readable from a script as event values 1100, 1101 and 1102. 0x002E40AC–0x002E40C8, 0x002E41E0; GetEventValueForScript 0x002E4F98–0x002E4FC0
+0x02 Suspend byte, copied from the character object on rebind. Non-zero skips the slot for the frame. FuncRun 0x002E2F44, 0x002E3094
+0x04 Script number, or the pending script of a deferred start. 0x002E4134
+0x08 The EVD base pointer. 0x002E4138
+0x0C The script cursor — a pointer into the command region. 0 means the slot is free. Branches write it. 0x002E413C
+0x10, +0x12 Default character id and variant. 0x002E4140, 0x002E4148
+0x14 Default object name. SetDefaultObjectName 0x002E4330
+0x28 The float passed to a deferred start, re-issued when the file arrives. 0x002E3F94; FuncRun 0x002E2F88
+0x34 … +0x5B Ten script-local event values, ids 1000–1009. SetEventValueForScript 0x002E4E68–0x002E4E78
+0x60, +0x70 Float and angle vector slots written by the expression command. Command_14 0x002EA590
+0x80 … +0xDF Eight 12-byte trigger records the wait command installs. Waiting 0x002E80C0, stride at 0x002E8154

For a plain start, SetScriptNumber initialises the slot like this, which is what puts the first command word at EVD+0x0C confirmed:

002E41A0 and v1, a1, -0x10
002E41A4 or v1, v1, mode ; low nibble of status byte 0 = start mode
002E41A8 sb v1, 0x00(scr_data)
002E41AC sw script, 0x04(scr_data)
002E41B0 sw evd_base, 0x08(scr_data)
002E41B4 sw v0, 0x0c(scr_data) ; v0 = evd_base + 0x0c
002E41B8 sh chara, 0x10(scr_data)
002E41BC sh variant, 0x12(scr_data)

Commands that transfer control — jumps, conditional branches, marker seeks — work by writing SCR_DATA+0x0C and returning a status that tells the stepper not to advance normally.

SetScriptNumber(slot, mode, script, character, variant, float) is the one entry point for starting any script, and the mode says which container the script number indexes. GetScriptAddress (0x002E3A20) resolves it confirmed:

Mode Script number indexes Evidence
1 The event file loaded by LoadScriptFile: outer Kods member = script number, SLZ-decompressed, then inner Kods member 0. Member 1 is the script’s RMF1 message file. 0x002E3A98–0x002E3C78
2, 4 The character’s action data, kinds 2 and 0 — NPC act scripts. 0x002E3C80–0x002E3CD4
3 The current map’s nested script table (gpBackGround+0xB4). 0x002E3CD8–0x002E3D10
0xB The schedule-selection file held by the data centre (+0x48). 0x002E3D18–0x002E3D6C
0xC, 0xD The character’s action data, kinds 5 and 4, used directly as the EVD — the kick and talk reaction scripts. 0x002E3D70–0x002E3D9C

Two different id spaces are therefore involved in the commands, and conflating them is the single easiest mistake to make with this format:

  • Command_06 calls CRadiScript::LoadScriptFile. Its operand is a global event file id. Argument bit 0 forces bit 15 of that id, which selects the second event table and disk file 187 instead of 186 (0x002EB464–0x002EB47C; Events §1).
  • Command_04 and the stacked-start variant Command_01 call SetScriptNumber. Their operand is a script number inside the container the mode names — for mode 1, the event file that is already loaded.

So in event file 516, a mode-1 start with operand 5 runs script 516_05, not event file 5. The corpus is consistent with this: stacked starts only ever use values 1..24, the range of script numbers a single file holds.

Entering a script anywhere but its first command. The sixth parameter, a float, is the only way. When it is non-zero SetScriptNumber walks the marker table from index 0 and moves the cursor to just after the last consecutive 0xF0 marker of mode 2 whose 20-bit value, divided by 100, is at most the float (0x002E4220–0x002E42EC; 100.0f is the lui 0x42C8 at 0x002E42AC). Of the 43 call sites in the debug build, 41 pass 0.0; the exceptions are CCharacterPerson::ActRun, which passes an NPC’s schedule percentage, and FuncRun re-issuing a deferred start with the float the original caller gave. Every other script — every cutscene, map script and reaction — starts at +0x0C.

810_10.evd, 120 bytes, complete:

0000 45 56 44 01 03 00 00 00 00 00 00 00 10 01 00 80
0010 00 00 01 00 10 01 01 80 84 12 00 00 8A 00 01 80
0020 23 03 01 00 AC 26 00 00 00 00 00 00 01 00 00 00
0030 03 01 00 00 02 00 18 00 89 01 04 80 00 00 00 00
0040 03 01 00 00 04 00 00 00 34 03 01 80 AC 26 00 00
0050 00 07 16 00 FF FF 00 00 2F 03 01 80 AC 26 00 00
0060 FF F1 FF FF FF F1 FF FF 8A 00 00 80 10 01 00 80
0070 00 00 00 00 00 00 00 00

Header: magic, flags 3 (synchronous first step, refuse a duplicate of the same script), no marker table. Execution starts at +0x0C.

The first command word is 80 00 01 10 little-endian = 0x80000110:

  • opcode 0x10 — set config/event flags
  • extra words 0x01 — one operand follows
  • argument byte 0x00
  • flags 0x80 — keep-going set

Its operand at +0x10 is 0x00010000: flag id 0 in the low half, value in the high half. So: set flag 0 on.

The next word 0x80010110 is the same opcode with argument 0x01 and operand 0x00001284 — a run of two flags starting at 4740, cleared.

Decoded in full:

set flag 0 on
clear flags 4740..4741
set window mode (argument 0x01)
play animation 1, group 0, on the current character (0x26AC) ; yields
wait for trigger type 0x02, flags 0x0018 ; yields
start dialogue line 0x0000
wait for trigger type 0x04 ; yields
set expression on the current character
set attributes on the current character
set window mode (argument 0x00)
clear flag 0
end script ; yields

Every byte of the file is accounted for. This is the generic “talk to a signpost” event: 103 contact scripts on 48 maps launch it (Events §2).

Stated plainly, because a format document that hides its gaps causes downstream errors.

  • About 15,000 operand fields — roughly 1.1% of decoded lines — are preserved and round-trip correctly but have no established meaning. The largest groups:
    • Command_17’s battle-order payload. The record is the game’s own RADI_BTL_ORDER (it is passed to CBtlCharacterManager::MakeBtlCharacter) and its layout is known field-by-field, but only the team id and leader flag have been tied to specific readers.
    • Command_35’s character sub-object entries. The mask and per-slot structure are understood; what each sub-object type does is not.
  • Condition bases 12 and 13 (CheckCondition_18 and _19) are installed and occur in the battle-side archive scripts, but their payloads have not been traced (control flow §2.4).
  • 15 opcodes are installed but unused by any shipped script, so their operand layouts rest on handler tracing alone with no real data to check against. They are marked traced in the opcode reference.
  • Symbol coverage. Names for characters, items, maps and music come from community-compiled tables and are good; flag names cover only 271 of 8,192, so most flag ids appear as bare numbers.

Two earlier entries in this list are closed: the header flags at +0x04 (§2.1), and the second payload word of condition base 03, which turned out to be a controller-button mask rather than a flag mask (control flow §2.3).