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 |
IRevSaveOrdered | interface | Optional, on a participant — where it must sit in the restore sequence |
RevSaveOrder | static class | Early / Default / Late constants for the above |
RevSaveCoordinator | static class | Capture and restore. Takes a string, returns a string |
RevSaveManager | component | The coordinator driven: participants, slots, storage and carry-over |
IRevSaveStore | interface | Where payloads are kept — replaceable |
FileSaveStore | class | The default store: one file per slot under Application.persistentDataPath |
RevSaveRestore | static class | What a participant declares from inside its own Restore |
RevSavePartialRestoreException | exception | "I applied part of this section, then failed" |
RevSaveReport | class | What happened, per section — and when the save was stamped |
RevSaveSectionOutcome | class | One section's result |
RevSaveSectionStatus | enum | The seven possible results |
RevSaveSection | serializable class | One section of the file |
RevSaveEnvelope | serializable class | The file container |
StableId | component | Durable object identity |
Two layers, and you can use either
RevSaveCoordinator is the router: participants in, string out, string in, participants restored. It never opens a file, and never will. RevSaveManager is that router driven — it holds the participant list, owns a slot-addressed IRevSaveStore, and carries forward the sections a load could not place. Reach for the coordinator when your game already has somewhere to put the string; reach for the manager when it does not.
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.
IRevSaveOrdered and RevSaveOrder¶
Optional, and most participants should not implement it. Restoring is normally independent per section — Inventory does not care whether Health has run.
public interface IRevSaveOrdered
{
int RestoreOrder { get; }
}
public static class RevSaveOrder
{
public const int Default = 0; // what a non-implementer is treated as
public const int Early = -1000; // before ordinary participants
public const int Late = 1000; // after them
}
| Guarantee | Detail |
|---|---|
| Effect | Restore sorts your participant list by declared order before applying anything |
| Non-implementers | Treated as RevSaveOrder.Default; never moved relative to each other |
| Ties | The sort is stable by construction — equal orders keep the sequence you supplied |
Any int is legal | The values are only ever compared with each other |
Capture | ✔ Ignores it entirely — no capture order produces a different file |
RestoreOrder getter throws | Recorded Failed under the key <order threw>; that participant defaults to the position you supplied it in, and still restores |
Implement it only when your restore reads or writes another system's state, because that is the only case where order can change the outcome. CraftingSaveParticipant is the framework's one example: its restore reconciles offline progress, delivering craft outputs into the inventory and refunding currency, so it declares RevSaveOrder.Late.
A list you ordered deliberately comes back untouched
Only a participant that explicitly asks to move, moves. The sort carries each participant's original index as its tie-break rather than trusting List.Sort, which is not stable — an unstable sort here would quietly shuffle a caller's deliberate order and be almost impossible to attribute.
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 |
| Timestamp | Stamped into the envelope and reported on report.SavedAtUtc — one UtcNow, so file and report agree |
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 |
| No / zero envelope version | FatalError set; nothing applied — the payload parsed but is not a RevSave envelope |
| 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 |
| Sequencing | Your list order, stably sorted by IRevSaveOrdered. Never the order in the file |
| Timestamp | Parsed onto report.SavedAtUtc; null when missing or unreadable, never a default date |
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.
RevSaveManager wires it for you — that is most of what the manager is for.
RevSaveManager¶
Component menu: RevFramework ▸ Core ▸ Save Manager. [DisallowMultipleComponent].
The coordinator's opinions — where the string lives, who contributes to it, and what happens to the sections a load could not place — have to live somewhere, and until 1.2.0 that somewhere was every project separately. This is them.
public IRevSaveStore Store { get; set; }
public IReadOnlyList<IRevSaveParticipant> Participants { get; }
public IReadOnlyList<RevSaveSection> CarriedOver { get; }
public event Action<string, RevSaveReport> SaveCompleted;
public event Action<string, RevSaveReport> LoadCompleted;
public bool Register(IRevSaveParticipant participant);
public bool Unregister(IRevSaveParticipant participant);
public void Clear();
public RevSaveReport Save(string slot);
public RevSaveReport Load(string slot);
public bool HasSave(string slot);
public bool Delete(string slot);
public bool TryListSlots(List<string> results);
public void DiscardCarriedOver();
Registration¶
| Member | Contract |
|---|---|
Register | true when added. false for a null participant, or for the same instance already registered — refused rather than allowed to become a duplicate-key report at save time. Two different participants sharing a key is a real error and is still reported by the coordinator |
Unregister | true when it was registered and has been removed |
Clear | Removes every participant |
Participants | Registration order. This is the order Capture walks, and the order Restore starts from before IRevSaveOrdered is applied |
It cannot find the framework's own participants for you, and that is structural
They live in define-gated assemblies under Integrations/Save/ which reference RevFramework.Core; referencing them back would invert the dependency graph. You register the ones you use, once, wherever you compose your game — and yours sit alongside them with no privilege difference.
There is no Awake hook, no autosave, and no quit handler. Nothing happens until you call it.
Save(slot)¶
| Guarantee | Detail |
|---|---|
| Return value | The capture report. Never null |
| What it writes | Every registered participant's capture, plus CarriedOver written through untouched |
| Store failure | Recorded through report.FatalError — never thrown. See Write failure, below |
SaveCompleted | Raised after the write attempt, with the slot and the report, success or not |
| Carry-over after a save | ✔ Kept. A save does not consume it — only a Load replaces it and only DiscardCarriedOver empties it |
Load(slot)¶
| Guarantee | Detail |
|---|---|
| Return value | The restore report. Never null |
| Missing or unreadable slot | report.FatalError names the slot; nothing was applied, and CarriedOver is left exactly as it was |
| Slot read, payload rejected | The coordinator's fatal report is returned, and CarriedOver is replaced by that report's (empty) lists |
| What is carried | report.Unrecognised plus report.Unapplied — sections nobody claimed, and sections a participant refused having applied nothing |
| What is not | report.PartiallyApplied, deliberately. Part of one is live state now, and writing it back later would overwrite the owners that did load |
| Replaced, not accumulated | ✔ Loading a second slot does not drag the first slot's passengers into it |
LoadCompleted | Raised whether or not the report succeeded — check report.Success before acting |
Reconcile from LoadCompleted — what a restore raises varies by participant
Health and Attributes are silent. Health writes current, max and dead state directly — no Died, no Revived, no HealthChanged — because firing death events during a load would spawn VFX and drop loot every time a save was read. It means a health bar bound to those events shows stale values until something refreshes it.
The other four are not. Currency restores through SetBalance, Inventory writes into the live container and equipment, Status Effects re-applies through ApplyStatus, and Crafting completes offline jobs — each raising whatever its own API raises, sometimes several times in one load.
Both problems have the same answer: reload the scene, or reconcile from this event. What you must not do is read the absence of an event as proof that no load happened; that only ever held for two of the six. The guarantees matrix has the participant-by-participant table.
A throwing event subscriber cannot reach you
Both events invoke their subscribers one at a time inside a try. A listener that throws is logged and the others still run — a UI refresh failing should not also cost the analytics hook, and a save runs from a checkpoint or a quit button.
Slots and carried data¶
| Member | Contract |
|---|---|
HasSave / Delete / TryListSlots | Delegate straight to Store. TryListSlots returns true when the store could answer, whether or not any slots exist — an empty list means "no saves", false means "could not tell" |
CarriedOver | The sections the last load could not place. Exposed so a slot menu can say this save contains data from systems you do not have rather than presenting it as ordinary |
DiscardCarriedOver | ❌ Throws data away. The next save no longer contains those sections, permanently, for any build that could have read them. For starting a genuinely new game, not for tidying up |
IRevSaveStore and FileSaveStore¶
public interface IRevSaveStore
{
bool TryRead(string slot, out string payload);
bool TryWrite(string slot, string payload);
bool Exists(string slot);
bool TryDelete(string slot);
bool TryListSlots(List<string> results);
}
| Member | Contract |
|---|---|
TryRead | true when a payload was read; false when the slot is absent or unreadable — the two are not distinguished |
TryWrite | true when the payload was stored, replacing any existing one |
Exists | Whether a slot currently holds a payload |
TryDelete | true when a payload was removed; false when there was nothing to remove |
TryListSlots | results is cleared and filled. true means the store could answer — including with an empty list |
Nothing on this interface throws, and yours must not either
A save runs from a checkpoint or a quit button, and an exception there takes the rest of that frame's logic with it. Every operation reports success or failure instead — the same reasoning the coordinator applies to participant callbacks.
A slot name arrives from your game, not from this framework. An implementation that maps slots onto paths, keys or URLs is responsible for making an unexpected one safe.
The default store¶
RevSaveManager.Store defaults to a FileSaveStore rooted at Application.persistentDataPath/<saveFolderName>, where saveFolderName is a serialized inspector field defaulting to Saves. One file per slot, named <slot>.json.
| Aspect | Contract |
|---|---|
| Created | Lazily, on the first read of Store, from the folder name as it stood at that moment |
| Replacing it | Assign your own for cloud storage, PlayerPrefs, an encrypted blob, or a slot inside a larger save of your own |
Assigning null | ✔ Restores the default, rather than leaving the manager unable to do anything |
FileSaveStore.Root | The directory holding the slot files |
FileSaveStore.AtAbsolutePath(dir) | A store at an explicit directory, taken at its word and not validated. For tests, tooling, and projects with their own conventions |
FileSaveStore.IsValidSlot(slot) | Public, so you can validate before offering a name |
Slot and folder names are validated, not sanitised
A slot becomes a file name and the folder name becomes a directory, so both are checked: not empty, not . or .., no path separators, and no characters the platform rejects in a name. Everything a game would reasonably use — "auto", "slot1", "quicksave", a profile GUID — passes.
A bad slot is refused with a logged error and nothing is read or written. A bad folder name falls back to Saves with a logged error, because the manager builds its default store from a serialized inspector field and an exception out of a property getter on the first save is a worse failure than saves landing in the default folder.
Quietly rewriting a name into something safe would mean the caller's slot and the file no longer correspond, which surfaces much later as a load that silently finds nothing.
Path.Combine discards its first argument entirely when the second is absolute, so a full path passed as a folder name would relocate every save while appearing to work. That is what AtAbsolutePath exists to separate, and it is why the constructor refuses one.
Write failure — the exact guarantee¶
FileSaveStore.TryWrite writes the payload to a sibling <slot>.json.tmp, deletes the existing <slot>.json if there is one, and moves the temporary into place.
| Interruption | What is on disk afterwards |
|---|---|
| Invalid slot name | ✔ Nothing touched. The previous save is intact |
| Failure while creating the directory or writing the temporary | ✔ The previous save is intact. The temporary is deleted on the way out |
| Failure during the delete-then-move swap | ⚠ The previous save may already be gone, and the slot is left empty — but the payload survives as <slot>.json.tmp, which TryWrite deliberately keeps and logs the path of. Recoverable by renaming it, until the next write to that slot overwrites it |
| Process killed while the temporary is being written | ✔ The previous save is intact — but a stray <slot>.json.tmp is left behind, and nothing ever cleans it |
| Process killed inside the swap | ⚠ Same shape as the failure above, reached without any code running: the previous save may be gone and the new payload is sitting in <slot>.json.tmp, which TryListSlots, Exists and TryRead do not see, because they match *.json. Recoverable by renaming it |
What the temporary file does and does not buy you
It covers the write, which is the long part and the one a player force-quitting during an autosave is most likely to land in: an interrupted write leaves the previous save readable rather than a truncated one, so the worst case becomes losing the newest progress rather than all of it.
It does not make the operation atomic. The delete-then-move swap is a window this cannot cover, and a failure inside it costs the previous save. It is far smaller than writing the payload, and the alternative — File.Replace — requires the destination to already exist, which it does not for a first save, and adds a backup file as a third artefact to reason about.
What it does buy inside that window is the difference between losing the save and losing the file's name. The temporary at that point is a complete, valid payload, so TryWrite keeps it rather than cleaning it up and logs the path to rename. Nothing loads it for you: recovery is a deliberate rename, because a store that silently read back the file its own swap had rejected would hide the fault instead of reporting it. The next write to that slot overwrites it.
So: Save reporting a write failure means nothing was saved, which is the outcome that matters to the caller. It does not promise the previous save survived. If that distinction matters to your title — console certification, cloud sync — write your own IRevSaveStore with the atomicity guarantee your platform offers, which is exactly why the interface is there.
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 DateTime? SavedAtUtc { 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 |
SavedAtUtc | When the save was stamped, or null when the file carried no usable timestamp. Set on capture and restore alike |
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. Does not include SavedAtUtc |
SavedAtUtc is how you tell how long the game was closed
DateTime.UtcNow - report.SavedAtUtc.Value, read from LoadCompleted. The envelope has always recorded the stamp; before 1.3.0 it was written and never handed back, so anything needing elapsed time carried a second timestamp of its own inside a participant payload.
null means the file did not say — a save written before this was surfaced, or a corrupted stamp. There is deliberately no fallback date, because DateTime.MinValue would read as a real instant and turn "unknown" into two thousand years of offline progress.
It is wall clock, so it is the player's to change. Whether to trust it is your design decision.
How the coordinator decides which list a failure lands in¶
| What your participant does | Where the section lands |
|---|---|
| Returns normally | Ok |
Throws RevSavePartialRestoreException | PartiallyApplied |
Calls RevSaveRestore.MarkMutated(), then throws anything | PartiallyApplied |
Throws without having called MarkMutated() | Unapplied — safe to carry over |
public void Restore(string payload, int version)
{
if (version > Version) throw new NotSupportedException(...); // nothing touched: Unapplied
foreach (var entry in Parse(payload))
{
RevSaveRestore.MarkMutated(); // from here on, any throw
Apply(entry); // is PartiallyApplied
}
}
Why the declaration exists
Classification is exception-driven, and "any exception means nothing was applied" is right for the common case — a version guard refusing a payload before touching anything — and wrong for the expensive one. A participant that restored two owners and then hit an unexpected failure on the third threw something ordinary, so its section was recorded Unapplied and carried into the next save on top of state that had already changed. Three of the framework's own participants had that hole independently, which is why the answer is here rather than in each of them.
RevSavePartialRestoreException is still the better tool when you know you failed partway: it carries a message naming what did and did not land, which a classification alone cannot. MarkMutated is the backstop for the failure you did not anticipate — which is, by definition, the one you did not write a message for.
Calling MarkMutated() outside a restore does nothing, so a participant's own unit tests can call Restore directly without a coordinator around it.
The two supported members are the whole of that surface:
public static class RevSaveRestore
{
public static void MarkMutated(); // idempotent; a plain field write
}
public sealed class RevSavePartialRestoreException : Exception
{
public RevSavePartialRestoreException(string message);
public RevSavePartialRestoreException(string message, Exception inner);
}
Worth naming the owners that did not restore in that message: the ones that did are no longer recoverable from the save.
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; // round-trip "o"; read it on RevSaveReport.SavedAtUtc
public List<RevSaveSection> sections;
public const int CurrentEnvelopeVersion = 1;
}
frameworkVersion is diagnostic only. Read the stamp off the report, not the envelope
Nothing in the coordinator branches on either field. frameworkVersion is not yours to branch on — it exists so that when a player reports a broken save, knowing which build wrote it is the fastest way in, and treating it as a logic input makes it a compatibility surface it was never designed to be.
savedAtUtc is different: it is handed back parsed on RevSaveReport.SavedAtUtc, and branching on that is supported. What is still unsupported is reaching the raw field — parsing the save string yourself makes the envelope's shape your problem, and it is the coordinator's.
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 | Restore order | Constructor needs |
|---|---|---|---|---|
HealthSaveParticipant | revframework.health | 1 | Default | nothing |
CurrencySaveParticipant | revframework.currency | 1 | Default | service + currency ids |
InventorySaveParticipant | revframework.inventory | 1 | Default | ItemDatabase |
CraftingSaveParticipant | revframework.crafting | 1 | Late | service + recipes |
StatusEffectsSaveParticipant | revframework.statuseffects | 1 | Default | effect factory |
Their keys, payload formats and declared restore order are part of this contract. Their internals are not.
CraftingSaveParticipant orders itself last — you no longer have to
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 either one running after Crafting erases what Crafting just produced, with nothing reported.
It declares RevSaveOrder.Late through IRevSaveOrdered, so the coordinator moves it after them whatever order you register or supply them in. Listing it last as well is harmless and reads clearly; it is no longer load-bearing.
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 - Reading
savedAtUtcout of the envelope yourself instead of fromRevSaveReport.SavedAtUtc - 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
- Assuming an
IRevSaveStorewrite is atomic, or that a failed write left the previous save intact - Reading a
FileSaveStoreslot file directly, or relying on the.tmpname it writes through - Assuming
CapturehonoursIRevSaveOrdered— it does not, and no capture order changes the file - Reflecting into participant internals
- Calling the coordinator off the Unity main thread
Related¶
- Guarantees Matrix — the same surface expressed as guarantees and non-guarantees; §12 covers the manager and the store
- Integration Surfaces — implementing
IRevSaveParticipant,IRevSaveOrderedorIRevSaveStorefor your own project - System Boundaries — what the system refuses to do, and why