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

SlotBaseOwner
00x80084708Vahn
10x80084B1CNoa
20x80084F30Gala
30x80085344Terra (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
  • "+0x130 is Magic Rank, not character level" - the earlier reconciliation of the per-event +1 tick with the Level 99 cheat. Falsified by the starting-level randomizer boot test above: +0x130 is the retail displayed level. 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 cumulative XP it compares against. +0x000 is cumulative XP; +0x004 is 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.