Skip to content

Save — FAQ

Decision-making, not API reference. For the exact contract see Public API.


Getting started

Where should the save file live?

Wherever your game needs it — the coordinator returns a string and has no opinion.

Application.persistentDataPath is the usual answer for a PC or mobile build. WebGL has no synchronous filesystem, so PlayerPrefs or IndexedDB via a JS plugin is the norm. Consoles have certification requirements about write timing and save indicators that only your title knows.

That is exactly why there is no default: a wrong default in a save system loses player data.


Do I need all five participants?

No. Pass the ones your game actually uses. A participant you do not construct simply contributes no section, and its absence is not an error.

The list is ordinary code, so it can be conditional on defines if you ship multiple configurations.


Why did my object not save?

Almost always: it has no StableId.

Participants address objects by StableId.Id. An object without one is invisible — not saved, not restored, and not reported as missing, because there is nothing to report it against.

Add RevFramework ▸ Core ▸ Stable Id to anything whose state should survive.


I added a StableId and it still does not come back

Three likely causes, in order:

  1. The object is spawned at runtime. In a build it gets a fresh id every launch, so nothing next session claims the saved data. Call StableId.AssignId(...) at spawn with an id derived from durable data.
  2. It shares an id with another object. Duplicating a configured object copies its id, so one gets restored with another's state. Run RevFramework ▸ Validate ▸ Duplicate Stable Ids.
  3. The participant for that system was not in the list on capture, restore, or both.

Read the report first, though. If nothing in a section came back — not one owner, not one character, not one wallet — the report says so, and says which section. Cause 1 or 2 across the board, or the wrong scene loaded, will show up there rather than leaving you to guess:

RevSave: completed with problems
  revframework.health: Failed — Restore threw: None of the 12 health owner(s) this save
  names are in the loaded scenes, so the section restored nothing at all…

Some owners missing stays silent, because that is ordinary — a save outlives the objects it was taken from.


The load said Success and nothing came back

If it really applied nothing, it will not say Success — see the entry above. A section that matched no owners at all is reported as Failed and kept in Unapplied.

What is still quiet, deliberately, is partial reach: eleven of twelve owners restoring is a save outliving one of its objects, and reporting it would fire on almost every real load. Check report.Outcomes against what you expected if a specific object is missing rather than all of them.


I quickloaded and my crafts are still running

Loading a save taken while nothing was in flight now clears them, the same way loading one taken mid-craft replaces them. If you are on an older build, an idle capture wrote no section at all and the Crafting participant was never called on restore, so whatever you started after saving kept going.


I loaded a save and an NPC's wallet was emptied

By design, and it is the one place a participant clears rather than applies on top. A capture skips an owner with no wallet, so an owner paid after the save was written has no entry in it — and a restore that wrote only what the save names would leave that money in place while restoring the player's, which is the same duplication the currency-level union closed, one level up.

If your game's wallets legitimately outlive a load — a persistent world, a shop economy outside the player's save, a partial restore — opt out:

new CurrencySaveParticipant(service, currencies, unsavedWallets: UnsavedWalletPolicy.Leave);

Leave still warns and names the owners, because the balance it preserves is indistinguishable from one arriving out of nowhere. See System Guarantees Matrix → §8.


The console says a wallet "could not be zeroed"

Your ICurrencyService answers HasWallet but returns false from ICurrencyWalletQuery.TryGetHeldCurrencies, so the participant can see that a wallet exists but not what is in it — and it will not guess. Implement TryGetHeldCurrencies on your service, or use SceneCurrencyService, which always can.


Versioning and compatibility

What happens when a player loads a save from an older build?

The version the payload was written with reaches your Restore, and you decide. Framework participants are all at version 1, so nothing has needed migrating yet.

For your own participants, branch on the version parameter. See Integration Surfaces → Choosing a version.


What about a save from a newer build?

Two separate answers:

  • Newer envelope version — still read. Sections are self-describing, so the build restores the ones it recognises rather than refusing the file outright.
  • Newer section version — the participant decides. The framework participants refuse, because applying data you do not understand is worse than declining it.

When do I bump Version?

When the payload changes in a way older code could not read — a renamed or removed field, a changed meaning, a restructured shape.

Adding an optional field that older code ignores harmlessly does not need a bump.


Can I rename a participant key?

You can, but every existing save's data for that section is orphaned — it stays in the file, nothing claims it, and it reports as Unrecognised.

If you must: keep a participant reading the old key for one release while writing under the new one, then retire the old.


SKUs and missing systems

What happens on a build that does not have every system?

Supported and normal — the framework ships in SKUs.

Sections nobody claims report as Unrecognised, which does not trip report.Success, and they come back verbatim in report.Unrecognised.


How do I stop a save shrinking every time it is loaded?

Pass the unrecognised sections back when you next capture:

var report = RevSaveCoordinator.Restore(json, participants);
_carried = report.Unrecognised;

// later
string json2 = RevSaveCoordinator.Capture(participants, out _, _carried);

This is not automatic

Omit it and saving on a build with a system removed permanently discards that system's data. Load a Complete-era save in a project without Crafting, save again without carrying over, and the Crafting section is gone.


Should I always pass carryOver?

Yes, unless you have a specific reason not to. It costs nothing when there is nothing to carry, and the failure mode without it is silent data loss.

A carried section whose key a live participant also claims is dropped in favour of the live one, so it cannot resurrect stale state.


Writing participants

Should a participant throw, or return null?

Situation Answer
Nothing to save Return null from Capture — records as Skipped, writes no section
Payload genuinely unusable Throw from Restore — records as Failed, names the section
One bad item inside an otherwise fine payload Handle it yourself — drop the item, keep the section

The Crafting participant takes the third route for unknown recipes: one job is dropped with a warning and every other job still loads. Losing one job beats losing all of them.


Can my own data sit in the same file as framework data?

Yes, and it is not second-class. Implement IRevSaveParticipant, use your own key prefix, and hand it to the coordinator alongside the framework ones. Quest flags, unlocked levels, settings — all the same mechanism.


Can one participant read another's payload?

No. The coordinator never parses payloads and neither should you — formats are deliberately private so they can change freely.

If you need state another participant owns, get it from that system's own API at capture time, or write your own participant for it.


Behaviour and failure

report.Success is false — did anything load?

Probably yes. Success means nothing failed, not nothing applied.

Check FatalError first: if it is set, the payload was unreadable and nothing was applied. Otherwise some sections failed and the rest applied — report.Outcomes says which were which, and report.ToString() is a readable summary.


Does a participant throwing break the save?

No. It is isolated, recorded against its own section, and every other participant still runs. That mirrors how the framework treats consumer callbacks everywhere: your game does not break because one handler misbehaved.


Two participants declared the same key — which wins?

The first registered. The clash is reported as DuplicateKey and trips report.Success.

It is reported rather than resolved because picking by enumeration order produces behaviour that only shows up in someone else's project.


Can I save on a background thread?

Not the capture itself — participants read live Unity state, so Capture must run on the main thread.

Once you have the string, writing it to disk on a background thread is entirely fine. The string is the boundary.


Is the save file human-readable?

The envelope is JSON, so keys, versions and timestamps are legible. Each payload is whatever its participant wrote — the framework ones are JSON too, but nested as escaped strings, so it is readable with effort rather than pleasant.

Do not build tooling that edits payloads in place. Formats are private and free to change.


  • Overview — what the system is and the shortest working example
  • Integration Surfaces — writing a participant properly
  • Guarantees Matrix — the behavioural contract, including non-guarantees
  • System Boundaries — what it deliberately will not do