Skip to content

Save — Public API

This page is the contract

If a type or member is not listed here, it is not supported — even if it appears public in code. Behaviour not stated here may change without a major version bump.

Namespace: RevGaming.RevFramework.Core.Save Assembly: RevFramework.Core — ships in every package, with no define guard.


Supported surface at a glance

Type Kind Role
IRevSaveParticipant interface The seam you implement
RevSaveCoordinator static class Capture and restore
RevSaveReport class What happened, per section
RevSaveSectionOutcome class One section's result
RevSaveSectionStatus enum The five possible results
RevSaveSection serializable class One section of the file
RevSaveEnvelope serializable class The file container
StableId component Durable object identity

IRevSaveParticipant

The interface your own state implements. Framework systems are not privileged — they implement the same one.

public interface IRevSaveParticipant
{
    string Key { get; }
    int Version { get; }
    string Capture();
    void Restore(string payload, int version);
}
Member Contract
Key Stable identifier for this section. Must not change between versions of your game — renaming orphans every existing save's data for that section. Framework keys use a revframework. prefix; use your own to avoid collision.
Version The payload format version you currently write. Increment when the format changes in a way older code could not read.
Capture() Returns the payload, or null when there is nothing to save. A null or empty payload is recorded as Skipped, not written as an empty section.
Restore(payload, version) Applies a payload. version is the version the payload was written with, which may be older or newer than Version.

Throwing from Restore is legitimate

It is the supported way to refuse a payload you cannot apply. The coordinator isolates the exception, records that section as Failed, and carries on with the others.


RevSaveCoordinator

Static and stateless. Two saves running against different participant sets cannot interfere, and there is no instance to wire up or keep alive.

public static string Capture(IEnumerable<IRevSaveParticipant> participants);

public static string Capture(
    IEnumerable<IRevSaveParticipant> participants,
    out RevSaveReport report,
    IEnumerable<RevSaveSection> carryOver = null);

public static RevSaveReport Restore(
    string json,
    IEnumerable<IRevSaveParticipant> participants);

Capture

Guarantee Detail
Output JSON string; one section per participant that returned a payload
Null participants Entries that are null in the sequence are ignored
Null/empty key Recorded Failed; that participant contributes nothing
Duplicate key First wins; later ones recorded DuplicateKey and skipped
Participant throws Recorded Failed; every other participant still captures
Key or Version getter throws Recorded Failed; isolated exactly as a throwing method body is
Empty payload Recorded Skipped; no section written
carryOver Sections written through untouched, unless a live participant wrote a section under the same key

Restore

Guarantee Detail
Return value Never null
Bad input Never throws — reported through FatalError
Null/empty/unparseable JSON FatalError set; nothing applied
Unclaimed section Recorded Unrecognised and preserved in report.Unrecognised
Participant throws Recorded Failed; every other section still restores
Key getter throws Recorded Failed; every other section still restores
Newer envelope version Still attempted; recognised sections restore
Routing A section goes to the first participant claiming its key — never order-dependent between runs with the same set

carryOver is not automatic

Omitting it when saving on a build that lacks a system discards that system's data permanently. See Overview → Loading on a build without every system.


RevSaveReport

public IReadOnlyList<RevSaveSectionOutcome> Outcomes { get; }
public IReadOnlyList<RevSaveSection> Unrecognised { get; }
public IReadOnlyList<RevSaveSection> Unapplied { get; }
public IReadOnlyList<RevSaveSection> PartiallyApplied { get; }
public IReadOnlyList<RevSaveSection> Displaced { get; }
public string FatalError { get; }
public bool Success { get; }
public override string ToString();
Member Contract
Outcomes Every section's outcome, in processing order
Unrecognised Sections nobody claimed, verbatim — pass to Capture as carryOver to preserve them
Unapplied Sections a participant refused without applying any of — safe to carry over
PartiallyApplied Sections a participant applied part of — kept for diagnostics, ❌ never carry these over
Displaced Carried sections a capture did not write, because something already held the key
FatalError A problem with the save as a whole (unreadable JSON, nothing to read), as opposed to one section
Success True when nothing failed: no fatal error, no Failed, no DuplicateKey, no PartiallyApplied
ToString() Multi-line summary suitable for logging; names any displaced sections in its header

All four section lists hand back copies. RevSaveSection has public fields for JsonUtility, so IReadOnlyList protects the list and not its contents — editing what you are given cannot reach anything the framework is still holding, and the same instances are safe to pass straight back as carryOver.

Success is deliberately not tripped by Unrecognised or Skipped

Loading a save on a build without every system installed is a supported situation. The framework ships in SKUs, so it is the normal case rather than the exception.

Nor by Displaced, which is the closer call: a capture that displaced a carried section still produced a complete and valid save, and failing it would report correct work as broken. The loss is surfaced by its own status, its own list, and a line in ToString() instead.


RevSaveSectionStatus

public enum RevSaveSectionStatus
{
    Ok = 0,
    Skipped = 1,
    Unrecognised = 2,
    Failed = 3,
    DuplicateKey = 4,
    PartiallyApplied = 5,
    Displaced = 6,
}
Value Meaning Trips Success?
Ok Handled without complaint no
Skipped Participant had nothing to write no
Unrecognised No participant claimed the section no
Failed Participant threw, or declared a null/empty key yes
DuplicateKey Two participants declared the same key; first used yes
PartiallyApplied Participant applied part of its section, then failed yes
Displaced A carried section was not written; something already held its key no

Skipped and Displaced are opposites, and were once the same value

Skipped is the commonest non-event in a capture: a participant with nothing to write. Displaced is the moment data you were trying to preserve ceases to exist. While they shared a status, the only notice of a real loss was indistinguishable from a no-op, and telling them apart meant matching on message prose.


RevSaveSectionOutcome

public string Key { get; }
public RevSaveSectionStatus Status { get; }
public string Message { get; }
public override string ToString();

Message is populated for anything other than Ok.


RevSaveSection and RevSaveEnvelope

Both are [Serializable] with public fields so the envelope round-trips through JsonUtility — which keeps a JSON library out of RevFramework.Core, an assembly that ships everywhere and has no dependencies to spend.

[Serializable] public sealed class RevSaveSection
{
    public string key;      // participant key this section belongs to
    public int version;     // participant's payload version at time of writing
    public string payload;  // the participant's own payload; never parsed by the coordinator
}

[Serializable] public sealed class RevSaveEnvelope
{
    public int envelopeVersion;
    public string frameworkVersion;   // diagnostic only
    public string savedAtUtc;         // diagnostic only, round-trip "o" format
    public List<RevSaveSection> sections;

    public const int CurrentEnvelopeVersion = 1;
}

frameworkVersion and savedAtUtc are diagnostic only

Nothing in the coordinator branches on either, and your code must not either. They exist so that when a player reports a broken save, knowing which build wrote it is the fastest way in. Treating them as logic inputs makes them a compatibility surface they were never designed to be.

You will normally only touch RevSaveSection as an opaque item in report.Unrecognised.


StableId

Namespace: RevGaming.RevFramework.Core.Identity Component menu: RevFramework ▸ Core ▸ Stable Id

public string Id { get; }
public void AssignId(string value);   // throws ArgumentException on null/whitespace
Aspect Contract
Scene-authored objects ✔ Id generated in the editor and serialized into the scene — same value every session
Runtime-spawned objects Not stable across sessions unless you call AssignId
AssignId Replaces rather than refuses, because in a build Awake has already generated a throwaway before a spawner could act
Blank id Rejected — an object with no id is skipped by every participant, so accepting one would quietly make it unsaveable
Duplicated objects Share an id; not auto-corrected. Use RevFramework ▸ Validate ▸ Duplicate Stable Ids

AssignId will overwrite an authored id

Assign at spawn time and leave scene objects alone. Calling it on an authored object breaks every existing save that referenced the old value.


Framework participants

Namespace: RevGaming.RevFramework.Integrations.Save.* Each lives in a define-gated assembly and compiles only when its system is present.

Participant Key Version Constructor needs
HealthSaveParticipant revframework.health 1 nothing
CurrencySaveParticipant revframework.currency 1 service + currency ids
InventorySaveParticipant revframework.inventory 1 ItemDatabase
CraftingSaveParticipant revframework.crafting 1 service + recipes
StatusEffectsSaveParticipant revframework.statuseffects 1 effect factory

Their keys and payload formats are part of this contract. Their internals are not.

CraftingSaveParticipant goes last

It is the only participant whose restore writes into another system's state — offline craft outputs land in the inventory and refunds land in a wallet, during the load. Inventory's restore clears the container first and Currency's writes absolute balances, so Inventory and Currency must come before it or what it produced is erased with nothing reported.

It is also the only one that replaces rather than applies on top: its restore clears the live job list, and its capture writes a section even when nothing is in flight so an idle save clears too.

CurrencySaveParticipant takes one more optional argument than the table implies:

new CurrencySaveParticipant(
    service, currencies,
    reason: "RevSaveRestore",
    unsavedWallets: UnsavedWalletPolicy.Zero);   // default; or .Leave

UnsavedWalletPolicy decides what a restore does about an owner holding a wallet the save does not mention — Zero (default) zeroes what it holds, Leave keeps it and warns. This is the one participant that clears rather than applying on top, because the alternative is minting money. See System Guarantees Matrix → §8.


Explicitly not supported

  • Parsing, rewriting or hand-authoring another participant's payload
  • Branching on frameworkVersion or savedAtUtc
  • Relying on enumeration order to decide which of two duplicate keys wins
  • Assuming an object without a StableId is saved, or that its absence is reported
  • Assuming report.Success == false means nothing was applied
  • Assuming a partial owner miss is reported — only a section that matched none of its owners is
  • Ordering CraftingSaveParticipant before Inventory or Currency
  • Reflecting into participant internals
  • Calling the coordinator off the Unity main thread

  • Guarantees Matrix — the same surface expressed as guarantees and non-guarantees
  • Integration Surfaces — implementing IRevSaveParticipant for your own state
  • System Boundaries — what the system refuses to do, and why