Per-character save record Confirmed
Everything the game knows about one party member - XP, HP/MP, stats, learned arts, equipped items, magic - lives in a single 0x414-byte record in RAM. Four records sit contiguously at 0x80084708 + slot * 0x414 (Vahn / Noa / Gala / guest); this is the structure a save file preserves and the one every stat screen, level-up, and equip change reads and writes. Field offsets are pinned by a fusion of decompiled function dumps, captured save-state observations, and - an unusually good ground-truth source - the GameShark cheat database under data/cheats/: a cheat labelled “Max ATK”
that pokes one address is a free field label for that offset.
Confidence
Confirmed for every offset in the table below. Each is cross-referenced against at least two of: a Ghidra-dumped function, a captured save-state observation, or a GameShark cheat with a labelled effect.
Per-character bases
| 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) |
Layout
Note the paired stat fields: the _live copies are the effective values the aggregator rebuilds (with equipment and buffs applied), while the _record copies are the base stats the level-up system maintains. The quoted names ("Max ATK", "Infinite HP") are the GameShark cheat labels that pinned each offset.
+0x000 u32 LE xp_cumulative ; cumulative experience - "Max Exp" cheat
; target; what the level-up applier
; compares against the threshold.
+0x004 u32 LE xp_next_threshold ; next-level XP threshold - the status
; screen's "next" readout.
+0x0F4 u8[16] ability_bits ; OR'd into 0x80074358 each frame
+0x104 u16 LE hp_max_live ; effective max, aggregator-rebuilt
+0x106 u16 LE hp_curr_live ; "Infinite HP"; walk-regen bumps it
+0x108 u16 LE mp_max_live ; (each pair is (max, cur))
+0x10A u16 LE mp_curr_live ; "Max MP"
+0x10C u16 LE sp_max_live
+0x10E u16 LE sp_curr_live ; "100 AP"
+0x110 u16 LE agl_live ; "Max AGL" (live copy)
+0x112 u16 LE atk_live ; "Max ATK"
+0x114 u16 LE udf_live ; "Max UDF"
+0x116 u16 LE ldf_live ; "Max LDF"
+0x118 u16 LE spd_live ; "Max SPD"
+0x11A u16 LE int_live ; "Max INT" (LiveStats::int)
+0x11C u16 LE hp_max_record
+0x11E u16 LE mp_max_record
+0x120 u16 LE stat_cap_constant ; always 100 in captured saves
+0x122 u16 LE agl_record
+0x124 u16 LE atk_record
+0x126 u16 LE udf_record
+0x128 u16 LE ldf_record
+0x12A u16 LE spd_record
+0x12C u16 LE int_record
+0x130 u8 level_displayed ; retail displayed character level -
; "Level 99" cheat target. See note.
+0x131 u8 magic_rank_candidate ; possible separate magic-rank byte
; (unconfirmed; new-game seeds it to 1).
+0x13C u8 magic_slot_activator ; cheat sets to 0x24 to enable
+0x13D u8[12] magic_group_0 ; "Magic Modifier 1..12" hits
+0x149 u8[12] magic_group_1
+0x155 u8[12] magic_group_2
+0x161 u8[16] summon_levels ; "All Summons Level 9"
+0x185 u8 displayed_skill_count
+0x186 u8[16] displayed_skill_ids ; "Has all Arts" target
+0x196 u8 armor_id ; "Armor Modifier"
+0x197 u8 head_gear_id ; "Head Gear Modifier"
+0x198 u8 weapon_id ; "Weapon Modifier"
+0x199 u8 accessory_or_seru_lock ; "Activate Meta/Terra/Ozma at Lv9"
+0x19A u8 leg_gear_id ; "Leg Gear Modifier"
+0x19B..0x19D accessory_1..3_id ; "Accessory N Modifier"
+0x2B0..0x37F active_spell_slots[14] ; 14 x 0x14-byte runtime slots
Reconciled contradictions
+0x130 is the displayed character level
The status screen reads +0x130 as "LV" and the Level 99 GameShark cheat sets it to 0x63 (99). Boot-confirmed via the starting-level randomizer: a New Game record with level-10 cumulative experience (+0x000), level-10 stats, and the correct next-level threshold (+0x004) but +0x130 == 1 still displays LV 1, and setting +0x130 = 10 displays LV 10 - the shown level is read from +0x130 directly, not re-derived from cumulative XP. The retail level-up applier maintains the byte by incrementing it +1 per level-up event, so it can momentarily lag the XP-derived level after a rare multi-level jump; for single-level play and the new-game seed it equals the level. Whether a separate magic-rank byte lives at the adjacent +0x131 (which the new-game seed also inits to 1) is unconfirmed. The runtime accessor legaia_save::CharacterRecord::magic_rank() reads +0x130 - it is the level byte under a legacy name; the crate's level() reads +0x100, which is always zero in retail (the engine port uses it as its own internal level cell).
+0x000 is cumulative XP; +0x004 is the next-level threshold
+0x000 is the cumulative experience (the "Max Exp" cheat target, the "Experience" readout, and the value the level-up applier compares against the threshold); +0x004 is the next-level XP threshold - the "next" readout on the status screen. Confirmed live: a randomized ROM that wrote a level-10 XP value into +0x004 showed it as "next level: 11195" while experience stayed 0, leaving the derived level at 1. One divergence to be aware of: the crate accessor CharacterRecord::cumulative_xp() reads a u16 at +0x004 (empirical, from level-up captures) - the accessor and the doc's field reading have not yet been reconciled.
+0x161..+0x178 is the summon-level array, not spell levels
The prior SpellList::levels field claimed +0x161..+0x184 was a parallel "spell level" array. The All Summons Level 9 cheat stamps 0x09 into 16 bytes at +0x161..+0x178 - one byte per summon ID, not per spell. The corrected accessor in legaia_save::CharacterRecord is summon_levels().
The +0x120 cap constant
Across every captured save, +0x120 reads as 100. The level-up overlay writes this constant on every level-up event regardless of character or stat. Interpreted as the hard ceiling the record-side stats clamp against; the runtime then re-projects through FUN_80042558's 0x3E7 clamp on the live copy, so equipment / buffs can push past the record-side cap up to 999 in battle.
The legaia_save::CharacterRecord::stat_cap() / set_stat_cap() accessors read/write this +0x120 cap constant (the same field as RecordStats::cap_constant); +0x11A is the live INT stat (int_live, typed as LiveStats::int), which the engine clamps at 0x3E7 and the cheat database calls “Max INT”.
Superseded readings
- "
+0x130is Magic Rank, not character level" - the earlier reconciliation of the per-event+1tick with theLevel 99cheat. Falsified by the starting-level randomizer boot test above:+0x130is the retail displayed level. If a magic-rank byte exists it is the unconfirmed+0x131. - "The level-up logic reads
+0x004as cumulative XP" - conflated the threshold the applier reads with the cumulative XP it compares against.+0x000is cumulative XP;+0x004is the threshold.
See also
- Cheat databases - the GameShark / Mednafen cheat parser, full citation table, and runtime applier.
- RAM map - pinned globals around the per-character records.
- Level-up subsystem - the multi-frame write split during a level-up event.