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_game_tables::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_wordsopdeene loads first and the opening chain plays through to town01 (Rim Elm), the first interactive scene and the executable's default map name. A developer initmap.txt override exists but is gated off in retail.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
One row per character, 26 bytes, no header: eight little-endian u16 stats and a NUL-padded name.
| Field | Meaning |
|---|---|
hp_max / mp_max | also the starting HP / MP |
agl | lands in the live agility cells (+0x110 current, +0x122 max) - not the cap cells, which are a literal 100 |
udf / ldf | upper (physical) / lower (magical) defence |
spd | turn-order initiative seed |
| display name | the default name the name-entry screen later pre-fills |
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 |
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.
The entry seat
Three further stores in the same routine are absolute rather than save-context cells: 0x80073EF4 = 0xE40, 0x80073EF8 = 0x2DC0, 0x80073EFC = 0. They are the field initialiser's destination-entry operand - the same words a door's scene change or the world-map arrival writes. The first scene a New Game loads is the prologue opdeene, so the seed seats the player there at world (0xE40, 0x2DC0), tile (28, 91); opdeene's entry script reads the seat once to pick its region (and view window) before the opening record moves the player on.
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 - seeded, then read by nothing on the disc (see below) | 1 |
+0x131 is write-only. The seed's li $v0, 0x1 at 0x800561C4 feeds
sb $v0, 0x6F9($s0) (+0x131) at 0x800561C8 and sb $v0, 0x6F8($s0)
(+0x130) at 0x800561CC, and 0x800561C8 is the only instruction anywhere on the disc that
touches the byte: sweeping SCUS_942.54 plus every based overlay image for a lb/lbu/sb
at displacement 0x131, or at the four slots' 0x6F9/0xB0D/0xF21/0x1335 off the
0x80084140 game-state block, returns that store alone, and no wider access covers it. The "magic-rank candidate" reading is
therefore retired - the magic-rank counter is capture-pinned at record +0x9C.
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.
In the port
legaia_game_tables::new_game reads the table and the seed routine's literals from the user's own SCUS_942.54 (same address-to-file-offset resolver as the item-name table): StartingParty for the rows, StartingInventory for the bag (it replays the routine's stores, so it reads both the retail byte pair and the randomizer's packed halfword), new_game_seed_words for the world-state cells and seed_live_records for the expander's record stores. CLI: asset new-game <SCUS> [--json].
Retail's seed loop fills all four live records, so a member who joins later (field op 0x3C) already has a level-1 record. The engine's New Game roster is Vahn alone and keeps the template beside it; when a party list names a slot the roster has no record for, World::set_active_party seeds that slot from the template, so a joining member fights with real HP rather than an empty record counted alive.
How we know
The table base comes from a byte-search of the executable for Vahn's stat run and name, cross-checked field by field against the live record in an early town01 save state; the bag, gold and seat literals are read off the seed routines' disassembly.
Provenance: the routines involved
| 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 |