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 slotSaveSlotId newSlot = saves.CreateSlot("My Game");
// Load a slot (replaces current world state)await saves.LoadSlotAsync(slotId);
// Delete a slotsaves.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 slotawait saves.QuickSaveAsync();
// Save to a specific slotawait 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);}