Per-character save record Confirmed
Open the status screen and everything you see about Vahn - level, HP, the eight stats, his
learned magic, what he is wearing - is one block of RAM: a 0x414-byte record per party member,
four of them side by side. Level-ups write into it, equipping writes into it, and saving to a memory card copies
it out verbatim. Its field offsets are pinned by an unusually good ground-truth source: a GameShark cheat
labelled "Max ATK" that pokes one address is a free field label for that offset.
At a glance
- In the game
- the status / equip / magic screens, level-ups, and every memory-card save
- Magic / marker
- none - a fixed RAM layout; the save block is a verbatim dump of it
- Lives in
- RAM
0x80084708 + slot × 0x414(slots 0..3); on the card atblock + 0x5C8 + n × 0x414 - Size / stride
0x414bytes per record, 4 contiguous records- Parser
legaia_save::CharacterRecord(typed accessors + round-trip);Party::from_retail_sc_block- Confidence
- Confirmed for every offset in the layout table - each cross-referenced against at least two of: a Ghidra-dumped function, a captured save-state observation, a labelled GameShark cheat
- Used by
- level-up, field menu, save screen, battle formulas
The record and where it lives
0x414 bytes. Solid regions are mapped; dashed regions are unmapped or partially mapped.| Slot | Base | Owner |
|---|---|---|
| 0 | 0x80084708 | Vahn |
| 1 | 0x80084B1C | Noa |
| 2 | 0x80084F30 | Gala |
| 3 | 0x80085344 | Terra (the New Game template's 4th roster entry; never a savable battle-party member) |
Slot 3's footprint runs into the global story-flag bitmap and the inventory, so only its leading fields are exclusive; the tail aliases the globals. Benign - Terra is never saved as an active member.
On the memory card
The save block dumps this region verbatim: record n sits at block + 0x5C8 + n×0x414.
"Verbatim" is true of the bytes, not the whole block: editing a record also invalidates the additive checksum at
block + 0x1FFC, which retail compares before accepting the save - a stale word captions the slot
"Damaged data." and the record never reaches RAM. legaia_save::card::restamp_sc_block_checksum is the
fix-up; see the save screen.
Layout
Stats come in pairs: the _live copies are the effective values the aggregator rebuilds every frame
(equipment and buffs applied), while the _record copies are the base stats the level-up system
maintains. The quoted names are the GameShark cheat labels that pinned each offset.
The stat band at +0x104, where most cheats and screens land:
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x000 | 4 | xp_cumulative | cumulative experience - "Max Exp" target; what the level-up applier compares against the threshold |
+0x004 | 4 | xp_next_threshold | next-level XP threshold - the status screen's "next" readout (note); seeded per character to reach(L2) |
+0x008 | 8 | header tail | unmapped |
+0x010 | 228 | stat block | partially mapped via the stat aggregator |
+0x0F4 | 16 | ability_bits | OR'd into the shared ability bitfield by the stat aggregator (accessory passives) |
+0x104 | 2 | hp_max_live | effective max, aggregator-rebuilt |
+0x106 | 2 | hp_curr_live | "Infinite HP"; walk-regen bumps it, clamping at +0x104 |
+0x108 / +0x10A | 2+2 | mp_max_live / mp_curr_live | "Max MP"; each pair is (max, cur) |
+0x10C / +0x10E | 2+2 | sp_max_live / sp_curr_live | "100 AP" |
+0x110 | 2 | agl_live | "Max AGL" |
+0x112 | 2 | atk_live | "Max ATK" |
+0x114 | 2 | udf_live | "Max UDF" |
+0x116 | 2 | ldf_live | "Max LDF" |
+0x118 | 2 | spd_live | "Max SPD" |
+0x11A | 2 | int_live | "Max INT"; clamped at 0x3E7 |
+0x11C / +0x11E | 2+2 | hp_max_record / mp_max_record | "Max HP" / "Max MP" record copies |
+0x120 | 2 | stat_cap_constant | always 100 in captured saves (note) |
+0x122 .. +0x12C | 6×2 | agl/atk/udf/ldf/spd/int_record | record copies, same order as the live block |
+0x12E | 2 | battle_status_flags | the packed ailment word, mirrored from the battle actor (note) |
+0x130 | 1 | level_displayed | the displayed character level - "Level 99" target (note) |
+0x131 | 1 | unread | the new-game seed inits it to 1 (sb $v0, 0x6F9($s0) at 0x800561C8) and nothing on the disc reads it. +0x132..+0x13B unmapped |
+0x13C | 1 | learned_spell_count | length byte of the id list below - "Magic Slot Activator" writes 0x24 here (note) |
+0x13D / +0x149 / +0x155 | 3×12 | magic_group_0..2 | "Magic Modifier 1..12" - three copies of the slot |
+0x161 | 36 | spell_levels | per-spell level, parallel to the ids at +0x13D; the "All Summons Level 9" target, because summons are list entries (note) |
+0x185 | 1 | displayed_skill_count | menu-overlay art roster count |
+0x186 | 16 | displayed_skill_ids | "Has all Arts"; item-use learn does an ordered insert, keeping the list sorted ascending by id (the applier arm at 0x80041FB4, not a head insert). Fifteen arts per character, so the 16th slot +0x195 is never reached - the patcher's --show-super-arts keeps its performed-Super mask there |
+0x196 | 1 | armor_id | "Armor Modifier" / "Best Equipment" |
+0x197 | 1 | head_gear_id | "Head Gear Modifier" |
+0x198 | 1 | weapon_id | "Weapon Modifier" |
+0x199 | 1 | ra_seru_item_id | the Ra-Seru equipment byte for Vahn and Gala; Noa's is one byte lower, at +0x198. "Activate Meta at Lv9" writes item 0x09 (final-form Ra-Seru Meta) here; the grant scan in FUN_80042558 then inserts spell 0x9E |
+0x19A | 1 | leg_gear_id | "Leg Gear Modifier" |
+0x19B .. +0x19D | 3 | accessory_1..3_id | "Accessory N Modifier"; +0x19E..+0x1A6 unmapped |
+0x1A7 / +0x1B7 | 16+16 | auto_command_string_a / _b | the battle menu's Auto replay queue, primary and secondary AP band (note); +0x1C7..+0x2A6 unmapped |
+0x2A7 | 9 | display name | NUL-padded |
+0x2B0 | 14×0x14 | summon_stash_slots[14] | stash slots: the level (+5) and u32 counter (+1..+4) of an equipment-granted summon while its item is unequipped; code addresses slots 0..4 only (active_spell_slot() accessor); +0x380..+0x413 unmapped |
Field notes
+0x12E is the battle status word
The halfword mirrors the battle actor's packed ailment flags, copied verbatim every frame a party actor
ticks - the record carries the live in-battle condition. The battle HUD reads the same halfword through a
second base (details in How we know); the per-bit map is on the
accessory-passive page, engine mirror
engine-battle-vm::status_effects::display_flags.
+0x130 is the displayed character level
The status screen reads it as "LV" directly; the shown level is not re-derived from XP. The
level-up applier bumps it once per level-up event, so it can momentarily lag after a rare
multi-level jump. The accessor CharacterRecord::magic_rank() reads this byte - the level byte
under a legacy name; level() reads +0x100, always zero in retail.
+0x000 is cumulative XP; +0x004 is the next-level threshold
+0x000 is the "Experience" readout and the value the applier compares; +0x004 is
the "next" readout, rewritten on every level change. The starting-level randomizer seeds +0x000,
not +0x004.
+0x13C is the learned-spell count - which is why the cheat works
The database calls it "Magic Slot Activator", but two menu-overlay readers treat it as a plain length byte
for the id list at +0x13D. "Activating" the list is just declaring it 36 entries long. Treat it as
a count when writing: a value past the real id run displays uninitialised slots rather than granting spells.
+0x161.. is the per-spell level array, parallel to the ids at +0x13D..
+0x161 + i is the level of the spell whose id is at +0x13D + i, with the u32 counter at
+0x08 + i * 4 as the third parallel array: the insert routine FUN_800432BC shifts and writes all
three together, and FUN_80042DBC compacts all three together. Summons are ordinary entries of this list
(ids 0x99..0xA0), which is why the All Summons Level 9 cheat, stamping 0x09 across
+0x161.., raises them: it raises every listed spell. There is no separate per-summon-id level array.
The summon_levels() accessor reads the first 16 bytes of this array.
+0x120 is the record-stat cap constant
Always 100: the hard ceiling record-side stats clamp against, rewritten by the level-up
overlay on every level-up. The live copies then clamp at 0x3E7, so equipment and buffs can push
past it up to 999 in battle. Accessors stat_cap() / set_stat_cap().
+0x1A7 / +0x1B7 are the Auto command strings
The battle command menu's Auto replays the 16-byte action queue the player last confirmed, parked here between turns. Which band is used is decided by the actor's action gauge (base gauge strictly below live gauge takes the primary band +0x1A7). The two legs are asymmetric: the primary read falls back to +0x1B7 when its head byte is zero, the secondary read zero-fills instead, and the write-back overwrites exactly one band, only for a live actor using the attack/arts command. Readers FUN_801DA34C / FUN_801DA59C in the battle overlay address them off the live-state window (0x80084140 + slot × 0x414 + 0x76F / +0x77F), which sits 0x5C8 below the record base.
Evidence for each note
- Status word.
FUN_80047430copies battle actor+0x16Everbatim (lhu 0x16e(actor)thenshat0x80047680and0x80048040). The HUD's status element selectorFUN_8002C2E4indexes0x80084140 + slot × 0x414; since0x80084140 + 0x5C8 == 0x80084708, its+0x6F6is this+0x12E, its+0x6CEis+0x106, its+0x6F8is+0x130. - Level byte. Boot-confirmed via the starting-level randomizer: a New Game record with level-10 XP, stats and threshold but
+0x130 == 1displays LV 1; setting+0x130 = 10displays LV 10. The captured Noa / Gala four-level jumps bumped the byte once. - XP vs threshold. A randomized ROM that wrote a level-10 XP value into
+0x004showed "next level: 11195" while experience stayed 0. The u16 that appeared to move by an XP-shaped delta at+0x004in the level-up triplets is the low half of the threshold word, which the applier rewrites on every level change. - Spell count. The per-character list screen
FUN_801DA2A0reads+0x13Cas the Ra-Seru-gated learned-spell count before walking the ids; the window-7 character prompt substitutesrecord[+0x13D + sel](field menu).
How we know
Each offset is cross-referenced against at least two of three independent sources:
| Source | Anchor | What it proves |
|---|---|---|
| Per-frame stat aggregator | FUN_80042558 | live-stat block layout, the 0x3E7 live clamp |
| Ability-bitmap reader | FUN_800431D0 | +0x0F4 ability bits OR'd into 0x80074358 |
| Spell-list pop | FUN_80042DBC | the +0x13C count / +0x13D id-list pairing |
| Level-up applier | FUN_801E9504 (overlay) | record-stat writes, threshold rewrite, level byte tick, +0x120 constant |
| Ailment mirror | FUN_80047430, HUD selector FUN_8002C2E4 | +0x12E status word and the second HUD base |
| Save-state captures | save-state pairs around level-ups (incl. Noa / Gala four-level jumps) | which bytes move on a level-up event |
| GameShark database | data/cheats/ | a labelled (addr, value) write per named field |
Cheat-database citation table (address per field)
Vahn's addresses; Noa (+0x414) and Gala (+0x828) shift each by one stride.
| Cheat | Address (Vahn) | Offset | Field |
|---|---|---|---|
| 100 AP | 0x80084816 | +0x10E | sp_curr_live |
| Max HP | 0x8008480C / 0E / 0x80084824 | +0x104 / +0x106 / +0x11C | hp live max / cur / record |
| Max MP | 0x80084810 / 12 / 0x80084826 | +0x108 / +0x10A / +0x11E | mp live max / cur / record |
| Max AGL .. Max INT | 0x80084818..22 / 0x8008482A..34 | +0x110..+0x11A / +0x122..+0x12C | live / record stat pairs |
| Level 99 | 0x80084838 | +0x130 | level_displayed |
| Magic Slot Activator | 0x80084844 | +0x13C | learned_spell_count |
| Magic Modifier 1 | 0x80084845 / 51 / 5D | +0x13D / +0x149 / +0x155 | three magic-group copies |
| All Summons Level 9 | 0x80084869..86 | +0x161..+0x178 | spell_levels (first 16 list positions) |
| Has all Arts | 0x8008488D..9A | +0x185..+0x192 | displayed_skill_count + ids |
| Best Equipment | 0x8008489E / A0 / A2 | +0x196 / +0x198 / +0x19A | armor / weapon / leg gear |
| Activate Meta at Lv9 | 0x800848A1 | +0x199 | ra_seru_item_id (value 0x09, the final form; triggers the Meta grant) |
| Activate Terra / Ozma at Lv9 | 0x80084CB4 / 0x800850C9 | Noa +0x198 / Gala +0x199 | the Ra-Seru byte, but the values written (0x12, 0x1B) are an unnamed item gap and the Ra-Seru Blade, not the final forms 0x11 / 0x19, so neither reaches the grant scan |
| Accessory 1 Modifier | 0x800848A3 | +0x19B | accessory_1_id |
| Max Exp | 0x80084708 / 0A | +0x000 / +0x002 | xp_cumulative (combined u32) |
Source of record: docs/formats/save-record.md.