Skip to content

Operands and state

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.

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.

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”.

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.

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.

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.

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.

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 with Command_00 and starts the callee in the same slot (0x002EBF20–0x002EBF48), so the callee inherits the caller’s ten locals. The prologue chain 500/13 → 14 → 15 → 16 → 17 passes its state this way.
  • A plain start (Command_04) reads control bits from the high halfword of its first operand word (srl $s2, $v0, 0x10 at 0x002EB658). Bit 2 of that halfword copies the caller’s 0x28-byte local block into the new slot’s +0x34 (andi $v0, $s2, 4 at 0x002EB6D0, the memcpy at 0x002EB6F0); bit 3 copies the float and angle vectors at +0x60. The decompiler’s inherit_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.

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.

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.