Skip to main content

Overview

The SaveFile class is the abstract base class for all Pokémon save file formats in PKHeX. It provides a unified interface for reading, writing, and manipulating save data across all generations of Pokémon games.

Key Concepts

Memory Buffer Architecture

SaveFile uses a memory-based architecture for efficient data manipulation:
public readonly Memory<byte> Buffer;
public Span<byte> Data => Buffer.Span;
This design allows direct byte-level access to save data while maintaining memory safety.

Core Properties

Every SaveFile implementation must provide:
  • Version: The game version (see GameVersion enum)
  • Generation: The generation number (1-9)
  • Context: The entity context for this save file
  • PKMType: The type of PKM entities this save uses
  • BoxCount: Number of storage boxes
  • BoxSlotCount: Slots per box (typically 30)

Working with Save Files

Loading a Save File

using PKHeX.Core;

// Load from file bytes
byte[] saveData = File.ReadAllBytes("pokemon.sav");
var saveFile = SaveUtil.GetVariantSAV(saveData);

if (saveFile == null)
{
    Console.WriteLine("Invalid save file!");
    return;
}

Console.WriteLine($"Game: {saveFile.Version}");
Console.WriteLine($"Generation: {saveFile.Generation}");
Console.WriteLine($"Trainer: {saveFile.OT}");
Console.WriteLine($"TID: {saveFile.DisplayTID}");

Accessing Trainer Information

SaveFile implements ITrainerInfo and provides trainer properties:
// Basic trainer info
string trainerName = saveFile.OT;
byte gender = saveFile.Gender;
int language = saveFile.Language;

// Trainer IDs
uint tid = saveFile.DisplayTID;  // Visible TID
uint sid = saveFile.DisplaySID;  // Visible SID
uint id32 = saveFile.ID32;       // Combined 32-bit ID

// Play time
int hours = saveFile.PlayedHours;
int minutes = saveFile.PlayedMinutes;
int seconds = saveFile.PlayedSeconds;
string playTime = saveFile.PlayTimeString;  // "123ː45ː67"

// Money and resources
uint money = saveFile.Money;

Box Data Management

Reading Box Data

// Get all Pokémon from all boxes
IList<PKM> allPokemon = saveFile.BoxData;

// Get Pokémon from a specific box (0-indexed)
PKM[] box1Pokemon = saveFile.GetBoxData(0);

// Get a specific Pokémon
PKM pokemon = saveFile.GetBoxSlotAtIndex(box: 0, slot: 5);

// Check if slot has Pokémon
int offset = saveFile.GetBoxSlotOffset(0, 0);
bool hasData = saveFile.IsPKMPresent(saveFile.BoxBuffer[offset..]);

Writing Box Data

// Set a Pokémon in a box slot
PKM myPokemon = CreatePokemon();
saveFile.SetBoxSlotAtIndex(myPokemon, box: 1, slot: 10);

// Set all box data at once
IList<PKM> newBoxData = LoadPokemonFromFile();
saveFile.BoxData = newBoxData;

// Find next open slot
int nextSlot = saveFile.NextOpenBoxSlot();
if (nextSlot >= 0)
{
    saveFile.SetBoxSlotAtIndex(myPokemon, nextSlot);
}
else
{
    Console.WriteLine("Storage is full!");
}

Party Management

// Check if save has party data
if (saveFile.HasParty)
{
    // Get party Pokémon
    IList<PKM> party = saveFile.PartyData;
    int partyCount = saveFile.PartyCount;
    
    // Get specific party member
    PKM starter = saveFile.GetPartySlotAtIndex(0);
    
    // Set party data
    PKM[] newParty = new PKM[6];
    // ... populate newParty ...
    saveFile.PartyData = newParty;
    
    // Delete a party slot (shifts remaining down)
    saveFile.DeletePartySlot(2);
}

Pokédex Operations

if (saveFile.HasPokeDex)
{
    // Check seen/caught status
    bool seenPikachu = saveFile.GetSeen(25);
    bool caughtPikachu = saveFile.GetCaught(25);
    
    // Set seen/caught
    saveFile.SetSeen(25, true);
    saveFile.SetCaught(25, true);
    
    // Get completion stats
    int seenCount = saveFile.SeenCount;
    int caughtCount = saveFile.CaughtCount;
    decimal percentSeen = saveFile.PercentSeen;
    decimal percentCaught = saveFile.PercentCaught;
    
    Console.WriteLine($"Seen: {seenCount}/{saveFile.MaxSpeciesID} ({percentSeen:P1})");
    Console.WriteLine($"Caught: {caughtCount}/{saveFile.MaxSpeciesID} ({percentCaught:P1})");
}

Storage Operations

Sorting Boxes

// Sort all boxes by species
int repositioned = saveFile.SortBoxes();

// Sort specific box range
int count = saveFile.SortBoxes(
    BoxStart: 0, 
    BoxEnd: 5,
    reverse: false
);

// Custom sort method
int sorted = saveFile.SortBoxes(
    BoxStart: 0,
    BoxEnd: -1,
    sortMethod: (pokemon, startIndex) => pokemon.OrderBy(p => p.CurrentLevel)
);

Clearing Boxes

// Clear all Pokémon from boxes 10-15
int deleted = saveFile.ClearBoxes(BoxStart: 10, BoxEnd: 15);

// Clear with criteria
int deletedShinies = saveFile.ClearBoxes(
    BoxStart: 0,
    BoxEnd: -1,
    deleteCriteria: pk => pk.IsShiny
);

Modifying Pokémon in Bulk

// Apply modification to all Pokémon in boxes
int modified = saveFile.ModifyBoxes(
    action: pk => pk.Heal(),  // Heal all Pokémon
    BoxStart: 0,
    BoxEnd: -1
);

// Make all Pokémon shiny
int shinified = saveFile.ModifyBoxes(
    action: pk => pk.SetShiny(),
    BoxStart: 0,
    BoxEnd: 5
);

Flags and Event Data

// Read and write flag bits
bool flag = saveFile.GetFlag(offset: 0x1000, bitIndex: 3);
saveFile.SetFlag(offset: 0x1000, bitIndex: 3, value: true);

// Marks save as edited
// saveFile.State.Edited is automatically set to true

Saving Changes

Writing Save File

// Export save data
Memory<byte> finalData = saveFile.Write();
File.WriteAllBytes("pokemon_modified.sav", finalData.ToArray());

// With export settings
var settings = BinaryExportSetting.IncludeFooter;
Memory<byte> dataWithFooter = saveFile.Write(settings);

Checksums

SaveFile automatically manages checksums:
// Check if checksums are valid
if (!saveFile.ChecksumsValid)
{
    Console.WriteLine("Warning: Invalid checksums!");
    Console.WriteLine(saveFile.ChecksumInfo);
}

// Checksums are automatically updated on Write()
var data = saveFile.Write();  // SetChecksums() called internally

Game-Specific Implementations

Generation 8 Example (Sword/Shield)

if (saveFile is SAV8SWSH swsh)
{
    // Access game-specific blocks
    Box8 boxInfo = swsh.BoxInfo;
    Party8 partyInfo = swsh.PartyInfo;
    MyItem8 items = swsh.Items;
    Zukan8 pokedex = swsh.Zukan;
    Record8 records = swsh.Records;
    
    // Check save revision (base game, IoA, CT)
    int revision = swsh.SaveRevision;
    string revisionStr = swsh.SaveRevisionString;  // "-Base", "-IoA", "-CT"
    
    // Access raid data
    RaidSpawnList8 galarRaids = swsh.RaidGalar;
    RaidSpawnList8 armorRaids = swsh.RaidArmor;
    RaidSpawnList8 crownRaids = swsh.RaidCrown;
}

Advanced Topics

Entity Import Settings

// Configure how Pokémon are imported
var settings = new EntityImportSettings
{
    UpdateToSaveFile = EntityImportOption.Enable,   // Adapt to save format
    UpdatePokeDex = EntityImportOption.Enable,      // Update Pokédex
    UpdateRecord = EntityImportOption.Enable        // Update records
};

saveFile.SetBoxSlotAtIndex(pokemon, 0, 0, settings);

Slot Protection

// Check if slot is locked or protected
bool isLocked = saveFile.IsBoxSlotLocked(box: 0, slot: 0);
bool isProtected = saveFile.IsBoxSlotOverwriteProtected(box: 0, slot: 0);

// Check storage status
bool isFull = saveFile.IsStorageFull;

Box Management

// Current box
int currentBox = saveFile.CurrentBox;
saveFile.CurrentBox = 5;

// Swap two boxes
bool success = saveFile.SwapBox(box1: 0, box2: 5);

// Move box to new position
bool moved = saveFile.MoveBox(box: 3, insertBeforeBox: 0);

Best Practices

Always validate save data before making modifications:
  • Check ChecksumsValid before editing
  • Verify Version and Generation match expectations
  • Use IsOriginValid to validate Pokémon species
Use the State.Edited property to track changes:
if (saveFile.State.Edited)
{
    Console.WriteLine("Save file has been modified");
}

Build docs developers (and LLMs) love