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, then write values into a scoped SaveStore.
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
{
SaveSlot _slot;
void Awake()
{
_slot = SaveSlot.Create("slot_1");
}
public void SavePlayer(int health)
{
using var store = SaveStore.Open(_slot);
store.Write(new PlayerSave
{
position = transform.position,
health = health
});
if (!store.TryCommit(out var report))
Debug.LogError(report.GetDebugMessage());
}
}
SaveSlot.Create("slot_1") resolves a folder under Application.persistentDataPath/Saves.
Scopes let one slot store many copies of the same DTO type:
var scope = SaveScope.Create("region", "0_1");
using var store = SaveStore.Open(slot);
store.Write(scope, chunkRegionSave);
store.Commit();
Each scope is stored as its own GameStream file under the slot folder.
3. Load Data
var slot = SaveSlot.Create("slot_1");
if (!SaveStore.TryOpen(slot, out var store, out var report))
{
if (report.hasBackup)
{
// Prompt the player before loading backup.
// A backup manifest may represent older progress.
store = SaveStore.OpenBackup(slot);
}
else
return;
}
using (store)
{
if (store.TryRead(out PlayerSave player, out var readReport))
{
transform.position = player.position;
}
}
SaveStore.Open 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 or corrupt scope files.
Partial recovery is explicit:
using var stream = new GameStream();
if (stream.TryRecoverFromData(scopeBytes, 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