Skip to content

Save — Teaching Panels

The Teaching folder contains IMGUI-based panels used for learning, debugging, and validating the save coordinator directly in-scene.

These panels act as hostile consumers — they use only the public API and demonstrate real behaviour without shortcuts.

Status: Teaching / development-only UI (not for production) Define Guard: REV_TEACHABLES

⚠️ Teaching panels are not stripped automatically. Leaving them in a shipped scene will expose them at runtime.


Panel Overview

Panel Focus Notes
SaveCoordinatorPanel Capture / restore across participants Failure isolation, carry-over, the real payload

What the panel demonstrates

  • Capturing and restoring across several participants at once
  • Reading the real payload string the coordinator produced, not a summary of it
  • Failure isolation — one participant throwing does not stop the others, on capture and on restore
  • Carry-over of unrecognised sections — data belonging to a participant that is not in the scene survives a save/load round trip instead of being quietly dropped
  • Reading a RevSaveReport: per-section outcomes, and whether the operation as a whole succeeded

Both participants are defined in the panel's own file, in about fifteen lines each. That is the skill worth taking away — your game state joins the same file the framework's systems use, through the same interface, with no privilege difference.

It teaches the contract, not storage. Nothing here writes a file. That is one layer up: RevSaveManager holds the participant list, addresses saves by slot, writes through a replaceable IRevSaveStore — one file per slot under Application.persistentDataPath by default — and carries forward the sections a load could not place. See Documentation/Save/.


What it does not demonstrate, and why it matters

The panel is a first hour, not a specification. Three parts of the coordinator's contract are load-bearing for real persistence and are not visible in it. If you are building save/load from this panel alone, read this section as well.

Unapplied versus PartiallyApplied

When a participant's Restore throws, the coordinator sorts the section into one of two buckets, and the difference decides whether you may keep the data:

Bucket What happened What you may do with it
RevSaveReport.Unapplied The participant threw before applying anything. The section is still exactly what the save said. Safe to carry into the next save. Otherwise it exists only until the catch block ends, and the next save overwrites it with no record of the loss.
RevSaveReport.PartiallyApplied The participant threw after applying some of it. Part of that section is live state now. Do not carry it over. Writing it into a later save would overwrite the owners that did load correctly.

A report that only told you "this section failed" could not support either decision. That is why the two exist as separate collections rather than one failure list — and why treating them alike is the mistake worth naming here.

Related: RevSaveReport.Displaced reports a carried-over section dropped because a live participant wrote the same key. For a section carried from Unapplied, that is the moment the older copy stops existing anywhere, which is precisely why it is reported rather than dropped in silence.

Version handling, in either direction

The panel cannot show you this working, and it is worth saying plainly rather than implying otherwise.

PlayerProgressParticipant does carry the guard, and it is worth reading in the source:

public void Restore(string payload, int version)
{
    if (version > Version)
        throw new NotSupportedException($"Save is version {version}; this build reads {Version}.");

    _state = JsonUtility.FromJson<State>(payload) ?? new State();
}

But no control in the panel can make that line run. Every payload it can restore was captured by that same participant at version 1, the pre-baked foreign payload is version 1 and belongs to a key nothing here claims, and nothing in the UI edits a version. The pattern is visible; the behaviour is not reachable.

Two things follow that the panel therefore cannot teach you:

  • Refusing a newer payload is the participant's decision, not the coordinator's. The guard above is ordinary participant code. The coordinator's part is what happens after the throw: it isolates the exception, records the section, and carries on with the others. It has no opinion about versions and never compares one — it only delivers the version the payload was written with.
  • Migrating an older payload forward is the other direction, and the normal case in a game that has released more than once. Restore receives the section's version alongside its payload for exactly that purpose. Branch on it and upgrade in place.

Whichever you write, run the guard before you touch anything. A participant that refused without applying anything lands in Unapplied and stays carryable; one that had already mutated should throw RevSavePartialRestoreException, or call RevSaveRestore.MarkMutated() first, so it lands in PartiallyApplied instead. That is the distinction the section above is about.

Storage

The panel stops at the string. Slots, where the bytes go, and what a failed write does or does not promise are all RevSaveManager and IRevSaveStore, one layer up — see Documentation/Save/PublicAPI. Building persistence from this panel alone leaves you writing that layer yourself, including the carry-over plumbing the manager already has.


  • Documentation/Save/ — the coordinator's guarantees, report structure, and participant contract
  • Runtime/Core/Save/ — RevSaveCoordinator, RevSaveReport and the participant interfaces, plus RevSaveManager, IRevSaveStore and FileSaveStore, the layer this panel deliberately stops below
  • Integrations/Save/ — the five per-system participants, which implement the same interface the two in this panel do