A Windows Forms editor for party stats in Breath of Fire (SNES), working directly on
SNES9x 1.63 save states (*.000 – *.009).
The window is styled after the game's own status screen: navy ground, outlined boxes and white fixed-pitch text.
Everything the status screen shows is editable, except the level:
| Tab | Group | Fields |
|---|---|---|
| Stats | Combat | ATK, DEF, ACT, MAG, INT, FATE |
| Stats | Vitals | EXP, HP current/max, AP current/max |
| Stats | Base stats | Str., Vigor, Agil., Wisdom, Luck |
| Stats | Read-only | LEVEL, character name, equipped Wepn/Shld/Armr/Helm and the two Etc. accessories |
| Items | Gold | The party purse |
| Items | Inventory | Quantity of each consumable the party is carrying; spare equipment is listed but read-only |
| Magic | Spells | Read-only: the spells the picked character knows at their level, or the hero's dragon forms |
Item quantities are editable; which item sits in a slot is not. The equip menu will not
let you take a piece off, only swap it for another, so equipment is reported rather than
edited. A slot can still be empty — Bo joins with no shield, id 0x0000, and the status
screen simply leaves the line blank — it just cannot be emptied from inside the game.
Level is deliberately locked. Changing it on its own does not touch the stat spread or the level-up tables, so it just produces a character the game disagrees with about its own progression.
ATK, DEF, ACT, MAG, INT and FATE are derived values. The game recalculates them from the base stats and equipment, so an edit to one of them can be overwritten at the next level up or equipment change. Edit the base stats if you want a change that sticks.
Magic is reported, not editable — for the pool casters there is nothing in a save to edit, since the game builds the list from the ROM each time it draws the menu. Which characters have magic and how many spells each gets is settled; the level each spell arrives at is not, which is why lists are still added one character at a time. See Magic.
The hero is the exception on both counts. His list is the dragon transformations, out of a
table of their own, and they are unlocked by story trials rather than by levelling — so that part
really is carried in the save, as a mask at block +0x482. It is read and reported like every
other character's magic, and left alone for the same reason: it would put the hero further through
the trials than the story he is in. See Dragon transformations.
The two Etc. accessory slots are reported like the other four. They are not stored with
them: they sit at the front of the record, at +0x04, which is why they read as an
unexplained zero run for as long as nobody in the party owned an accessory.
- File → Open and pick a save state, e.g.
Breath of Fire.000from your SNES9xSavesfolder. Slot 0 of bank 0 is the file with the.000extension. - Pick a party member from the CHARACTER list.
- Edit the values and File → Save (
Ctrl+S). - Load the state in SNES9x.
A .bak copy of the previous file is written next to the state on every save. The editor
reopens the last file you worked on when it starts.
Notes from reverse-engineering an actual SNES9x 1.63 state, in case they are useful elsewhere.
The file is gzip-compressed. Decompressed it is a header line followed by fixed-length blocks laid back to back:
#!s9xsnp:0012\n
NAM:000008:<8 bytes>
CPU:000048:<72 bytes>
REG:000016:...
PPU:002652:...
DMA:000152:...
VRA:065536:...
RAM:131072:... <- SNES WRAM, $7E0000-$7FFFFF
SRA:524288:...
FIL:032768:...
SND:066560:...
CTL:000091:...
TIM:000070:...
SHO:734213:...
Each block is NAME:LLLLLL: followed by exactly LLLLLL bytes. Blocks the editor does not
understand are preserved byte for byte, and the file is written back gzip-compressed.
Live party data sits in WRAM at $7E1040, as an array of 0xA0-byte records. Slots 0–6 are
confirmed by the per-slot index bytes the game pre-initialises at record offset +0x6F; the
eighth slot is read as well and filtered out when it holds nothing plausible.
Record layout:
| Offset | Size | Field |
|---|---|---|
+0x00 |
4 | Name |
+0x04 |
2 | Equipped "Etc." accessory, first slot (probe confirmed). Zero on everyone until the hero picked up a ring, and not EXP: a probe holding 12345 here still read EXP 0 on screen. |
+0x06 |
2 | Equipped "Etc." accessory, second slot (probe confirmed). |
+0x08 |
2 | Flags. Bit 0x40 means the character is not in the active party; see The save block. 0x0080 for Jack, Bo and the palace soldiers, 0x0081 for Nina and Karn, 0x00C0 on a benched Jack and 0x80C0 on a benched soldier. 0x01 is not "has magic", tempting as that looks: the ROM's starting records give Karn 0x0081 with no spell list at all and Bleu 0x0080 with 31. |
+0x0A |
1 | Level |
+0x0C |
2 | HP max |
+0x0E |
2 | HP current |
+0x10 |
2 | AP max |
+0x12 |
2 | AP current |
+0x14 |
3 | EXP (little endian) |
+0x17 |
1 | Str. |
+0x18 |
1 | Vigor |
+0x19 |
1 | Wisdom (probe confirmed) |
+0x1A |
1 | Agil. |
+0x1B |
1 | MAG |
+0x1C |
1 | Luck (probe confirmed) |
+0x1D |
2 | ATK |
+0x1F |
2 | DEF |
+0x21 |
1 | INT (probe confirmed) |
+0x22 |
1 | ACT |
+0x23 |
1 | FATE (probe confirmed) |
+0x24 |
4 | Unidentified. 05 00 00 00 in the reference save. |
+0x28 |
2 | Equipped weapon id |
+0x2A |
2 | Equipped shield id |
+0x2C |
2 | Equipped armour id |
+0x2E |
2 | Equipped helmet id |
Names use the game's own character set rather than ASCII. 0x81-0x9A is a-z, 0xC1-0xDA
is A-Z, 0x20-0x3F is plain ASCII so digits and punctuation land where you expect, 0x7F
is a space, and 0x00 ends the field. "Jack" is stored as CA 81 83 8B. The item screen draws
0x2E as a middle dot rather than a full stop, which is why M.Drop reads as M·Drop in game.
Note the ordering of the two vitals pairs: the record holds max first and current second,
which is the reverse of the order the status screen prints them in. A state saved either side of
a won battle settles both that and EXP — the character came out four HP down and two EXP up, and
the only bytes of the record that moved were +0x0E (20 to 16) and +0x14 (0 to 2).
The two Etc. accessory slots are the same kind of 16-bit equipment id, but they are nowhere
near the other four: they are the +0x04 pair, which had read as four unexplained zero bytes
through every save until the hero finally had a ring to wear. The save that has one puts 8D 21
there and nowhere else in the whole 128K of WRAM, and 0x218D is DreamRG in the armour
table. The second slot is the other half of the run, at +0x06.
A probe settled both halves and their order at once: 0x218C (SmartRG) written to +0x04 with
0x218D (DreamRG) at +0x06 drew Etc. SmartRG and Etc. DreamRG, in that order, on the
status screen — the first slot taking the id that had moved, which is what tells the two apart.
Equipment ids are 16-bit, and both bytes matter. Writing 0x08 over the low byte of the
weapon id turned Dirk (0x1100) into EmporSD (0x1108), and the shield went from WoodSH
(0x2130) to WolfSkin (0x2109) the same way, while the armour and helmet ids that were left
alone did not budge. Zeroing +0x24..+0x2F empties all four lines.
Offsets are only trusted once a probe state has been written, loaded in SNES9x and read back off the screen. Two things are still open.
Where the spell learn levels are indexed from. Each character's learn levels are a plain array —
Nina's at 0x05BFF1 — but nothing in the ROM points at them: $BDF1 appears nowhere, and there is
no table of $BDxx words near the spell data. The obvious guess is that the level arrays sit at a
fixed distance from the id lists, and for two of them they do: Bleu's ids are 0x20 past Nina's and
her levels are 0x20 past Nina's. Run the same arithmetic back to Bo, whose ids are 8 before
Nina's, and it lands on 00 00 00 1C 00 04 00 — which is not a rising run of levels, and is ruled
out outright by the game: Bo knows all seven of his spells at level 1, so nothing in his array can
be 28. The pattern is a coincidence, the arrangement has a hole in it, or Bo has no array at all.
Learn tables are therefore still transcribed one character at a time. See Magic.
What +0x24 is. It is the last unexplained run inside a record, and it was the standing
guess for the accessory slots until those turned up at +0x04 instead. Its low byte moves with
equipment rather than with level — Nina went from 0E to 0C on changing armour alone — and it
does not look like an item id.
The 0x10 of padding after each record in the save block used to be listed here as the last place
a spell list could hide. It is not: Nina, at level 18 with twelve spells, sits in the block with
that padding entirely zero.
There is a second, 0x500-byte structure at $7E5000, byte for byte what ends up in the
.srm file:
| Block offset | Contents |
|---|---|
0x000 |
16-bit little-endian checksum, the sum of bytes 0x002–0x4FF |
0x002 |
CAPCOM!! |
0x020 |
Gold, three bytes little-endian (a probe holding 123456 read back as 123456 in game) |
0x038 |
Party roster: one character id per byte, ending 0xFF, one per block record that is still in the party |
0x140 |
Inventory: 64 back-to-back two-byte slots |
0x1C0 |
A second copy of each character record, 0x40 apart |
0x482 |
The hero's dragon transformations, a 16-bit mask with the nine forms at bits 4–12 |
This is not a staging buffer. Gold and the inventory live only here, and the game keeps them
current as you play: winning a battle worth 7G moved 0x020 from 188 to 195 on the spot, with only
the map position moving alongside it.
The character records in the block behave differently. In the same pair of states the live record was four HP down and two EXP up while its copy in the block still held the values from the last in-game save — so the block copy trails the live record rather than tracking it. Edits are written to both, so an in-game save cannot resurrect the old numbers.
The records are 0x40 apart inside the block, not the 0xA0 of the live array. A three-character
save puts them at +0x1C0, +0x200 and +0x240: 0x30 of record then 0x10 of padding. Eight of
those reach +0x3BF and stop clear of the next occupied bytes at +0x3F0, which the live stride
does not — it runs out of block after five.
Comparing the two copies is also what pins down how much of a record is real save data: the first
0x30 bytes match, and everything after is zero in the save block. So +0x30 onward — the pointers
at +0x38 and friends — is runtime-only.
Live slot n is not block record n. The game lets the party be reordered from the menu, and
that moves the live records around while the block keeps the order it had at the last in-game save.
A state with Bo, Jack and Nina live had them stored as Jack, Nina, Bo in the block, with the roster
reading 00 02 01 to match. Pairing the two copies by position therefore mirrors each character's
edits onto a neighbour's record — Jack's block record came back holding Bo's HP, AP, EXP and stats,
with only the fields the editor never writes (name, level, equipment) left to give it away. The two
copies are paired by name instead. A live character with no block record has joined since the
last in-game save and simply has no copy to keep in step yet.
Leftover records sit in among the live ones, not after them, which is what the fourth party
member exposed. The block here holds five occupied records — Jack, Nina, Bo, a stale second Jack
at level 13, then Karn — against a roster of 00 02 01 05. Karn is fourth in the party and fifth
in the block. Matching records only as far as the roster is long, on the assumption that anything
past that was a leftover, therefore dropped him: he came out with no id, no spell list, listed as
(not in party), and — the part that actually costs something — no mirror, so every edit to him
stopped at the live record and the block kept the level 12 he joined at.
An in-game save writes the block as a snapshot of the live array, in live order, one record per
party member, and leaves everything past the party untouched. The save taken on finishing the
Krypt rewrote records 0–3 as Karn, Nina, Jack, Bo with the roster reading 05 02 00 01 to match,
overwriting the stale Jack that had been sitting at record 3 — while the previous Karn record,
five deep, was left exactly where it was.
So a leftover is anything the last save did not reach, and there are two different reasons a record
can be one. The first is bit 0x40 of the flags at +0x08, which marks a character who is not
in the active party: set on that stale Jack (0xC0) and on every live Sr-1 left behind by the
palace soldiers (0x80C0), clear on all four of the current party (0x80 or 0x81). The roster is
a list of the party rather than of the records, so the two line up only once the flagged records are
skipped — which is how a party of four came to be spread over five records, with Karn's fourth id
landing on the fifth record. The second reason is simply running past the end of the roster: the
Karn record now sitting fifth is stale but carries no flag at all, because it was a live party
member's record when it was written and nothing has overwritten it since.
Both rules are needed, and either one alone gets a save wrong. That flag is also a better answer to "is this character in the party" than the roster is, because it is in the record itself and so still right for someone who joined after the last in-game save.
A duplicate name is settled the same way: the block has held two records reading Jack, and two
reading Karn, and the live character is paired with the first record that both matches the name
and agrees about party standing. First match is the right one because the game always writes the
party into the records ahead of anything it leaves behind.
The checksum is not maintained by the editor. The game's own copy in WRAM is already stale against its contents, so it is plainly recomputed only when SRAM is written.
No pool caster's magic is stored in a save. In a state where Nina had nine spells, neither the ids nor a bitmask appeared anywhere in the 128K of WRAM, and the persistent save block held no record for her at all — its party was still Jack and the two palace soldiers from the last in-game save — yet the game drew her list correctly. It is built from the ROM at draw time, from a character and a level. So the editor reports magic and cannot edit it. (The hero is the one exception: his dragon transformations are unlocked by story trials and so must be recorded in the save somewhere. That does not make them editable — where it lives is still unknown.)
The ROM is LoROM with a 512-byte header, so a bank $0B address lands at
bank * 0x8000 + (addr - 0x8000) + 0x200:
| What | Where |
|---|---|
| Spell names | 0x05920C, stride 0x11, 0x50 entries, eight-byte name then nine of data |
| Spell id pool | 0x05B196..0x05B1DA, ending FF FF |
| Pointers into the pool | 0x05B180, eleven 16-bit bank $0B addresses |
| Bo's ids | 0x05B197, 7 entries |
| Nina's ids | 0x05B19F, 23 entries |
| Nina's learn levels | 0x05BFF1, 23 entries |
| Dragon transformations | 0x059906, stride 0x0F, 9 entries, eight-byte name then seven of data |
| Starting character records | 0x009808, stride 0x30, twelve entries |
A character's spells are a contiguous slice of the pool, in the order the magic menu prints them,
paired with an array giving the level each one arrives at. The character knows every entry whose
level it has reached. Nina's levels are 1 4 5 6 7 9 11 12 13 14 15 17 19 21 23 24 26 28 30 34 37 41 46, which was checked in game three ways by patching her level byte: 5 gives 3 spells, 13
gives 9, and 30 gives 19, each matching exactly.
The pointer table is indexed by character id, which is what the roster at save block +0x38
holds. The ids are pinned down by the game's own table of starting records at 0x009808 — twelve
of them 0x30 apart, in exactly the record layout above, so they read out as names directly:
| Id | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Zack | Bo | Nina | Ox | Gobi | Karn | Mogu | Bleu | Sr-1 | Sr-2 | Sr-3 | Nina (Lv 99) |
Every id a roster has produced so far lines up with it: Jack is 0 (the hero, Zack before he is
named), Bo is 1, Nina is 2 and the palace soldiers are 8 and 9.
How long each slice is used to be inferred from where the next pointer landed. It is not: the
byte immediately before a pointer's target is the count. Nina's list at $AF9F is preceded by
0x17 and is 23 long; Bleu's at $AFB7 by 0x1F and is 31; Ox's at $AFD7 by 0x02 and is 2 —
and Ox's two ids run straight into the FF FF that the other seven pointers aim at directly, which
is how "no magic" is spelt. That settles the whole table:
| Id | 0 Zack | 1 Bo | 2 Nina | 3 Ox | 4 Gobi | 5 Karn | 6 Mogu | 7 Bleu | 8-10 Sr-n |
|---|---|---|---|---|---|---|---|---|---|
| Pointer | $AFDA |
$AF97 |
$AF9F |
$AFD7 |
$AFDA |
$AFDA |
$AFDA |
$AFB7 |
$AFDA |
| Spells | none | 7 | 23 | 2 | none | none | none | 31 | none |
So the editor can say "has no magic" outright for six of the eleven, rather than reporting that nothing is known about them. Zack is the seventh and is not one of them, however much his pointer looks like it — see Dragon transformations.
The hero's pointer aims at the terminator like Gobi's and the soldiers' do, and for four days that
was read as "the hero has no magic". It is not: it means the hero is not fed from the pool. His
magic is the dragon transformations, and they live in a table of their own at 0x059906 —
stride 0x0F, an eight-byte name then seven bytes of data, nine entries and no count byte in
front of it:
| # | Name | Data |
|---|---|---|
| 0 | SnoDr | 20 06 07 00 28 00 1f |
| 1 | FlmDr | 40 07 0a 00 8c 00 29 |
| 2 | ThrDr | 14 08 0c 00 a0 00 34 |
| 3 | IceDgn | 24 0a 14 00 b4 00 3e |
| 4 | FirDgn | c4 1a 1b 00 04 01 48 |
| 5 | BltDgn | 94 1f 1e 00 18 01 52 |
| 6 | GldDgn | a4 25 28 00 2c 01 5d |
| 7 | Rudra | cc 2d 32 00 90 01 68 |
| 8 | Agni | cc 2d 3c 00 f4 01 68 |
The names decode with the game's own character set, the same as everywhere else.
Byte 2 is the AP cost, not a learn level, and the difference cost an hour. The column reads
7, 10, 12, 20, 27, 30, 40, 50, 60, which is exactly the shape of a learn table and was written
down as one. It is not: with SnoDr highlighted the magic menu prints AP Used 7, and probe states
holding Jack at level 6 and at level 60 both drew the same three forms he has at his real
level 31. Level does not enter into it at either end. Byte 1 — 6, 7, 8, 10, 26, 31, 37, 45, 45 —
is unidentified; it was the AP cost by a bad analogy with the spell table, where byte 1 of the
nine is the cost, and that reading is now retracted.
The forms are unlocked by story trials, so what a save carries is which trials are done, and it is
a single byte at save block +0x482. Reverting that one byte to its value in a save from
before the trials empties Jack's magic menu outright; reverting any of the other sixteen bytes
that had moved in the same flag run leaves all three forms in place, as does the front run
0x5400–0x543F and the header 0x5010–0x5033.
It is a bitmask, and it starts at bit 4. With three forms unlocked it holds 0x70. Written
one bit at a time it comes back one form at a time, in table order:
+0x482 |
Menu |
|---|---|
10 |
SnoDr |
20 |
FlmDr |
40 |
ThrDr |
F0 |
SnoDr, FlmDr, ThrDr, IceDgn |
FF |
SnoDr, FlmDr, ThrDr, IceDgn — the same four |
01 |
nothing |
So bits 4–7 are entries 0–3 and the low nibble is not forms at all; FF and F0 draw an
identical menu. The menu also parks each form at a fixed grid position rather than packing the
list up — 20 alone leaves the first column empty and prints FlmDr in the second — which is what
a mask over a fixed table looks like from the front.
The remaining five carry on into +0x483, so the whole thing is a 16-bit little-endian mask
with the nine forms at bits 4–12. 0x0100 alone draws FirDgn, 0x1F00 draws the last five —
FirDgn, BltDgn, GldDgn, Rudra, Agni — and 0xF0 plus 0x1F00 draws all nine in the game's own
3 × 3 grid. It stops there: setting +0x481, +0x482 and +0x483 all to FF draws those same
nine and no more, so bits 0–3 and 13–15 are not forms and +0x481 is not part of the mask.
Bo is the second character with a table, and the odd one out: he learns nothing. Probe states
put him at level 1 and at level 45 and the magic menu printed the same seven both times, in the
pool's order — Flare Spark Cold Fry Flame Frost Cura — as it does at the level 9 he joins at. His
levels are stored here as flat 1s standing in for a learn array that has not been found and may
not exist; no level between 1 and 45 can disagree with them.
Because the roster lives in the save block it trails the live array, so a slot's id is only trusted when a block record still carries the same name as the live one. When the party has moved on since the last in-game save that check fails and the slot simply has no id, which also means no spell list — the honest answer rather than a wrong one.
Party membership is read from the record's own flags rather than from the roster, so it survives
a party change the block has not caught up with. A record left behind by a character who has
dropped out is still perfectly readable, so the editor lists it but marks it (not in party)
rather than hiding it — a sleeping Jack is still worth editing before he rejoins.
What is still not worked out is where the learn levels are indexed from, which is why learn tables are transcribed one character at a time and checked against the game before going in. See Still unverified.
An inventory slot is two bytes, but not always an (id, quantity) pair. Equipment can sit in the
bag unequipped, and then the two bytes are the same 16-bit equipment id a character record holds,
low byte first. A screenshot of a 34 item bag lined up against the bytes settled it: every entry
the item screen printed with no x count read back with a second byte of 0x11 or 0x21 — the
weapon and armour pages — and the low byte named the piece exactly.
5E 11 -> 0x115E Rang (read as "Icicle x17" while the pair was taken literally)
58 21 -> 0x2158 SuedeHT
04 11 -> 0x1104 LongSD
08 09 -> 0x08 Herb, nine of them
So the second byte is a count only while it stays inside its low nibble; above that its high nibble is a type. Two probes settled that rather than leaving it as a guess about where counts stop:
| Probe | Bytes | The item screen printed |
|---|---|---|
| Herb stack forced to 99 | 08 63 |
ArmPad — the armour whose id is 0x08, no count at all |
| Cure stack forced to 15 | 09 0F |
Cure x? — a count is one digit, and 15 has no glyph |
Which is why quantities are capped at 9 here. That also matches an untouched bag: thirty four Herbs sit in it as three stacks of nine and one of seven rather than merging, and no stack anywhere exceeds nine.
Type 6 in that first probe is an artefact of forcing a value the game never writes; only 1
(weapons) and 2 (armour) have turned up in a real save. Anything else is reported as a raw
$XXXX rather than guessed at.
Names are not guessed at one save state at a time — they are read out of the game's own tables in
the ROM, which is the only practical way to cover id spaces this large. Game/BofItemNames.cs
holds the result. Three fixed-stride tables of eight-byte, zero-padded names:
| Table | Offset | Stride | Entries | Indexed by |
|---|---|---|---|---|
| Consumables | 0x13F400 |
0x0E |
0x80 |
the one-byte inventory id |
| Weapons | 0x13E400 |
0x10 |
0x60 |
the low byte of a 0x11xx equipment id |
| Armour | 0x13EA00 |
0x10 |
0x8E |
the low byte of a 0x21xx equipment id |
Offsets are into the .smc file, which carries a 512-byte copier header. The armour table covers
shields, helmets and rings as well as body armour — WoodSH is 0x2130 and StrawHT is 0x216D.
Gaps in the tables are real; the game leaves unused slots blank.
Only the 0x11 and 0x21 pages are recognised, because those are the only two that have turned up
in a real save. Any other page, and any id whose slot is blank, falls back to $XX or $XXXX
rather than being guessed at. To add or correct entries without rebuilding, drop a
bof_items.json next to the executable:
{ "0x08": "Herb", "0x1100": "Dirk" }dotnet build
Requires the .NET 10 SDK with the Windows desktop workload. Output lands in bin/Debug/net10.0-windows/.
MIT. See LICENSE.