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.
1. Provenance
Section titled “1. Provenance”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.
2. Container layout
Section titled “2. Container layout”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 fileSetScriptNumber 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.
2.1 Header flags
Section titled “2.1 Header flags”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, 0x1d002E3FF0 dsrl32 v1, v0, 0x1e ; bits 1-2: duplicate-start scope002E3FF4 beqz v1, no_duplicate_check002E3FF8 addiu v0, zero, 3002E3FFC 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, 0x1c002E41D4 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 |
2.2 Marker table
Section titled “2.2 Marker table”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 startCRadiScript::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 tablebeqz v0, return_nullsll v0, v0, 2addu v1, a1, v0 ; marker_table = evd_base + header[0x08] * 4lhu v0, 0x00(v1) ; marker countsltu v0, marker_index, v0...addiu v0, marker_index, 1sll v0, v0, 2addu v1, v1, v0lw v0, 0x00(v1) ; marker target word offsetsll v0, v0, 2addu v0, a1, v0 ; return evd_base + target * 4All 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.
3. Instruction encoding
Section titled “3. Instruction encoding”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, 4sw v1, 0x0c(scr) ; cursor advances past command header firstlw s2, 0x00(a3) ; command wordsrl v1, s2, 8andi s1, s2, 0xff ; opcodeandi s0, v1, 0xff ; extra word countsrl v1, s2, 0x10andi a3, v1, 0xff ; third command byteand dispatches through CRadiScript::pfnTable, a table of 12-byte entries at
0x003B2D70:
lui v0, 0x003baddiu v0, v0, 0x2d70 ; pfnTablesll v1, opcode, 1addu v1, v1, opcodesll v1, v1, 2 ; opcode * 12addu t9, v0, v1jal 0x00149400 ; C++ member-function pointer call helperThe handler call shape is:
handler(this, step_process_arg, scr_data, argument_byte, extra_word_count)3.1 Handler return values
Section titled “3.1 Handler return values”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.
3.2 The keep-going bit
Section titled “3.2 The keep-going bit”The loop condition proves what the ubiquitous 0x80 flag byte does
confirmed:
002E3498 lui v0, 0x8000002E349C and v0, s2, v0 ; bit 31 of the just-executed command word002E34A0 bnez v0, 0x2e33e0 ; set: execute the next command immediately002E34A4 move v0, zero ; clear: return, resume next game updateBit 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.
3.3 The 0xF0 exception
Section titled “3.3 The 0xF0 exception”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.
4. Execution model
Section titled “4. Execution model”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.
4.1 SCR_DATA — the slot record
Section titled “4.1 SCR_DATA — 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, -0x10002E41A4 or v1, v1, mode ; low nibble of status byte 0 = start mode002E41A8 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 + 0x0c002E41B8 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.
4.2 Script sources, and the two id spaces
Section titled “4.2 Script sources, and the two id spaces”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_06callsCRadiScript::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_04and the stacked-start variantCommand_01callSetScriptNumber. 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.
5. Worked example
Section titled “5. Worked example”810_10.evd, 120 bytes, complete:
0000 45 56 44 01 03 00 00 00 00 00 00 00 10 01 00 800010 00 00 01 00 10 01 01 80 84 12 00 00 8A 00 01 800020 23 03 01 00 AC 26 00 00 00 00 00 00 01 00 00 000030 03 01 00 00 02 00 18 00 89 01 04 80 00 00 00 000040 03 01 00 00 04 00 00 00 34 03 01 80 AC 26 00 000050 00 07 16 00 FF FF 00 00 2F 03 01 80 AC 26 00 000060 FF F1 FF FF FF F1 FF FF 8A 00 00 80 10 01 00 800070 00 00 00 00 00 00 00 00Header: 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 onclear flags 4740..4741set window mode (argument 0x01)play animation 1, group 0, on the current character (0x26AC) ; yieldswait for trigger type 0x02, flags 0x0018 ; yieldsstart dialogue line 0x0000wait for trigger type 0x04 ; yieldsset expression on the current characterset attributes on the current characterset window mode (argument 0x00)clear flag 0end script ; yieldsEvery 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).
6. What is not established
Section titled “6. What is not established”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 ownRADI_BTL_ORDER(it is passed toCBtlCharacterManager::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
12and13(CheckCondition_18and_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
tracedin 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).