Persistence
If you want to persist world state to disk, Wyrd treats it as a package away, not a system you build yourself. Pick a codec, binary or JSON, then optionally layer continuous persistence on top.
Pick a codec
Section titled “Pick a codec”Binary and JSON share one DX: reference the package, every IComponent is included by default, no attribute required.
public struct Position : IComponent{ public float X; public float Y;}Opt a specific component out with [PersistenceIgnore], identically for either codec:
[PersistenceIgnore]public struct Transient : IComponent { }What differs between them is what the save file is for:
- Binary (
Wyrd.Ecs.Persistence.Binary, backed by MemoryPack) is compact and the faster of the two to write and read, at the cost of being opaque bytes on disk. A component field shape too complex to generate a serializer for automatically (a build error names the field) can still be hand-annotated with[MemoryPackable], following MemoryPack’s own conventions for that shape. - JSON (
Wyrd.Ecs.Persistence.Json) is human-readable and diffable, worth it for save files you want to inspect, hand-edit, or check into version control as test fixtures, at the cost of being larger and slower than binary.
Reference whichever fits, then wire it up with a one-liner:
var world = new WorldBuilder() .AddBinaryPersistence("save.bin") // or .AddJsonPersistence("save.json") .Build();Save and load whenever you want a checkpoint:
world.Save();world.Load();AddBinaryPersistence/AddJsonPersistence sets World.DefaultPersistenceStore to the path given, so a bare Save()/Load() targets it automatically. Pass a path or an IPersistenceStore explicitly to target a different file for that one call, without changing the default:
world.Save("saves/other.bin");world.Load("saves/other.bin");Rename-safety
Section titled “Rename-safety”A component/relation/tag’s discriminator defaults to its fully qualified type name. Pin it explicitly instead with [StableName]:
[StableName("Enemy")]public partial struct EnemyController : IComponent { }Renamed a type that’s already been saved under its old name? [RenamedFrom] keeps that data resolving, repeatable for a type renamed more than once:
[RenamedFrom("Old.Namespace.EnemyController")][StableName("Enemy")]public partial struct EnemyController : IComponent { }Migration
Section titled “Migration”A component’s shape can change over time without losing old saves. AddBinaryPersistence/AddJsonPersistence already set World.CodecRegistry to the registry they built, register a step on it for each schema-hash transition; Load walks the chain automatically:
world.CodecRegistry!.RegisterMigration("Enemy", fromSchemaHash: 1, toSchemaHash: 2, oldBytes => /* transform */);Tag presence is part of saved state, on by default for every ITag type. Opt a specific tag out with [PersistenceIgnore], the same attribute components use:
[PersistenceIgnore]public struct Hovered : ITag { }Then, optionally: continuous persistence
Section titled “Then, optionally: continuous persistence”For crash-safe incremental saves, layer Wyrd.Ecs.Persistence.Continuous on top of whichever codec you picked:
var world = new WorldBuilder() .AddBinaryPersistence("save.bin") .EnableContinuousPersistence() .Build();This writes a bootstrap checkpoint, then starts a background WAL writer that captures every tracked change, plus a checkpoint-merge thread that periodically folds the WAL back in.
The WAL lands next to the checkpoint by default, save.bin.wal.<tick> segment files in the same directory, inferred from DefaultPersistenceStore since it’s a FileStore. Pass walStore to EnableContinuousPersistence to put it somewhere else, or if the store isn’t a FileStore at all, where it’s required.
If the process exits without calling StopContinuousPersistence, a safety net stops and merges it for you. Call it yourself for a clean shutdown:
world.StopContinuousPersistence();How it fits together
Section titled “How it fits together”world.Save()/Load()are the manual, on-demand primitive: a synchronous walk of every entity and every registered component, through anIPersistenceStore. Continuous persistence’s periodic checkpoint calls the sameSave, it doesn’t duplicate the walk.IPersistenceStoreis where the bytes go.FileStore(a single local file, written atomically) is the only implementation today.CodecRegistryis the surface for a custom codec, a manualRegisterTag, or a hand-written migration/alias, the binary and JSON packages populate one automatically for the common case.