Skip to content

Save System

Softfire’s save system handles local serialisation, multiple save slots, optional cloud sync via Worldweave, conflict resolution, and rolling backup-based corruption recovery. It is accessible via the SaveSystem service.

Overview

SaveSystem manages one or more save slots. Each slot is an independent save state. A slot contains the scene state, any registered ISaveable services, and a manifest with metadata (timestamp, playtime, thumbnail).

The system serialises to a binary format (MessagePack) rather than JSON for compact file sizes and faster write times. The .sfsav extension is used for save files.

Save slots

var saves = Services.Get<SaveSystem>();
// List all slots (returns metadata without loading full data)
IReadOnlyList<SaveSlotInfo> slots = saves.GetSlots();
// Create a new slot
SaveSlotId newSlot = saves.CreateSlot("My Game");
// Load a slot (replaces current world state)
await saves.LoadSlotAsync(slotId);
// Delete a slot
saves.DeleteSlot(slotId);

By default, three slots are available. Override maxSlots in SaveConfig.json to change this:

{
"save": {
"maxSlots": 5,
"autoSaveIntervalSeconds": 300,
"rollingBackupCount": 3
}
}

Saving and loading custom data

Register your custom data with the save system by implementing ISaveable on a service:

using Softfire.Engine.Save;
public class PlayerProgressService : ISaveable
{
public int Level { get; private set; } = 1;
public int ExperiencePoints { get; private set; } = 0;
public List<string> UnlockedAbilities { get; private set; } = new();
// Called by SaveSystem when writing a slot
public SaveData Capture()
{
return new SaveData
{
["Level"] = Level,
["ExperiencePoints"] = ExperiencePoints,
["UnlockedAbilities"] = UnlockedAbilities,
};
}
// Called by SaveSystem when loading a slot
public void Restore(SaveData data)
{
Level = data.Get<int>("Level", defaultValue: 1);
ExperiencePoints = data.Get<int>("ExperiencePoints", defaultValue: 0);
UnlockedAbilities = data.Get<List<string>>("UnlockedAbilities", defaultValue: new());
}
}

Register the service with the save system in Initialize:

var progress = new PlayerProgressService();
Services.Register(progress);
Services.Get<SaveSystem>().Register(progress);

When SaveSystem.SaveSlotAsync(slotId) is called, it calls Capture() on every registered ISaveable and serialises the results. When loading, it calls Restore() with the deserialised data.

Saving a slot

var saves = Services.Get<SaveSystem>();
// Quick save to the currently active slot
await saves.QuickSaveAsync();
// Save to a specific slot
await saves.SaveSlotAsync(slotId);
// Auto-save (triggered automatically by the timer; can also be called manually)
await saves.AutoSaveAsync();

All save operations are async to avoid hitching the main thread. The save file is written atomically — the new file replaces the old file only after a successful write, using a temp file and rename.

Cloud sync and conflict resolution

When Worldweave is connected, SaveSystem automatically syncs save slots to cloud storage after each save. On login from a new device, if the cloud version is newer than the local version, the system displays a conflict resolution UI:

  • Use cloud save — downloads the cloud slot and replaces local
  • Keep local save — uploads the local slot to cloud, overwriting cloud
  • View comparison — shows both timestamps and playtime before deciding

You can override the conflict resolution UI by setting SaveSystem.ConflictResolver to your own IConflictResolver implementation.

Corruption recovery

If a save file fails to deserialise (truncated write, unexpected crash), the save system falls back to rolling backups. Up to rollingBackupCount previous versions are kept as .sfsav.bak1, .sfsav.bak2, etc. The system tries each backup in order until one loads cleanly.

If no backups can be loaded, LoadSlotAsync throws SaveCorruptedException. Handle this in your UI to offer the player a choice to start a new slot rather than silently crashing:

try
{
await saves.LoadSlotAsync(slotId);
}
catch (SaveCorruptedException ex)
{
Logger.Error($"Save slot {slotId} is corrupted: {ex.Message}");
// Show corruption dialog in UI
UI.ShowCorruptionDialog(slotId);
}