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.

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 per-frame stat aggregator
+0x0F416ability_bitsOR'd into the shared ability bitfield by the per-frame 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)
+0x1311magic-rank candidateunconfirmed; the new-game seed inits it to 1. +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
+0x16116summon_levels"All Summons Level 9" - one byte per summon id (note); +0x179..+0x184 unmapped
+0x1851displayed_skill_countmenu-overlay art roster count
+0x18616displayed_skill_ids"Has all Arts"; head insert on item-use learn. 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"
+0x1991accessory_or_seru_lock"Activate Meta/Terra/Ozma at Lv9" - the Seru lock byte the menu reads to gate summon access
+0x19A1leg_gear_id"Leg Gear Modifier"
+0x19B .. +0x19D3accessory_1..3_id"Accessory N Modifier"; +0x19E..+0x2A6 unmapped
+0x2A79display nameNUL-padded
+0x2B014×0x14active_spell_slots[14]runtime active-spell slots (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-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..+0x178 is the summon-level array, not spell levels

One byte per summon id, stamped 0x09 by the All Summons Level 9 cheat. Any per-spell levels sit in the magic-group window at +0x13D..+0x160. Accessor summon_levels(); the older SpellList::levels is kept for compatibility only.

+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().

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).
History: superseded readings
  • "+0x130 is Magic Rank, not character level" - reconciled the per-event +1 tick with the Level 99 cheat the wrong way round; falsified by the starting-level boot test. If a magic-rank byte exists it is the unconfirmed +0x131.
  • "The level-up logic reads +0x004 as cumulative XP" - conflated the threshold the applier reads with the XP it compares against.
  • "+0x161..+0x184 is a parallel spell-level array" - it is the 16-summon level array.

The falsified-reading catalogue is do-not-re-walk.

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 capturescapture_observations::char_level_up, levelup::observations::{noa,gala}_4_level_jumpwhich 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..+0x178summon_levels
Has all Arts0x8008488D..9A+0x185..+0x192displayed_skill_count + ids
Best Equipment0x8008489E / A0 / A2+0x196 / +0x198 / +0x19Aarmor / weapon / leg gear
Activate Meta at Lv90x800848A1+0x199accessory_or_seru_lock
Accessory 1 Modifier0x800848A3+0x19Baccessory_1_id
Max Exp0x80084708 / 0A+0x000 / +0x002xp_cumulative (combined u32)

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

See also