-
Notifications
You must be signed in to change notification settings - Fork 1
Save System
The Problem
Most save systems are vulnerable. JSON and XML serialization are easy to read and modify. Players can edit their save files to cheat. They can corrupt their saves by making mistakes. Malware can tamper with them. And if you update your game, old saves often break.
What Void Does
Void encrypts saves with AES-GCM 256-bit encryption, the same standard used by governments and militaries. The manifest system tracks exactly what data you wrote and in what order. When you load, it verifies every read against the manifest. If something is out of order, corrupted, or tampered with, the system catches it immediately.
The save is compressed if beneficial. It's written atomically to a temporary file first, then renamed. If the game crashes mid-save, your original save is untouched.
And version checking prevents loading incompatible saves.
The save system is designed to be secure and reliable:
- AES-GCM 256-bit encryption with PBKDF2 key derivation
- Manifest verification ensures data is read in the correct order
- Compression reduces file size
- Atomic writes prevent corruption
- Version checking prevents loading incompatible saves
Define your save data class:
public class PlayerSaveData
{
public string Name { get; set; }
public int Level { get; set; }
public Vect2 Position { get; set; }
public List<Item> Inventory { get; set; }
}
Create a save system that inherits from ContentTypeWriterReader:
public class PlayerSaveSystem : ContentTypeWriterReader<PlayerSaveData>
{
protected override void Write(PlayerSaveData data, ContentWriter writer)
{
writer.Write(data.Name);
writer.Write(data.Level);
writer.Write(data.Position);
writer.WriteObject(data.Inventory);
}
protected override PlayerSaveData Read(ContentReader reader)
{
return new PlayerSaveData
{
Name = reader.ReadString(),
Level = reader.ReadInt32(),
Position = reader.ReadVect2(),
Inventory = reader.ReadObject<List<Item>>()
};
}
}
Save data to a file:
var saveSystem = new PlayerSaveSystem("my-secret-key");
if (saveSystem.TrySave("player.sav", playerData, out var error))
{
Console.WriteLine("Saved successfully!");
}
else
{
Console.WriteLine($"Save failed: {error}");
}
Load data from a file:
if (saveSystem.TryLoad("player.sav", out var loadedData, out error))
{
Console.WriteLine($"Loaded {loadedData.Name} at level {loadedData.Level}");
}
The save system uses AES-GCM 256-bit encryption. Provide a key string to encrypt your saves:
var saveSystem = new PlayerSaveSystem("my-secret-key");
Without a key, saves are stored unencrypted but still use manifest verification.
The key is derived using PBKDF2 with 1000 iterations and SHA-256.
The save system automatically tracks what data you write and in what order. When loading, it verifies that you read values in the same order and with the correct types.
Write order:
writer.Write(data.Name); // String
writer.Write(data.Level); // Int32
writer.Write(data.Position); // Vect2
writer.WriteObject(data.Inventory); // Object
Manifest: [String, Int32, Vect2, Object]
If you try to read in the wrong order, the system throws an error:
reader.ReadString(); // Matches manifest[0] (String)
reader.ReadInt32(); // Matches manifest[1] (Int32)
reader.ReadVect2(); // Matches manifest[2] (Vect2)
reader.ReadObject<List<Item>>(); // Matches manifest[3] (Object)
If something doesn't match, the system catches it immediately.
Saves are written to a temporary file first. The file is only renamed to the final name if the write succeeds. This prevents corrupted saves from incomplete writes.
If the game crashes mid-save, the temporary file is discarded and your original save remains intact. If the save operation fails for any reason, the original file is never touched.
The Try pattern provides detailed error codes:
if (saveSystem.TrySave("player.sav", data, out var error))
{
// Success
}
else
{
switch (error)
{
case SaveError.InvalidPath: break;
case SaveError.OutOfSpace: break;
case SaveError.EncryptionFailed: break;
case SaveError.FileNotFound: break;
case SaveError.VersionMismatch: break;
case SaveError.WrongKey: break;
case SaveError.CorruptData: break;
case SaveError.ManifestMismatch: break;
}
}
The save system supports these types automatically:
- String
- Int32
- Single (float)
- Boolean
- Byte
- Int64
- Double
- Vect2
- Rect2
- Color
- Object (XML serialized)
Each save file stores the game version hash. When loading, the system checks if the save version matches the current game version:
public string AppVersionHash => $"{HashHelper.Cache64(AppVersion):X8}";
This prevents loading saves from incompatible game versions.
Save files are stored in:
-
Windows:
%APPDATA%/Company/Game/Saves/ -
macOS:
~/Library/Application Support/Company/Game/Saves/ -
Linux:
~/.config/Company/Game/Saves/
If you're not using application data, saves are stored in a Saves folder in your game's root directory.