PurrSave
dev.purrnet.saving A saving framework for PurrNet.
PurrSave
PurrSave is a DTO-first save/load framework for Unity.
You define stable save data types, register how they are packed, put values into a GameStream, then persist that stream with SaveFileIO.
Quick Start
1. Define Save Data
using PurrNet.Saving;
using UnityEngine;
[SaveKey("game.player")]
public struct PlayerSave
{
public Vector3 position;
public int health;
[BitLoader(0)]
public static PlayerSave LoadV0(GamePacker packer)
{
return new PlayerSave
{
position = packer.Read<Vector3>(),
health = packer.Read<int>()
};
}
[BitSaver]
public static void Save(GamePacker packer, PlayerSave value)
{
packer.Write(value.position);
packer.Write(value.health);
}
}
SaveKey is the stable identity for the data type. Do not rename it casually; old save files depend on it.
2. Save Data
using PurrNet.Saving;
using UnityEngine;
public sealed class PlayerSaveExample : MonoBehaviour
{
GameStream _stream;
SaveSlot _slot;
void Awake()
{
_slot = SaveSlot.Create("slot_1");
_stream = new GameStream();
}
public void SavePlayer(int health)
{
_stream.Set(new PlayerSave
{
position = transform.position,
health = health
});
if (!SaveFileIO.TrySave(_slot, _stream, out var report))
Debug.LogError(report.GetDebugMessage());
}
void OnDestroy()
{
_stream?.Dispose();
}
}
SaveSlot.Create("slot_1") resolves a path under Application.persistentDataPath/Saves and gives you the primary and backup paths.
Manual paths are also supported:
SaveFileIO.TrySave("C:/Temp/save.purrsave", stream, out var report);
For small updates, Write<T> can upsert one DTO without dropping the rest of the file:
SaveFileIO.Write(slot, playerSave);
SaveFileIO.Write(slot, worldSettings);
If the save file already exists, Write<T> loads the whole stream first, replaces only T, then saves the full stream back. If the existing file is corrupt, the write fails instead of overwriting it.
3. Load Data
using var stream = new GameStream();
var slot = SaveSlot.Create("slot_1");
if (!SaveFileIO.TryLoadPrimary(slot, stream, out var report))
{
Debug.LogWarning(report.GetDebugMessage());
if (report.hasBackup)
{
// Prompt the player before loading backup.
// A backup may represent older progress.
SaveFileIO.TryLoadBackup(slot, stream, out var backupReport);
}
}
if (stream.TryGet<PlayerSave>(out var player))
{
transform.position = player.position;
}
For a quick single-DTO read:
if (SaveFileIO.TryRead(slot, out PlayerSave player, out var report))
{
transform.position = player.position;
}
TryLoadPrimary never loads a backup automatically. Backup loading is explicit so the game can communicate possible lost progress.
Versioning
When a save data format changes, add a new loader and update the saver.
[BitLoader(1)]
public static PlayerSave LoadV1(GamePacker packer)
{
return new PlayerSave
{
position = packer.Read<Vector3>(),
health = packer.Read<int>()
};
}
Keep old loaders when possible. They are how old files migrate into the current DTO shape.
Recovery
Normal loads are strict. They reject corrupt entries.
Partial recovery is explicit:
if (SaveFileIO.TryRecoverPrimary(slot, stream, out var fileReport, out var recoveryReport))
{
Debug.Log($"Recovered entries: {recoveryReport.recoveredCount}");
Debug.Log($"Skipped entries: {recoveryReport.skippedCount}");
}
Recovery can skip entries with checksum mismatches or decompression failures when the stream structure is still readable.
Roslyn Support
PurrSave ships a Roslyn analyzer/source generator that:
- Registers valid
[BitLoader],[BitSaver], and[SaveKey]methods. - Reports invalid loader/saver signatures.
- Reports missing or invalid
SaveKeyattributes. - Offers quick fixes and generation/refactoring actions where supported by the IDE.
Docs
Log in and subscribe to the Studio plan to access this package.
Log In