Save — System Boundaries¶
What this page is for
Everything below is something the save system deliberately does not do. Each has a reason, and in most cases a pointer to what does it instead. Nothing here is a missing feature.
The coordinator does not touch the filesystem¶
RevSaveCoordinator.Capture returns a string. Restore takes one. There is no path, no File, no PlayerPrefs and no platform check anywhere in it.
Why: a file path drags every platform question into a type with no business holding an opinion about them — WebGL has no synchronous filesystem, consoles have certification requirements about save indicators and write timing, cloud saves need conflict policy. Any default baked into the router would be wrong for somebody, and a wrong default in a save system loses player data.
Instead: put the string wherever your game needs it — or let RevSaveManager do it.
There is storage in Runtime/Core/Save, one layer up
The opinion the coordinator refuses to hold still has to live somewhere, and until 1.2.0 that somewhere was every project separately. It now lives in IRevSaveStore, with FileSaveStore as the default implementation — one file per slot under Application.persistentDataPath — and RevSaveManager as the component that drives the two together.
That is a boundary moved, not a boundary crossed. The coordinator is still storage-free and still the type your own tooling composes against; the store is a replaceable default sitting beside it, and assigning RevSaveManager.Store swaps it for cloud, PlayerPrefs, an encrypted blob, or a slot inside a larger save file of your own.
See Public API → IRevSaveStore and FileSaveStore, which also states what a failed write does and does not promise.
It does not serialise your game¶
The coordinator never parses a payload. Of a section it reads and writes key, version and an opaque payload string, and nothing else. Envelope-level metadata is a separate matter: it reads that, reports the timestamp on RevSaveReport.SavedAtUtc, and still branches on none of it.
Why: participants own their formats entirely. That is what lets one change how it serialises without the envelope or any other participant caring, and what makes your own data exactly as first-class as Inventory's.
Instead: each participant serialises its own state, by whatever means suits it.
It does not migrate saves¶
The version a payload was written with is delivered to Restore. Nothing acts on it.
Why: only the participant knows what changed between its own versions. A generic migration engine would have to understand every payload format, which is precisely the coupling the payload-as-string design exists to avoid.
Instead: branch on the version parameter inside your Restore. See Integration Surfaces → Choosing a version.
It does not resolve objects¶
The coordinator has no concept of a GameObject. Object identity lives inside participant payloads, via StableId.
Why: the coordinator routes sections to participants. Which object a section's contents refer to is a participant concern, and different participants legitimately identify different things.
Instead: add StableId to anything whose state should survive, and address objects by StableId.Id in your own participants.
It does not decide when to save¶
No autosave, no timer, no scene-load hook, no application-quit handler — and that is as true of RevSaveManager as it is of the coordinator. The manager is a MonoBehaviour, but it does nothing in Awake, nothing on quit, and nothing on its own. Adding it to a scene starts no clock.
Why: save timing is a design decision with real consequences — checkpoint placement, mid-combat saves, save scumming, platform write-frequency limits. A framework default would be wrong for most games and invisible until it hurt.
Instead: call Capture, or RevSaveManager.Save(slot), when your game decides it is time.
It does not discover participants¶
There is no attribute, no registry and no reflection scan. You construct each participant and hand it over — to the coordinator per call, or once to RevSaveManager.Register.
Why: two reasons, and the second is structural. A participant usually needs context only your game has — the ItemDatabase, the recipe list, the currency ids that matter, the factory that rebuilds a status effect — and something found by reflection could not be handed any of it. And the framework's own participants live in define-gated assemblies under Integrations/Save/ which reference RevFramework.Core; referencing them back would invert the dependency graph and break the modularity that lets a deleted system still compile.
Instead: register the ones you use, once, wherever you compose your game.
It does not run in the background¶
Everything is synchronous, and must be called from the Unity main thread.
Why: participants read live system state — HealthSystem components, inventory containers, active status effects. Every framework participant resolves owners through StableIdOwners.Find<T>(), which calls Object.FindObjectsByType; Unity restricts that to the main thread and throws if called elsewhere. An async coordinator could therefore only offer async around a synchronous core.
Worth knowing if you write a participant over pure data: the constraint is inherited from what a participant reads, not intrinsic to the routing. The coordinator holds no state and touches nothing thread-bound beyond serialising the envelope.
Instead: if serialisation cost matters, the string is the boundary — write it to disk on a background thread once you have it.
It does not guarantee cross-participant consistency¶
Sections are captured in sequence. If game state changes between two participants capturing, the save reflects both moments rather than one.
Why: freezing the world would mean either stopping the game loop or having every system implement a snapshot-isolation protocol. Neither is proportionate for a single-player save.
Instead: capture at a quiet moment — a checkpoint, a menu, a level transition — rather than mid-frame during combat.
It does not enforce that a participant is correct¶
A participant that writes a payload it cannot read back will fail on restore, not on capture. The coordinator has no way to know.
Why: it never parses payloads, so it cannot validate one.
Instead: test the round trip. Every framework participant has PlayMode tests doing exactly that — see Testing Philosophy.
It does not auto-correct duplicate keys or duplicate ids¶
Two participants with one key: the first wins, the clash is reported. Two objects with one StableId: the first wins, and a duplicate-id check is available but never runs automatically.
Why: in both cases nothing can tell which one was intended. Picking by enumeration order would produce behaviour that only shows up in someone else's project, and regenerating the "wrong" id would break every save that referenced it.
Instead: read report.Outcomes for key clashes, and run RevFramework ▸ Validate ▸ Duplicate Stable Ids for id collisions.
It does not save Economy or Pickups¶
Neither has a participant, and neither is missing one.
Why: Economy orchestrates Currency and Inventory rather than owning state — saving Currency and Inventory saves everything Economy would have. Pickups are world objects whose state is the scene.
What it does guarantee¶
Everything above is a boundary. For the positive contract — what it does promise, and the explicit non-guarantees alongside — see the Guarantees Matrix.