Operands and state
1. The argument byte
Section titled “1. The argument byte”Bits 16–23 of the command header are handed to the handler as a bitfield. There is no format-wide meaning; each handler defines its own. Two patterns recur often enough to be worth stating.
1.1 Bit 0 as a “character follows” gate
Section titled “1.1 Bit 0 as a “character follows” gate”On the large family of character commands, argument bit 0 selects where the
character comes from:
- set — the next operand word is a character selector.
- clear — no selector word is present; the handler uses the script’s default
character from
SCR_DATA+0x10/+0x12.
It is a presence flag, not an option: it says whether an operand exists. Getting it wrong desynchronises every following command in the file, which is why it is one of the first things a decoder has to get right.
1.2 Low bits as payload gates
Section titled “1.2 Low bits as payload gates”Several handlers use the low argument bits to say which optional operands
follow, in a fixed order. Command_1c is a compact example: bit 0 gates a
context value, bit 1 a stand-position id, and bits 2 and 3 position and
posture vectors.
1.3 Operands masked before the engine sees them
Section titled “1.3 Operands masked before the engine sees them”Some argument bits are read in the delay slot of a branch, meaning they take
effect regardless of which way the branch goes. Command_06, Command_40 and
Command_72 all mask their operand this way, discarding the high half before
the engine ever sees it confirmed. A decoder must preserve those
high bits to round-trip, but they cannot change behaviour.
2. Character operands
Section titled “2. Character operands”A character operand is a 32-bit selector word:
31 24 23 16 15 0+--------------+--------------+------------------------------+| (unused) | variant | character id |+--------------+--------------+------------------------------+The low 16 bits are the id. Byte 2 is a secondary class/variant input passed
alongside the id. Values below 0x26AC are literal character ids, and 0 means
“the script’s default character”.
2.1 Abstraction codes
Section titled “2.1 Abstraction codes”Values from 0x26AC up are abstraction codes resolved at run time, and must
not be looked up in a character table. Two functions do the resolving:
GetAbstractionCharacterNumber at 0x002C0BA0 (a 12-entry jump table at
0x3A1160 covering 0x26AC–0x26B7) and
CRadiScript::GetAbstractionCharacterNumber at 0x002E5470 (a 22-entry table
at 0x3A38B0 covering 0x26DE–0x26F3, delegating anything lower to the
first) confirmed.
| Codes | Resolves to |
|---|---|
0x26AC |
The current / controlling character — the hero — from the config block’s halfword at +0x830 (0x002C0BF8–0x002C0C08) |
0x26AD–0x26B1 |
Party slots 1–5, halfwords at +0x834 + 4N (0x002C0C10–0x002C0C54) |
0x26B2 |
The current talk speaker: entry [gpTalk+0x1B9] of the talk object’s speaker table at +0x190, or 0 when no speaker is active (0x002C0C58–0x002C0CF8) |
0x26B3–0x26B7 |
Scans over the party slots. Rarely used; meanings not established. |
0x26DE–0x26E0 |
An equipment-based lookup via CCharaEquipmentManager::GetEquipedCharacterCls |
0x26E1–0x26EA |
The character held in event value 1000+N |
0x26EB–0x26F1 |
Jump to SetError; reserved / unimplemented |
0x26F2–0x26F3 |
The default character, resolved through the global table |
Usage counts across the sampled corpus show how lopsided this is in practice:
| Code | Uses | Meaning |
|---|---|---|
0x26AC |
25,989 | current active / controlling character |
0x26AD |
519 | party slot 1 |
0x26AE |
730 | party slot 2 |
0x26AF |
730 | party slot 3 |
0x26B0 |
730 | party slot 4 |
0x26B1 |
178 | party slot 5 |
The slot run is five wide, ending at 0x26B1. Slots 2–4 share an identical
count, which is what a uniform slot series looks like; slot 1 and slot 5 differ
because the first is normally the player and the last is often unfilled.
2.2 The 0x26E1 block
Section titled “2.2 The 0x26E1 block”This block is worth understanding because it is heavily used. The handler reads
SCR_DATA + 0x34 + 4N, and SetEventValueForScript maps event value ids
0x3E8–0x3F1 (1000–1009) onto exactly those words. So the code means
“whichever character that event value currently holds”.
0x26EA — event value 1009 — is the second most common character operand in the
entire game, behind only “current character”: scripts stash a character in value
1009 and then act on it.
3. Values and flags
Section titled “3. Values and flags”Three distinct stores of state are addressable from a script. The first two
live in one place: a 0x870-byte config block that CRadiApp::Startup
allocates (0x002D1C70–0x002D1CB0) and whose pointer sits at 0x003B2664
(CRadiApp+0x164). That block is what a save file holds — see
Events §3 for what is restored on load.
3.1 Config / event flags
Section titled “3.1 Config / event flags”8,192 boolean flags, a bit array at +0x10–+0x40F of the config block,
read and written through CRadiApp::GetConfigEventFlag (0x0028C860) and
SetConfigEventFlag (0x0028C8A0): word (flag >> 5) * 4, bit flag & 0x1F,
bounded at flag < 0x2000 confirmed. Commands can operate on a
run of consecutive flags, treating them as the bits of a small integer, LSB
first — so a single operand can carry a small number spread across several flag
ids, and a condition can test one.
3.2 Event values
Section titled “3.2 Event values”32-bit script variables. The id space is not one flat array confirmed:
| Id | Storage | Evidence |
|---|---|---|
0 |
Global value 0, reloaded from Random() on every read. |
GetEventValueForScript 0x002E4EC4–0x002E4EE8 |
1–255 |
Global, config_block + 0x410 + id * 4. |
0x002E4EA8–0x002E4EC0; writes SetEventValueForScript 0x002E4E18–0x002E4E38 |
256–999 |
Nothing. Reads return 0; writes are ignored. |
the sltiu 0x100 guards above |
1000–1009 |
Script-local, SCR_DATA + 0x34 + (id − 1000) * 4. |
0x002E4F14–0x002E4F90; writes 0x002E4E40–0x002E4E78 |
1100, 1101, 1102 |
Read-only slot status: status byte 1 bits 0–1 (the waiting state), bit 2 (the last trigger check), bit 3 (a duplicate start was refused). | 0x002E4EF4–0x002E4FC0 |
| anything else | Reads return 0; writes are ignored. |
This is why 1000–1009 appear constantly — they are scratch registers — and why a sibling script does not see them: they live in the running slot, not the config block. Two mechanisms make them look shared:
- A stacked start (
Command_01) ends the current slot withCommand_00and starts the callee in the same slot (0x002EBF20–0x002EBF48), so the callee inherits the caller’s ten locals. The prologue chain500/13 → 14 → 15 → 16 → 17passes its state this way. - A plain start (
Command_04) reads control bits from the high halfword of its first operand word (srl $s2, $v0, 0x10at0x002EB658). Bit 2 of that halfword copies the caller’s0x28-byte local block into the new slot’s+0x34(andi $v0, $s2, 4at0x002EB6D0, thememcpyat0x002EB6F0); bit 3 copies the float and angle vectors at+0x60. The decompiler’sinherit_values=1.
One global value is written by a command rather than a script. Command_19,
the inventory command, always ends by storing its result in global event
value 17 (sw $s2, 0x454($v1) at 0x002E97B4; 0x454 = 0x410 + 17 * 4): the
number of items actually added or removed, 0 for a query that found the item
or a delete-all, −2 for a query that did not, −1 when the character has no
personal record. A script that tests value 17 immediately after
change_inventory is checking that the item really went in, not reading a
story value.
3.3 Character properties
Section titled “3.3 Character properties”Reached through per-domain dispatch tables of property accessor functions, recovered by emulating the static initialisers confirmed:
| Table | Address | Entries | Symbols |
|---|---|---|---|
CRadiApp::pfnParamTbl |
0x003B0E30 |
14 | SystemParaCode_000..013 |
CCharacterManager::pfnParamTbl |
0x003B0FE0 |
75 | CharaParaCode_000..074 |
CItemManager::pfnParamTbl |
0x0035BC80 |
— | not yet recovered |
They cover HP, EXP, money, status and so on. Character entries 4, 5 and 73 all
point at the same stub (0x002A0FD0), so those property codes are unused —
which is a useful validity check on any decoded operand.
Every character accessor starts from the person record
[[gpCharaManager+0x54]+0x20] + id * 0x370, and most of the ones scripts read
go on to +0x0C of it, the CPersonalData sub-record. Offsets quoted for
these properties are into that sub-record.
3.4 Property 0x1D: storage the game never reads
Section titled “3.4 Property 0x1D: storage the game never reads”One property is worth calling out because it is not engine state at all.
Property 0x1D dispatches to CCharacterManager::CharaParaCode_029
(0x0029EED0), which reads and writes one byte at +0x82 of the character’s
CPersonalData.
Scanning every byte, halfword and word access across Main.elf, step0_00,
step1_02 and step2_00 finds no other reader of that byte. The only other
reference in the whole binary is CCharacterPerson::ReleaseScheduleList
(0x002B3760) zeroing it — together with the neighbouring +0x74, +0x76,
+0x78, +0x7C and +0x80 — when a character’s schedule list is released. The
engine never acts on its value; it only wipes it.
The corpus shows scripts using it as per-character flags, and using it heavily:
| Operation | Uses |
|---|---|
| Set the flag | 2,769 |
| Clear it | 795 |
| Read it into an event value to test | 114 |
That is the whole lifecycle, written and read entirely by event scripts. It is
general-purpose per-character storage rather than a piece of engine state with a
hidden meaning. The neighbouring bytes +0x7C–+0x7E, +0x81 and +0x83
have their own property codes and the same shape; the NPC scripts test them as
conditions, and what each one means is not established.
4. Symbol coverage
Section titled “4. Symbol coverage”Id-to-name tables cover 2,519 id-to-name entries across seven domains. Ids remain authoritative — a name is a convenience, not part of the format.
| Domain | Entries |
|---|---|
| character | 382 |
| item | 671 |
| location | 886 |
| BGM | 133 |
| skill | 39 |
| event | 137 |
| flag | 271 |
| Total | 2,519 |
That figure counts entries, not globally unique display strings.
Spot checks against ids taken from the corpus: character 0x0001 Jack, 0x0042
Miranda, 0x00D3 Monki; BGM 0x0037 Gratifying Guest; location 0x001F
Dawnbay Diner and 0x00A0 Radiata Castle Exterior.
Coverage is uneven. Flags are sparse by nature — 271 of 8,192 carry a
description — and the event table names every one of the 107 shipped event
files but not their individual scripts. Item coverage is good at 671 entries,
including per-category ids such as 0x0153.