New-game starting-party table Confirmed
Pick NEW GAME on the title screen and Vahn wakes up in Rim Elm at level 1 with 180 HP, five Healing Leaves and 500 gold. The stats and names come from a tiny static table in the executable - four 26-byte rows, one each for Vahn, Noa, Gala and Terra - that a seed routine expands into the live per-character records. The starting bag and the gold are not table data but literals baked into that routine's code. Both matter to modders: the starting-level and starting-bag options edit exactly these bytes.
At a glance
- In the game
- The moment NEW GAME is confirmed - the party's opening stats, names, bag and gold
- Where
- Table at SCUS VA
0x80078C4C; bag and world-state seeds are instruction immediates inFUN_80034A6C/FUN_800560B4 - Size
- 4 records × 26 bytes (8 × u16 stats + 10-byte name)
- Expands into
- The live save records at
0x80084708 + n × 0x414 - Parser
legaia_asset::new_game(StartingParty,StartingInventory,new_game_seed_words,seed_live_records); CLIasset new-game <SCUS> [--json]- Confidence
- Confirmed - byte-search of the executable cross-validated against a live
town01save state; seed stores read off the disassembly - Used by
- Boot, Level-up, Randomizer
What a New Game is made of
Confirming NEW GAME launches the field overlay with a fresh game state built from four sources. Only the first is a table.
StartingParty(id, count) slot, Healing Leaf ×5, written into the owned-item array.StartingInventorynew_game_seed_wordstown01 (Rim Elm), the default map-name buffer. A developer initmap.txt override exists but retail boots with it gated off.engine-core::new_gameAt a true New Game only Vahn has joined; the other rows are the templates used as each character is introduced.
Record layout
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | 2 | hp_max | also the starting HP |
+2 | 2 | mp_max | also the starting MP |
+4 | 2 | agl | lands in the live agility cells (+0x110 current, +0x122 max) - not the cap cells, which are a literal 100 |
+6 | 2 | atk | |
+8 | 2 | udf | upper / physical defence |
+10 | 2 | ldf | lower / magical defence |
+12 | 2 | spd | turn-order initiative seed |
+14 | 2 | intel | |
+16 | 10 | display name | NUL-padded |
Decoded values
| Slot | Name | HP | MP | AGL | ATK | uDEF | lDEF | SPD | INT |
|---|---|---|---|---|---|---|---|---|---|
| 0 | Vahn | 180 | 20 | 100 | 24 | 16 | 12 | 19 | 9 |
| 1 | Noa | 150 | 10 | 120 | 21 | 13 | 11 | 30 | 3 |
| 2 | Gala | 210 | 40 | 80 | 30 | 43 | 30 | 15 | 20 |
| 3 | Terra | 400 | 200 | 200 | 45 | 20 | 17 | 45 | 25 |
History: the “+4 seeds the cap” reading
A Vahn-only save shows 100 in the agility cells, the cap cells and the spirit gauge alike, which once read as the +4 field fanning out to all of them. The seed loop's disassembly stores a literal 100 into the cap cells for every slot; Noa (120) and Gala (80) separate the two. See the cap constant.
Starting bag and world state (code literals)
The new-game data-init routine holds the save-context base in a register for its whole body and writes fixed values at offsets from it. The bag is one (id, count) pair in the owned-item array at SC + 0x1818 - a single ordered list shared by every item category, which the menu merely filters into Items / Goods / Key tabs. A seeded slot can therefore hold any id, which is how the randomizer's starting-bag toggles seed keys and accessories.
| SC offset | Width | Field | Value |
|---|---|---|---|
+0x1818 / +0x1819 | 1 + 1 | bag slot 0: item id, count | 0x77 Healing Leaf, 5 |
+0x454 | 1 | party byte | 3 |
+0x457 / +0x458 | 1 | party bytes | 0 |
+0x459 .. +0x45B | 1 | party-slot bytes | 1 / 2 / 3 |
+0x45C | 4 | gold | 0x1F4 = 500 |
+0x460 / +0x464 / +0x470 / +0x478 | 4 | counters | 0 |
+0x590 .. +0x59C | 4 | words | 0x44 / 0x21 / 0x10 / 0x48 |
+0x1618 .. +0x1817 | 0x200 | story flags | cleared after the bag write - the Door-of-Wind warp mask at +0x161C is inside, so any warp preset is applied after the clear |
Widths come from the store opcodes (sb / sh / sw), not from the decompiler's globals naming. StartingInventory decodes the bag by replaying the routine's register-sourced stores, so it reads back either the retail byte pair or the randomizer's packed halfword (id | count << 8).
Details: the seed instruction shapes
| Op | Top 16 bits | Meaning |
|---|---|---|
addiu $v0, $zero, imm | 0x2402 | load constant into the source register |
sb $v0, off($s0) | 0xA202 | byte store at SC + off |
sh $v0, off($s0) | 0xA602 | halfword store at SC + off |
The bag write is four instructions at 0x80034B0C. A six-instruction loop after it zeroes the 512 bytes below the bag, but both callers of the routine memset the whole SC[0..0x1A18) first, so that loop is redundant - the reclaimable region is 10 instructions (40 bytes) from 0x80034B04. Three further absolute stores (0x80073EF4 = 0xE40, 0x80073EF8 = 0x2DC0, 0x80073EFC = 0) are not save-context cells.
Starting level and experience
The template expander also initialises each live record's progression cells. The shown level is read from +0x130 directly, never re-derived from experience: a record with level-10 experience but +0x130 == 1 shows LV 1.
| Record offset | Field | Vanilla seed |
|---|---|---|
+0x0 (u32) | cumulative experience | 0 |
+0x4 (u32) | next-level threshold (the status screen's “next”) | Vahn/Terra 121, Noa 102, Gala 140 - the per-slot level-up correction, not a per-character curve |
+0x130 (u8) | displayed level; the applier adds 1 per level-up | 1 |
+0x131 (u8) | second per-character byte (magic-rank candidate, unconfirmed) | 1 |
Details: how the starting-level option retargets the seed
The routine seeds +0x4 by storing one register literal and reloading it twice for the per-character values. The patch controls that register and reclaims the reload sites, seeding Vahn, Noa and Gala at level N with same-size edits:
- Level - a packed
addiu $v0, (1 << 8) | N; sh $v0, 0x6F8($s0); nopat0x800561C4..CCsets+0x130 = Nand keeps+0x131 = 1. - Experience - one
addiu $t0preload at0x800560FC(midpoint ofreach(N)..reach(N+1)) feeds threesw $t0stores at0x80056100/08/18(Vahn0x5C8, Noa0x9DC, Gala0xDF0). - Next threshold - the literal at
0x800560F0loadsreach(N+1); all three stores take it, and the applier re-applies the per-slot correction on the first level-up. - Stats - each growth slot's template stats are recomputed to level
N.
Seeding only slot 0 would leave Noa at 0 experience and Gala with a stale threshold of 140 (a spurious early level-up); seeding all three keeps the roster coherent.
Parser
legaia_asset::new_game::StartingParty reads the table from the user's own SCUS_942.54 at runtime through the PSX-EXE address-to-file-offset map (the same resolver as the item-name table), so no Sony bytes are committed. seed_live_records ports the expander's stores and the engine's record builder applies them rather than restating the mapping, so the two cannot drift. new_game_seed_words mirrors the world-state table; a disc-gated test re-decodes the routine and fails on any offset, value or width the port gets wrong.
How we know
| Function | Address | What it proves |
|---|---|---|
| Template expander | FUN_800560B4 | The 4-iteration loop copying template fields into live records; literal 100 into the cap cells; level / threshold seeds |
| New-game data init | FUN_80034A6C | Bag pair, gold and party bytes as immediates; story-flag clear |
| Callers | FUN_8001DCF8 (new-game branch, flag 0x8007B7AC), FUN_8001FFA4 | Both memset the save context first |
| Boot chain | FUN_80025B64 → FUN_801D6704 | Field overlay entered with the fresh state |
| Opening scene | 0x8007050C; FUN_8001D424 gated by _DAT_8007B8C2 | Default map buffer town01; the initmap.txt developer override is off in retail |
| Table base | 0x80078C4C | Byte-search for Vahn's stat run + name; every field cross-checked against the live record at 0x80084708 in a town01 save; disc-gated new_game_real |