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
frameworkVersionorsavedAtUtc - Relying on enumeration order to decide which of two duplicate keys wins
- Assuming an object without a
StableIdis saved, or that its absence is reported - Assuming
report.Success == falsemeans nothing was applied - Assuming a partial owner miss is reported — only a section that matched none of its owners is
- Ordering
CraftingSaveParticipantbefore Inventory or Currency - Reflecting into participant internals
- Calling the coordinator off the Unity main thread
Related¶
- Guarantees Matrix — the same surface expressed as guarantees and non-guarantees
- Integration Surfaces — implementing
IRevSaveParticipantfor your own state - System Boundaries — what the system refuses to do, and why