Gen 3 Save File Format Explained
Last updated: November 2026 · Applies to Pokémon Ruby, Sapphire, Emerald, FireRed and LeafGreen (Game Boy Advance)
Every Pokémon Game Boy Advance game keeps its save in a small flash chip on the cartridge. Dumped by an emulator or a backup tool, that chip becomes a single 128 KB file, usually named <game>.sav. It looks like 131,072 random bytes, but the layout is rigid and — unlike later generations — completely unencrypted except for one small, well-understood region. This guide walks through the whole file the way a parser sees it, and explains every step our online editor performs.
The big picture: two saves in one file
The 128 KB file is divided into five regions:
| Offset | Size | Contents |
|---|---|---|
0x00000 | 57,344 B | Game save A |
0x0E000 | 57,344 B | Game save B |
0x1C000 | 8,192 B | Hall of Fame |
0x1E000 | 4,096 B | Mystery Gift / e-Reader data |
0x1F000 | 4,096 B | Recorded battle |
The interesting part is the pair of game saves. The game alternates between them: each time you save, it writes the other slot, so one slot always holds a working backup. If the newest slot fails its checksum checks when the game loads, the game silently falls back to the older one. A save editor has to respect this dance — which is why our editor reports both slots and writes your edits to both of them.
Sections: fourteen blocks per save slot
Each 57,344-byte game save is a run of 14 sections of 4,096 bytes. Every section carries the same skeleton: up to 3,968 bytes of data followed by a fixed footer at offset 0xFF4 inside the section:
| Section offset | Size | Contents |
|---|---|---|
0x000 | 3,968 B | Data |
0xFF4 | 2 B | Section ID (u16) |
0xFF6 | 2 B | Checksum (u16) |
0xFF8 | 4 B | Signature — always 0x08012025 (little-endian bytes 25 20 01 08) |
0xFFC | 4 B | Save counter (u32) |
Two details matter for parsing. First, the sections are rotated: the game shifts the section order by one on every save, so section ID 0 (trainer info) is rarely the first physical section. A parser must walk all 14 slots, read the footer of each, and index sections by their ID instead of by position. Second, the footer's save counter is the same in all 14 sections of a slot, and the last physical section's counter decides which slot is newer: if slot A's counter is greater, A is the current save; otherwise (including ties) B wins.
Section IDs
| ID | Checksum covers | Contents |
|---|---|---|
| 0 | 3,884 B | Trainer info |
| 1 | 3,968 B | Team and items |
| 2 | 3,968 B | Game state (flags, variables) |
| 3 | 3,968 B | Misc data |
| 4 | 3,848 B | Rival info / game-specific data |
| 5–12 | 3,968 B each | PC buffer parts A–H |
| 13 | 2,000 B | PC buffer part I |
The section checksum, step by step
Every section protects itself with a 16-bit additive checksum. The documented algorithm is refreshingly simple:
- Start a 32-bit accumulator at zero.
- Add the section's data to it, four bytes at a time, interpreting each group as one little-endian 32-bit word. How many bytes are covered depends on the section ID (see table above).
- Fold the result into 16 bits: add the upper 16 bits of the accumulator to the lower 16 bits. That value is the checksum.
In JavaScript, a section checksum looks like this:
// data: Uint8Array of the section, size: bytes covered for this section ID
const view = new DataView(data.buffer, data.byteOffset, size);
let sum = 0;
for (let i = 0; i < size; i += 4) {
sum = (sum + view.getUint32(i, true)) >>> 0;
}
const checksum = ((sum & 0xffff) + (sum >>> 16)) & 0xffff;
This is why Gen 3 saves are so editor-friendly: the checksum is a plain addition, no lookup tables, no cryptography. After any edit the tool recomputes the affected section's checksum and writes it back to 0xFF6; a mismatch would make the game discard that save slot.
Trainer info (section 0)
Section 0 holds the player profile. The fields relevant to editing sit at fixed offsets, shared by all five games:
| Offset | Size | Contents |
|---|---|---|
0x00 | 7 B | Player name (game charset, 0xFF-terminated) |
0x08 | 1 B | Gender (0 = boy, 1 = girl) |
0x0A | 4 B | Trainer ID — low 16 bits = visible ID, high 16 bits = Secret ID |
0x0E | 5 B | Time played: u16 hours, then u8 minutes, seconds, frames |
0x13 | 3 B | Options (text speed, sound, battle style…) |
FireRed and LeafGreen can be told apart from Ruby/Sapphire/Emerald by a game code field: FRLG stores the value 1 at section offset 0x0AF8. The game family matters because the team section uses different offsets in the two groups.
Team and items (section 1)
The party is up to six Pokémon records of 100 bytes each. Ruby/Sapphire/Emerald store a 4-byte party counter at 0x234 and the records at 0x238; FireRed/LeafGreen use a 1-byte counter at 0x034 with records at 0x038. Money and coins also live in this section — in RSE they are stored plainly, while FRLG masks them with a security key. Our editor currently covers the four most-demanded fields (nickname, OT name, IDs, experience); item editing is a natural follow-up.
The PC buffer (sections 5–13)
Nine of the fourteen sections form one logical region, the PC buffer: 8 × 3,968 bytes plus 2,000 bytes, 33,744 bytes in total. To read it, a parser concatenates the data areas of sections 5 through 13 in section-ID order (not physical order). The assembled buffer then lays out as:
| Buffer offset | Size | Contents |
|---|---|---|
0x000 | 4 B | Currently viewed box minus 1 (u32) |
0x004 | 33,600 B | 420 Pokémon records of 80 bytes (14 boxes × 30 slots) |
0x8344 | 126 B | 14 box names, 9 bytes each |
0x83C2 | 14 B | One wallpaper byte per box |
Box records are only 80 bytes long — the trailing stats of the 100-byte party structure are not stored, because the game recomputes them from the data block when a Pokémon is withdrawn (this is the famous "Box trick": stats, including level from experience, are recalculated on withdrawal).
The Pokémon record, byte by byte
Both party (100 B) and box (80 B) records share the same head:
| Offset | Size | Contents |
|---|---|---|
0x00 | 4 B | Personality value (PID) — controls nature, gender, shininess and more |
0x04 | 4 B | OT ID — low 16 bits visible Trainer ID, high 16 bits Secret ID |
0x08 | 10 B | Nickname (game charset, 0xFF-terminated, 0x00-padded) |
0x12 | 1 B | Language (1 = Japanese, 2 = English, 3 = French, 4 = Italian, 5 = German, 7 = Spanish) |
0x13 | 1 B | Flags — bit 0 Bad Egg, bit 1 has species, bit 2 use egg name, bit 3 block Box R/S |
0x14 | 7 B | OT name |
0x1B | 1 B | Markings |
0x1C | 2 B | Checksum over the 48-byte data block |
0x20 | 48 B | Four encrypted substructures (12 bytes each) |
0x50 | 20 B | Status, level, mail, current HP and stats — party records only |
Substructures and their rotation
The 48-byte data block is four 12-byte substructures: Growth (species, held item, experience, friendship), Attacks (four moves and their PP), EVs & condition, and Misc (Pokérus, origin ball and game, IVs, egg and ability flags, ribbons). Their storage order is dictated by the PID modulo 24 — the same personality value that fixes a Pokémon's nature also shuffles its own data layout. PID 4, for example, gives order GMAE, so the growth block starts at byte 12 of the data area, not byte 0. A parser that assumes a fixed order corrupts the very fields it tries to edit.
Decryption
The whole 48-byte block is XOR-encrypted with a single 32-bit key:
key = OT ID (u32) XOR personality value (u32) for each 4-byte word of the data block: word ^= key
Decrypt, patch whatever you need (say, the u32 experience at growth offset +4), sum the 48 bytes as u32 words into a u16, store that as the record checksum, then re-encrypt with the same key. Editing the Trainer ID is even simpler — it lives outside the encrypted block — but note that the OT ID participates in the XOR key, so any tool that changes it must also re-encrypt every Pokémon owned by that trainer. Our editor recomputes keys and checksums automatically for every edit it applies.
The Gen III character set
Nicknames and trainer names are not stored as ASCII or UTF-8. The GBA games use a proprietary single-byte encoding: capital letters start at 0xBB, digits at 0xA1, the male/female symbols sit at 0xB5/0xB6, and the PK/MN ligatures at 0x53/0x54. Fixed-length fields end with a 0xFF terminator and are padded with 0x00 bytes. This is why a name exported to a text file can contain odd accented letters, and why an editor must map characters explicitly — writing raw ASCII into a nickname field produces garbage in-game. Our editor only accepts characters the Western versions can display and substitutes "?" for anything else, so the game never meets an invalid byte.
Why all this makes Gen 3 saves a great editor target
- No global encryption — later generations encrypt or hash large parts of the save; Gen 3 protects each section with one additive checksum and each Pokémon block with one XOR key.
- Deterministic layout — every offset above is fixed per game family; only the section rotation varies, and the rotation is self-describing via the section IDs.
- Self-healing structure — the dual-slot design gives every save a built-in backup, and a careful editor keeps both slots consistent.
If you want to poke at a save yourself, open the editor with your .sav file — everything runs locally — or read the format documentation this article is based on:
References
- Bulbapedia — Save data structure (Generation III)
- Bulbapedia — Pokémon data structure (Generation III)
- Bulbapedia — Pokémon data substructures (Generation III)
- Bulbapedia — Character encoding (Generation III)