Skip to content

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.


It does not touch the filesystem

Capture returns a string. Restore takes one. There is no path, no File, no PlayerPrefs, no platform check anywhere in Runtime/Core/Save.

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 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. Application.persistentDataPath, PlayerPrefs, a cloud service, inside a larger save of your own, or encrypted first.


It does not serialise your game

The coordinator never parses a payload. It reads and writes key, version and an opaque payload string, and nothing else.

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.

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 when your game decides it is time.


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.