Save — Overview¶
In one sentence
RevSaveCoordinator composes any number of IRevSaveParticipants into a single versioned save payload, and routes a saved payload back to them — without performing file I/O, parsing any payload, or letting one bad section break the rest.
The problem it solves¶
Every RevFramework system already knew how to save itself. None of them knew how to save together.
| System | Its own snapshot shape |
|---|---|
| Inventory | JSON strings |
| Currency | objects |
| Health | a struct |
| Crafting | wants identity resolvers injected |
| Status Effects | had no persistence surface at all |
Combining those into one file was left to the caller, and it was genuinely awkward work: five different shapes, five different failure modes, and no answer at all for what happens when a save written by the Complete package is loaded on a build that only has Health.
The coordinator is the common denominator those shapes are adapted to. None of the five systems changed to make it work — each participant is a translator living in a define-gated assembly, so removing a system still compiles.
The shape¶
your game
│
│ IRevSaveParticipant[]
▼
RevSaveCoordinator ──────► JSON string ──────► wherever you want it
│ (file, PlayerPrefs, cloud,
│ inside a bigger save, encrypted)
▼
RevSaveReport
(per-section outcomes)
Three types are all most projects touch:
IRevSaveParticipant— something that can write its state out and read it backRevSaveCoordinator— static, stateless;CaptureandRestoreRevSaveReport— what happened, per section
The shortest version¶
using RevGaming.RevFramework.Core.Save;
var participants = new IRevSaveParticipant[]
{
new HealthSaveParticipant(),
new InventorySaveParticipant(itemDatabase),
};
// Save
string json = RevSaveCoordinator.Capture(participants);
File.WriteAllText(path, json);
// Load
var report = RevSaveCoordinator.Restore(File.ReadAllText(path), participants);
if (!report.Success) Debug.LogWarning(report);
RevSaveReport.ToString() produces a multi-line per-section summary, which is why logging the report directly is usually enough during development.
No file I/O, deliberately¶
The coordinator takes a string and returns a string. Where that string lives is entirely yours: Application.persistentDataPath, PlayerPrefs, a cloud save service, inside your own larger save file, or an encrypted blob.
Baking a path in would drag every platform question — WebGL, console certification requirements, cloud sync conflict policy — into a type that has no business holding an opinion about them.
The one thing that will catch you¶
Every object whose state you want saved needs a StableId component (RevFramework ▸ Core ▸ Stable Id).
A GameObject reference means nothing across sessions, so participants identify owners by StableId.Id. An object without one is invisible to every participant: not saved, and not restored.
That is deliberate rather than an oversight — saving it would write state that could never be matched back to anything on load.
StableId is necessary, not sufficient
It is stable for scene-authored objects, where the id is generated in the editor and serialized into the scene.
A runtime-spawned object has no serialized id, so one is generated in Awake — a different one every launch. Anything saved against it can never be matched again. Call StableId.AssignId(...) with an id your game derives from durable data (a spawn table entry, a room and slot index, a quest id).
Duplicating an object copies its id. Unity's duplicate copies serialized fields and the automatic generation only fills an empty value, so building a level by duplicating a configured enemy leaves several objects claiming one identity — and one gets restored with another's state. Nothing self-corrects this, because nothing can tell which copy was the original. Use RevFramework ▸ Validate ▸ Duplicate Stable Ids to find collisions.
The five framework participants¶
| Participant | Key | What it needs | What it saves |
|---|---|---|---|
HealthSaveParticipant | revframework.health | nothing | Current, max and dead state per owner |
CurrencySaveParticipant | revframework.currency | service + currency ids | Balances for the currencies you name |
InventorySaveParticipant | revframework.inventory | ItemDatabase | Container contents, and equipment where present |
CraftingSaveParticipant | revframework.crafting | service + recipes | Queued, running and paused jobs |
StatusEffectsSaveParticipant | revframework.statuseffects | effect factory | Active statuses, with remaining time |
Health is the only one needing nothing. A participant is something your game assembles and hands over — not something discovered by reflection — which is why the others take constructor arguments.
Where one needs more than a service reference, the reason is always the same: the thing has no durable identity of its own. Currency has no "every currency this wallet holds" query. Recipes have no id (an ItemDefinition carries a guid; a recipe has no equivalent). Status effects are polymorphic objects rather than data, so only your game can rebuild one from an id.
Economy and Pickups have no participant, deliberately. Economy orchestrates Currency and Inventory rather than owning state, and Pickups are world objects. Nothing is missing — there is nothing there to save.
Failure is reported, not thrown¶
Nothing here throws at you.
- A participant that throws is isolated and recorded; every other section still runs.
- A corrupt, truncated, null or empty payload reports through
RevSaveReport.FatalError. - Duplicate keys are reported rather than resolved by enumeration order.
- A section that found none of its owners is reported rather than passing as a clean load. Some owners missing stays silent — a save outlives the objects it was taken from — but none of them found means the section applied to nothing, usually because the wrong scene is loaded or ids have changed.
A half-loaded save that names the broken section is considerably more useful than an exception that abandons the lot. A load that throws is how a game ends up stuck on its main menu.
One ordering rule¶
Participants restore in the order you supply them, and Crafting goes last — after Inventory and Currency. It is the only participant whose restore writes into another system's state: it reconciles offline progress as it loads, delivering craft outputs into the inventory and refunding currency. Inventory's restore clears the container first and Currency's writes absolute balances, so either one running afterwards erases what Crafting produced, with nothing reported.
var participants = new IRevSaveParticipant[]
{
new InventorySaveParticipant(itemDatabase),
new CurrencySaveParticipant(currencyService, currencies),
new CraftingSaveParticipant(craftingService, recipes), // last
};
Everything else is order-independent between framework participants. See System Guarantees Matrix → §10.
Loading on a build without every system¶
Supported and handled deliberately, because RevFramework is sold in SKUs — this is the normal case, not an edge case.
A section no participant claims is reported as Unrecognised rather than failing the load, and handed back so you can preserve it:
var report = RevSaveCoordinator.Restore(json, participants);
// Later, when saving again — keep data for systems this build does not have
string json2 = RevSaveCoordinator.Capture(participants, out _, report.Unrecognised);
Omitting carryOver destroys data
Without that third argument, saving on a build with a system removed silently discards that system's section. Load a Complete-era save in a project with Crafting removed, save again without carrying over, and the Crafting data is gone for good.
report.Success is deliberately not tripped by unrecognised or skipped sections. Those are supported situations, not failures.
Where to go next¶
- Mental Model — how to think about participants, keys and versions
- Public API — the supported surface
- Integration Surfaces — writing a participant for your own state
- Guarantees Matrix — the behavioural contract, including what is not guaranteed