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 at block + 0x5C8 + n × 0x414
Size / stride
0x414 bytes 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

XP +threshold stat block,ability bits live stats(per frame) record stats+ level magic +summons arts +equipment unmapped name +spells tail +0x000 +0x104 +0x11C +0x13C +0x185 +0x19E +0x2A7 +0x380 0x414
One record, 0x414 bytes. Solid regions are mapped; dashed regions are unmapped or partially mapped.
SlotBaseOwner
00x80084708Vahn
10x80084B1CNoa
20x80084F30Gala
30x80085344Terra (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:

+0x104HP max / curlive
+0x108MP max / curlive
+0x10CAP max / curlive
+0x110AGL ATK UDF LDF SPD INTlive
+0x11CHP / MP maxrecord
+0x120cap = 100u16
+0x122AGL ATK UDF LDF SPD INTrecord
+0x12Estatusu16
+0x130LVu8
OffsetSizeFieldMeaning
+0x0004xp_cumulativecumulative experience - "Max Exp" target; what the level-up applier compares against the threshold
+0x0044xp_next_thresholdnext-level XP threshold - the status screen's "next" readout (note); seeded per character to reach(L2)
+0x0088header tailunmapped
+0x010228stat blockpartially mapped via the stat aggregator
+0x0F416ability_bitsOR'd into the shared ability bitfield by the stat aggregator (accessory passives)
+0x1042hp_max_liveeffective max, aggregator-rebuilt
+0x1062hp_curr_live"Infinite HP"; walk-regen bumps it, clamping at +0x104
+0x108 / +0x10A2+2mp_max_live / mp_curr_live"Max MP"; each pair is (max, cur)
+0x10C / +0x10E2+2sp_max_live / sp_curr_live"100 AP"
+0x1102agl_live"Max AGL"
+0x1122atk_live"Max ATK"
+0x1142udf_live"Max UDF"
+0x1162ldf_live"Max LDF"
+0x1182spd_live"Max SPD"
+0x11A2int_live"Max INT"; clamped at 0x3E7
+0x11C / +0x11E2+2hp_max_record / mp_max_record"Max HP" / "Max MP" record copies
+0x1202stat_cap_constantalways 100 in captured saves (note)
+0x122 .. +0x12C6×2agl/atk/udf/ldf/spd/int_recordrecord copies, same order as the live block
+0x12E2battle_status_flagsthe packed ailment word, mirrored from the battle actor (note)
+0x1301level_displayedthe displayed character level - "Level 99" target (note)
+0x1311unreadthe new-game seed inits it to 1 (sb $v0, 0x6F9($s0) at 0x800561C8) and nothing on the disc reads it. +0x132..+0x13B unmapped
+0x13C1learned_spell_countlength byte of the id list below - "Magic Slot Activator" writes 0x24 here (note)
+0x13D / +0x149 / +0x1553×12magic_group_0..2"Magic Modifier 1..12" - three copies of the slot
+0x16136spell_levelsper-spell level, parallel to the ids at +0x13D; the "All Summons Level 9" target, because summons are list entries (note)
+0x1851displayed_skill_countmenu-overlay art roster count
+0x18616displayed_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
+0x1961armor_id"Armor Modifier" / "Best Equipment"
+0x1971head_gear_id"Head Gear Modifier"
+0x1981weapon_id"Weapon Modifier"
+0x1991ra_seru_item_idthe 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
+0x19A1leg_gear_id"Leg Gear Modifier"
+0x19B .. +0x19D3accessory_1..3_id"Accessory N Modifier"; +0x19E..+0x1A6 unmapped
+0x1A7 / +0x1B716+16auto_command_string_a / _bthe battle menu's Auto replay queue, primary and secondary AP band (note); +0x1C7..+0x2A6 unmapped
+0x2A79display nameNUL-padded
+0x2B014×0x14summon_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_80047430 copies battle actor +0x16E verbatim (lhu 0x16e(actor) then sh at 0x80047680 and 0x80048040). The HUD's status element selector FUN_8002C2E4 indexes 0x80084140 + slot × 0x414; since 0x80084140 + 0x5C8 == 0x80084708, its +0x6F6 is this +0x12E, its +0x6CE is +0x106, its +0x6F8 is +0x130.
  • Level byte. Boot-confirmed via the starting-level randomizer: a New Game record with level-10 XP, stats and threshold but +0x130 == 1 displays LV 1; setting +0x130 = 10 displays 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 +0x004 showed "next level: 11195" while experience stayed 0. The u16 that appeared to move by an XP-shaped delta at +0x004 in 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_801DA2A0 reads +0x13C as the Ra-Seru-gated learned-spell count before walking the ids; the window-7 character prompt substitutes record[+0x13D + sel] (field menu).

How we know

Each offset is cross-referenced against at least two of three independent sources:

SourceAnchorWhat it proves
Per-frame stat aggregatorFUN_80042558live-stat block layout, the 0x3E7 live clamp
Ability-bitmap readerFUN_800431D0+0x0F4 ability bits OR'd into 0x80074358
Spell-list popFUN_80042DBCthe +0x13C count / +0x13D id-list pairing
Level-up applierFUN_801E9504 (overlay)record-stat writes, threshold rewrite, level byte tick, +0x120 constant
Ailment mirrorFUN_80047430, HUD selector FUN_8002C2E4+0x12E status word and the second HUD base
Save-state capturessave-state pairs around level-ups (incl. Noa / Gala four-level jumps)which bytes move on a level-up event
GameShark databasedata/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.

CheatAddress (Vahn)OffsetField
100 AP0x80084816+0x10Esp_curr_live
Max HP0x8008480C / 0E / 0x80084824+0x104 / +0x106 / +0x11Chp live max / cur / record
Max MP0x80084810 / 12 / 0x80084826+0x108 / +0x10A / +0x11Emp live max / cur / record
Max AGL .. Max INT0x80084818..22 / 0x8008482A..34+0x110..+0x11A / +0x122..+0x12Clive / record stat pairs
Level 990x80084838+0x130level_displayed
Magic Slot Activator0x80084844+0x13Clearned_spell_count
Magic Modifier 10x80084845 / 51 / 5D+0x13D / +0x149 / +0x155three magic-group copies
All Summons Level 90x80084869..86+0x161..+0x178spell_levels (first 16 list positions)
Has all Arts0x8008488D..9A+0x185..+0x192displayed_skill_count + ids
Best Equipment0x8008489E / A0 / A2+0x196 / +0x198 / +0x19Aarmor / weapon / leg gear
Activate Meta at Lv90x800848A1+0x199ra_seru_item_id (value 0x09, the final form; triggers the Meta grant)
Activate Terra / Ozma at Lv90x80084CB4 / 0x800850C9Noa +0x198 / Gala +0x199the 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 Modifier0x800848A3+0x19Baccessory_1_id
Max Exp0x80084708 / 0A+0x000 / +0x002xp_cumulative (combined u32)

Source of record: docs/formats/save-record.md.

See also