# Breath of Fire Save State Editor 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. ## What it edits 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. ### Not handled yet 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](#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](#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. ## Using it 1. **File → Open** and pick a save state, e.g. `Breath of Fire.000` from your SNES9x `Saves` folder. Slot 0 of bank 0 is the file with the `.000` extension. 2. Pick a party member from the **CHARACTER** list. 3. Edit the values and **File → Save** (`Ctrl+S`). 4. 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. ## Save state format 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. ### Party data 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](#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. #### Still unverified 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](#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. ### The save block 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. ### Magic 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](#dragon-transformations). ### 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 `1`s 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](#still-unverified). ### Items 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: ```json { "0x08": "Herb", "0x1100": "Dirk" } ``` ## Building ``` dotnet build ``` Requires the .NET 10 SDK with the Windows desktop workload. Output lands in `bin/Debug/net10.0-windows/`. ## Licence MIT. See [LICENSE](LICENSE).