At a glance

Where
Main RAM 0x80085958; save block offset +0x1818 (rides verbatim into every memory-card save)
Size
256 slots × 2 bytes: [item id][count]; id 0 = free; stacks cap at 99
Window
Three halfwords gp[+0x2D2] start, gp[+0x2D4] end, gp[+0x2D6] span - one writer
Accessors
Five SCUS_942.54 helpers (add / consume / capacity / reserve / normalize); nothing touches the array raw
Engine
engine-core inventory kernel (apply_battle_loot, buy_from_shop, field-VM GIVE_ITEM)
Confidence
Confirmed - disassembly of every helper plus a live three-member window read
Used by
Field menu, Shop, Field script VM, Battle item use

What players see

Open the menu and the bag looks like a handful of tabbed pages. Play a segment where one character is alone and the bag seems cut in half: items the party had are gone, and what the loner picks up never shows up once everyone regroups. Both impressions come from one array and one window. The pages are views; the halves are access rules; nothing is ever physically moved.

The inventory array and its three possible active windows 0x80085958 - 256 slots x [id][count] slots 0 .. 127 slots 128 .. 255 party of 2+ (or story flag 20 set): window [0, 256) Vahn alone: window [0, 128) any other lone character: window [128, 256)
The five inventory helpers only ever look between start and end. A lone character gets one half; a party gets the whole array.

Storage and access

Nothing indexes the array directly. The pause menu, chests and scripts (the field VM’s GIVE_ITEM op), battle rewards, shops and the equip swap-back all go through the same small helper family, passing item ids or helper-returned slot numbers. There is no raw-index sort or swap primitive anywhere in retail.

HelperWhat it does
addfind a matching id, else the first free slot in the window
consumedecrement by id; zeroes an emptied id in place, never compacts
capacity / reservepaired before adds in the equip swap-back path
normalizere-installs the window, merges duplicate stacks (cap 99), pulls occupied slots down into holes
window setupthe sole writer of the three window halfwords

A new game seeds exactly one slot - five Healing Leaves - after pre-zeroing the whole range. Occupancy keys on the id alone, so a live id with a zero count survives compaction.

The active window

The helpers never see “256 slots”; they see [start, end). Window setup picks the bounds from the party roster - member count and the first roster id - every time normalize runs.

Party membersStory flag 20First roster memberWindow installed
0--none - the previous window stays
1set-[0, 256)
1clearVahn[0, 128)
1clearanyone else[128, 256)
2 or morenot testednot tested[0, 256)
  • Consequence. A solo character has an isolated pocket of the bag: they can neither see nor spend the party’s items, and what they pick up alone never reaches the party inventory.
  • Design intent. Vahn owns the lower half because the game opens with Vahn alone - the early-game bag simply is [0, 128) until the party forms. Once two members are present the full 256 opens.
  • Bounds. end is only ever 128 or 256. Slots outside the window exist in RAM but are invisible to add, consume, capacity and normalize alike.

Three “divisions” that are not the window

DivisionWhere it livesWhat it is
The window halvesthe three gp halfwordsRuntime bounds over one shared array, keyed on party composition. Storage is never partitioned - only access is.
Per-character geareach character record, equip bytes +0x196..+0x19DWorn weapon, armor and Goods are not in the bag. Equipping refunds the displaced item through the add helper.
Menu pagesconsumables 0x77..0x8E, equipment below, books and key items aboveThe pause menu’s tabs filter the one array by item-id band. The “72-slot” figure is the general-items page size.

The add helper’s off-by-one

The add helper stores the item id before it bound-checks. On a completely full window the scan exits at i == end and the id byte lands one slot past the window; only the count store is guarded. This is the core of the open arbitrary-code-execution reachability thread - see How we know for the exact addresses.

Details: reaching the leak, and the probe trap

The stray id lands at 0x80085958 + end·2: 0x80085A58 when end = 128, 0x80085B58 when end = 256. Demonstrating it needs a bag genuinely filled to end (256 slots with a multi-member party) with the hit shown to land past the guard. An execution probe at pc = 0x800422BC fires on every successful add, before the guard, so a hit there is not out-of-bounds evidence by itself. Status: open RE threads.

History: the 72-slot ACE ceiling

The earlier ceiling “0x800859E8 = first key-item slot” assumed the 72-slot display page was the window. The accessors bound on end, which is only ever 128 or 256, so every out-of-bounds figure derived from a 72-slot bag is void. See do-not-re-walk.

How we know

FunctionAddressWhat it proves
window setupFUN_8004313Csole SCUS writer of gp[+0x2D2 / +0x2D4 / +0x2D6]; 11 callers; branches on party count (0x80084594), story flag 20 (FUN_8003CE64(0x14)), first roster byte (0x80084598)
addFUN_800421D4id store precedes the bound check; count store guarded
consumeFUN_80042310zeroes the emptied id in place
capacity / reserveFUN_80042EE0 / FUN_80043048the pair the equip swap-back calls before adding
normalizeFUN_800423E0calls window setup first; occupancy = id ≠ 0
new-game seedFUN_80034A6Cwrites slot 0 = (0x77 Healing Leaf, ×5)
battle rewardsFUN_8004E568loot grants go through the add helper
menu overlay sweepPROT 0899, all 129 functionszero direct array writes; 17 inventory operations, all via the helpers
live readthree-member mid-game statewindow (0, 256, 256), 160 contiguous occupied slots - more than any 72-slot model allows

Cheat-device names (Have 99 Items, Item Modifier) are third-party anchors on the same region; the Have 99 Items range covers 72 slots because that is the page it targeted. Per-cell detail: memory map.

See also