CoreThe foundation: persistent assets, managers and the save API.

Core types

PersistentScriptableObject

SOabstract

Base class for your data. Inherit it and its serialized fields persist between sessions. Explained in How saving works.

Properties
PersistenceManager PersistenceManager { get; }The manager in charge of this object's persistent data, or null when none is assigned, in which case the object does not persist.

PersistenceManager

PMabstractextending

Saves and loads a persistent asset. The built-in managers derive from it; so does any custom one. Worked example: A custom persistence manager.

State & identity
bool IsReady { get; }Whether the target holds its data and is safe to read and save. The one rule.
bool IsBusy { get; }Whether an operation is running right now, for a loading indicator. Not a data-safety signal; branch on IsReady for that.
Readiness Readiness { get; }A snapshot of IsReady with why it is not ready and where the data came from. For interface and diagnostics only.
event Action<Readiness> OnReadinessChangedRaised on every readiness transition, including the ones no other event reports (a clear, a slot change, leaving scope).
Awaitable WhenReady { get; }Await (or yield) until everything that decides readiness has settled (the automatic load and its imports, a queued load or Slots.LoadFrom, an in-flight or queued clear, and any follow-up issued while waiting), then read IsReady.
Object Target { get; }The persistent asset this manager saves and loads.
string UniqueID { get; }Identifier unique to this manager.
Serializer Serializer { get; protected internal set; }The serialization system in use.
bool PayloadIsBinary { get; }Whether the payloads this manager reads and writes are binary rather than text. A storage that can only hold text base64 encodes them when it is true.
string Slot { get; set; }Current slot (multi-slot targets). Setting it switches slot as a scope transition. The setter throws InvalidOperationException in the editor only when set outside play mode.
bool UseSlot { get; }Whether this saves to and loads from multiple slots: the manager's Use Slot toggle, or what the target's IPersistencePolicy forces.
bool AlwaysGlobal { get; }Whether the target stays in scope all session and is registered in GlobalPersistedData: the manager's Always Global toggle, or what the target's IPersistencePolicy forces.
Load / Save / Clear operations
Every method below throws InvalidOperationException in the editor only when called outside play mode (never at runtime).
LoadResult LoadSync()Force synchronous; the result is final on return.
void Load()Fire and forget; wireable to a UnityEvent.
void Load(Action<LoadResult> onComplete)Default; sync when it can, else async, then callback.
Task<LoadResult> LoadAsync(ExecutionMode? executionModeOverride = null)Async; await the result.
PersistenceOperation<LoadResult> LoadRoutine()Coroutine; yield return it, then read Result.
SaveResult SaveSync()Force synchronous save.
void Save()Fire and forget save (wireable to a UnityEvent).
void Save(Action<SaveResult> onComplete)Default save, then callback.
Task<SaveResult> SaveAsync(ExecutionMode? executionModeOverride = null)Async save; await the result.
PersistenceOperation<SaveResult> SaveRoutine()Coroutine save.
ClearResult ClearSync()Force synchronous clear: deletes the slot's save, resets the target to defaults, pauses saving until the next load or slot change.
void Clear()Fire and forget clear (wireable to a UnityEvent).
void Clear(Action<ClearResult> onComplete)Default clear, then callback.
Task<ClearResult> ClearAsync(ExecutionMode? executionModeOverride = null)Async clear; await the result.
PersistenceOperation<ClearResult> ClearRoutine()Coroutine clear.
Other operations
void ResetToDefaults()Reset the target's fields to pre-play values, in memory only (the save is untouched). Throws InvalidOperationException in the editor only when called outside play mode (never at runtime).
Events
event Action<PersistenceSource> OnBeforeLoad / Action<PersistenceSource, LoadResult> OnAfterLoadAround each load that runs (with source, and result on After).
event Action<PersistenceSource> OnBeforeSave / Action<PersistenceSource, SaveResult> OnAfterSaveAround each save that runs.
event Action OnBeforeClear / Action<ClearResult> OnAfterClearAround each clear that runs.
event Func<PersistenceSource, bool> BlockLoad / Func<PersistenceSource, bool> BlockSave / Func<bool> BlockClearReturn true from a subscriber to veto the operation.
Settings (configurable in the inspector / overridable in a subclass)
bool AutoLoad / AutoSaveWhether to load on launch and save at safe points automatically.
float RegularSaveDelay / AutoLoadRetryDelayTimer cadences (use NoRegularSave / NoAutoLoadRetry to disable).
const float MinimumRegularSaveDelay / MinimumAutoLoadRetryDelay = 5 · NoRegularSave / NoAutoLoadRetry = 0A positive delay below the minimum is raised to it; the No* value (or lower) disables the timer.
ExecutionMode ReadExecutionMode / WriteExecutionModeWhich sync/async paths the manager supports, and the default.
float LoadTimeout / SaveTimeout / ClearTimeoutSeconds an async operation may run before cancellation.
For implementers (protected)
LoadResult ReadPayloadSync(PersistenceSource source, string slot)Read and return the stored payload (or Invalid / Corrupt / Failure). The base implementation throws NotSupportedException, so override the shape your storage supports.
SaveResult WritePayloadSync(PersistenceSource source, string slot, byte[] payload)Persist the payload. A text serializer's payload is the UTF8 of what it wrote.
ClearResult ClearPayloadSync(PersistenceSource source, string slot)Delete the stored data. Manual for a clear or slot delete the game requested, Automatic for the system wiping a consumed import source.
LoadResult ReadFallbackPayloadSync(PersistenceSource source, string slot)The payload the read returned would not decode: return an alternate copy of the save if your storage keeps one (the default returns Invalid, meaning none).
ReadPayloadAsync / WritePayloadAsync / ClearPayloadAsync / ReadFallbackPayloadAsyncAsync equivalents (with a CancellationToken).
bool IsInspectRead { get; }Whether the read under way only inspects what storage holds and applies nothing (a save menu, an export, SaveData.Stored). Such a read can name any slot and no operation follows it, so record nothing for a later one while it is set. Read it before the first await of an async override and keep it in a local: it describes the read you started, not the one that resumes next.
string StorageLocation(string slot)Report a stable storage path or key. Shape it so no other storage can produce the same string: the editor reports two managers resolving to one location as a conflict.
void OnEnteringScope() / OnLeavingScope()Per-session setup and teardown. OnEnteringScope runs after the scope's first automatic load was issued, so do not prepare state that load depends on here.
void OnImportingFrom(PersistenceManager source, string slot)An empty load is recovering by importing this source's save: it was just decoded into the target and is about to be re-persisted here. Override to carry storage facts your manager keeps about a save across the import.
PersistencePolicy Constraint { get; }Settings this manager's type fixes as facts of its storage: a non-null value wins over both the inspector setting and a target's IPersistencePolicy, and a policy contradicting it is reported and ignored. Override it only for what the manager cannot work without; for a different default, override the setting itself.
bool SelfBoundsAsyncOperations { get; }True when this manager enforces the operation timeouts itself, so the core never cancels one of its asynchronous operations for running long. Only for a manager whose every asynchronous path is guaranteed to return: one that hangs is never stopped.
string DisabledReasonWhy this manager cannot persist at all. null when it can. Read once as the target enters scope: a reason refuses every operation for the whole scope and is reported, so return one only where operating could lose or corrupt data (nothing to route to, no backend configured), never for a transient fault.
bool RaiseConflict(ConflictInfo conflict, Action<ConflictResolution> resolver)Report that a load found two diverging saves of the target and cannot decide between them: raises Conflicts.OnAny, and resolver runs once the game answers through Conflicts.Resolve. Return the load as a failure, since the data is undecided and IsReady must stay false until it is settled. Returns whether anybody is listening: when nobody is, nothing is pending and you have to settle the conflict yourself rather than leave the load waiting on an answer that can never arrive.
ConflictInfo NewConflict(DateTime localTime, DateTime remoteTime, byte[] localPayload, byte[] remotePayload)Build the two sides of a conflict for RaiseConflict. Each payload is one of the diverging saves, as your read would have returned it; a side that cannot be read as data reads as SaveNode.Missing rather than being refused, so the times are still shown.
SaveNode ParsePayload(byte[] payload)Read a payload as a neutral SaveNode tree, the same way a save upgrade sees one, for previewing a save without loading it. SaveNode.Missing when there is no serializer or it does not support Serializer.ParseToNode, so never null.
Composed On(PersistenceManager) (static)Another manager's protected surface, for a manager composing others (Platform (Routing) is one): On(child).ReadPayloadSync(source, slot) forwards the call, so the composite reuses that manager whole. Only drive a manager you own (see ManagerComposition).
PersistenceManager ComposedManagerThe manager this one hands its storage to. null when it stores by itself. Name it and the load, save and clear events this manager raises reach it too, which a composed manager never gets otherwise since it runs no operation of its own. Read again per event, so it may answer differently as the composition resolves, but it must hold its answer for a whole operation.
void Log(string text, LogLevel level = None)Write to the manager's inspector log (development only).

Persistence

static

Global entry point for state and operations shared across every manager. Explained in How saving works.

State & events
Awaitable WhenAllReady { get; }Await (or yield) until every active manager's load has settled.
bool AreAllReady { get; }Whether every active manager is ready right now, and true when none is active. A snapshot, not a wait.
IReadOnlyList<PersistenceManager> ActiveManagers { get; }Live view of every active manager, not a snapshot: copy it before iterating if a handler may scope one in or out, and null-check each element, since a manager being collected momentarily reads as null.
int ManagerCount { get; }Count of currently active managers.
bool IsDraining { get; }Whether a shutdown save drain is running.
string Distribution { get; set; }Which distribution of the game this build is ("Steam", "MSStore", "Demo"), for the setups the platform alone cannot tell apart. Nothing detects it: set it from your own build defines before anything loads or saves. Read by Platform (Routing), which reports once per session when the value matches none of the distributions its routes ask for, since nothing can validate a free string.
void ForceQuitDrain()Cut a running shutdown drain short (unsaved data may be lost).
event Action<PersistenceManager> OnManagerAdded / OnManagerRemovedA manager went in or out of scope.
event Action OnShutdownDrainStarted / OnShutdownDrainCompletedAround the shutdown save drain (play-mode exit or app quit).
event Action<PersistenceManager, PersistenceSource> OnBeforeAnyLoad / Action<PersistenceManager, PersistenceSource, LoadResult> OnAfterAnyLoadAround any manager's load: (manager, source), and (manager, source, LoadResult) on after.
event Action<PersistenceManager, PersistenceSource> OnBeforeAnySave / Action<PersistenceManager, PersistenceSource, SaveResult> OnAfterAnySaveAround any manager's save (with SaveResult on after).
event Action<PersistenceManager> OnBeforeAnyClear / Action<PersistenceManager, ClearResult> OnAfterAnyClearAround any manager's clear (with ClearResult on after).
bool IsAnyBusy { get; }Whether any manager is running an operation right now.
ScriptableObject[] GetActiveTargets()Every active manager's target, for the operations that take objects. A fresh array each call, unlike ActiveManagers, so it needs neither a copy nor a null check.
Operations on every active manager
Every method below throws InvalidOperationException in the editor only when called outside play mode.
LoadResult[] LoadAllSync()Force synchronous; every result is final on return.
void LoadAll()Fire and forget (wireable to a UnityEvent).
void LoadAll(Action<LoadResult[]> onComplete)Default; callback with every result.
Task<LoadResult[]> LoadAllAsync(ExecutionMode? executionModeOverride = null)Async; await every result.
PersistenceOperation<LoadResult[]> LoadAllRoutine()Coroutine; yield then read Result.
SaveResult[] SaveAllSync() / void SaveAll() / SaveAll(Action<SaveResult[]>)Save every manager: sync, fire-and-forget, callback.
Task<SaveResult[]> SaveAllAsync(ExecutionMode? = null) / PersistenceOperation<SaveResult[]> SaveAllRoutine()Save every manager: async, coroutine.
ClearResult[] ClearAllSync() / void ClearAll() / ClearAll(Action<ClearResult[]>)Clear every manager: sync, fire-and-forget, callback.
Task<ClearResult[]> ClearAllAsync(ExecutionMode? = null) / PersistenceOperation<ClearResult[]> ClearAllRoutine()Clear every manager: async, coroutine.

SaveData

static2.0

A manager's data as portable text, and where it is stored. Every method takes the manager to act on and does nothing when it is null.

Members
string Snapshot(PersistenceManager)Capture the target's current values into a string for a checkpoint. Null when there is no serializer or the encode fails. Throws InvalidOperationException in the editor only when called outside play mode (never at runtime).
bool Restore(PersistenceManager, string snapshot)Restore from a Snapshot string, in memory only (the save is untouched). False when the string is null, could not be fully applied (it would not decode, or its upgrade or the target's OnAfterDeserialize did not finish), or persistence is disabled. Throws InvalidOperationException in the editor only when called outside play mode (never at runtime).
string Stored(PersistenceManager, string slot = null) / Task<string> StoredAsync(...)What storage holds, in the same portable string Snapshot produces and Restore takes, applied to nothing: Snapshot answers what a save would write, this answers what a load would read. Reads any slot, works outside play mode, and is a direct read rather than a queued operation. Null when nothing is stored there, the read failed, or the manager reads asynchronously only.
string LocationOf(PersistenceManager) / LocationOf(PersistenceManager, string slot)Where the manager stores its data (the current slot, or a given one), or null (a normal answer for remote managers).

GlobalPersistedData

static

Reach a registered persistent asset from anywhere, by type, with no serialized reference. A target whose manager is Always Global registers itself; you can also register your own. Explained in Save and load.

T Find<T>(Func<T, bool> filter = null) where T : classThe first registered target assignable to T (a concrete type or interface) matching the optional filter, or null.
IEnumerable<T> FindAll<T>(Func<T, bool> filter = null) where T : classEvery registered target assignable to T matching the filter, as a fresh snapshot.
void Add(ScriptableObject target) / AddPermanent(ScriptableObject target)A target that is not a persistent scriptable object is refused, with a warning in the console. Register a target manually (AddPermanent cannot be undone by Remove).
bool Remove(ScriptableObject target)Unregister a target added with Add; returns whether it was removed.
int Count { get; } / bool Contains(ScriptableObject target)How many targets are registered, and whether a given one is. A target destroyed since it was added counts for neither, exactly as Find no longer returns it.

Operations & results

Result

abstract

Base for operation outcomes. Use the intent-named flags rather than the raw Type. Explained in How saving works.

Members
ResultType Type { get; }The outcome category.
bool IsSuccess / IsFailure / IsIgnored / IsCancelled / IsBusyIntent-named checks on the outcome.
bool IsTimedOut { get; }Refines IsCancelled: the operation overran its LoadTimeout, SaveTimeout or ClearTimeout rather than being superseded or abandoned at scope exit. Storage never answered, which is what closes a panel opened on OnBeforeLoad.
string Message { get; }Optional explanation (often set on Ignored / Failure).
Exception Exception { get; }Set when Type is Exception.
double ElapsedMilliseconds { get; }Operation duration (NaN in non-development builds).
string ToString() / void Deconstruct(out ResultType, out string, out Exception[, out double])Readable summary; deconstruct into its fields (with or without the elapsed time).

LoadResult

sealed

A load outcome. Adds IsInvalid (no usable data, the normal first run) and IsCorrupt, which refines it: the save was there and could not be read, rather than the ordinary first run. Factory methods Success(byte[] payload, string message = null, byte[] mergeWith = null), Invalid(string message = null), Corrupt(string message = null), Failure(string message = null) for managers. Returning the right one is what keeps saves safe: Invalid and Corrupt leave the target on its values with saving allowed, so the next save overwrites; Failure means the storage is temporarily unavailable and leaves the target not ready, so no save can overwrite what a retry could still recover. Explained in How saving works.

SaveResult

sealed

A save outcome. Factory methods Success(string message = null), Failure(string message = null). Explained in How saving works.

ClearResult

sealed

A clear outcome. Factory methods Success(string message = null), Failure(string message = null). Explained in How saving works.

ResultType

enum
SuccessThe operation succeeded.
FailureFailed; for a load, temporarily unavailable, a retry might help.
ExceptionCode threw.
CancelledStarted but abandoned (timeout, superseded, left scope).
InvalidLoad only: no usable data (missing or corrupt). The normal first run.
IgnoredNever ran (not dirty, blocked, no slot, not loaded yet, shutting down, superseded while queued, busy).

ExecutionMode

enum
PreferSync / PreferAsyncSupports both; defaults to the named one.
SyncOnly / AsyncOnlyOnly that path; as an override, fails when unsupported.

PersistenceSource

enum

Why a load or save ran, carried by its events and by a manager's storage methods. Manual: an explicit Save or Load call. Automatic: the auto-load on scope-in, an auto-save safe point, or a slot change. SlotCopy and SlotSnapshot: a Slots copy or rename, and a Slots.SaveTo, which only a custom manager's storage methods see. Explained in Save and load.

ManualA load/save/clear method was called.
AutomaticTriggered by the system (launch, safe point, timer, import-source read or wipe).
SlotCopyA Slots copy or rename is reading or writing a slot's storage, moving the payload verbatim. Only a custom manager's read and write methods see it; the events never carry it.
SlotSnapshotA Slots.SaveTo is writing the target's live data to a slot other than the current one: like a manual save, aimed elsewhere. Only a custom manager's read and write methods see it; the events never carry it.

PersistenceOperation<TResult>

sealed

Coroutine handle on a running operation (where TResult : class; TResult is a Result or Result[]). yield return it; Result (null while running), IsDone.

Readiness

Readiness

struct2.0

A snapshot of a manager's readiness, taken when read. IsReady remains the only data-safety signal; Reason and Origin are for interface and diagnostics. Explained in How saving works.

bool IsReady { get; }Whether the target holds its data and is safe to read and save.
NotReadyReason Reason { get; }Why it is not ready, or None while it is.
DataOrigin Origin { get; }Where the data the target holds came from.
implicit operator boolReads as its IsReady, so a readiness can be tested directly.
string ToString()Ready (Origin) or Not ready (Reason), for a log line.

NotReadyReason

enum2.0
NoneThe manager is ready.
NotLoadedYetNo load has been attempted yet, or the scope just changed.
LoadingA load is running right now.
LoadFailedThe last load could not complete; a retry may still succeed.
NoSlotIt saves per slot and no slot is selected.
ClearedThe data was cleared, so saving is paused until the next load.
ScopedOutThe target is not in scope.
PersistenceDisabledThe manager cannot persist its target, so every operation is refused.
AwaitingConflictResolutionA load found a conflict the game has to settle.

DataOrigin

enum2.0

Where the data the target holds came from, which is what answers Continue versus New Game. Not PersistenceSource, which says what triggered an operation.

NotLoadedNothing has been loaded into the target.
StoredSaveA stored save was read and applied: a returning player.
DefaultsNothing usable was stored, so the target holds its defaults: a fresh start.
ImportedA previous configuration's save was recovered and re-persisted.

Optional interfaces

IUpgradable

iface

Migrate old saves forward. int SaveVersion, void Upgrade(int fromVersion, SaveNode oldData), bool Downgrade(int fromVersion). Explained in Update a shipped save.

IMergeable

iface2.0

Fold two diverging saves of one object together instead of discarding one. bool Merge(int fromVersion, SaveNode otherData), returning whether anything changed. Needs a serializer supporting ParseToNode; without one there is no other side to fold in and the manager keeps the most recent save instead. Must be commutative and idempotent, or concurrent devices never settle. The asset already holds the more recent of the two, so fold otherData into it; otherData is never null, a missing value reading as its default, and fromVersion is the other save's version, which only matters if your field names changed. Implementing it settles the conflict, so a remote manager's conflict policy is not consulted and Conflicts.OnAny never fires for the object. See Merge two saves.

IFieldResettable

iface

Reset or restore individual fields. Events DefaultValuesRequested and CurrentValuesRequested return a SaveNode (do not subscribe from your own code).

IScopeCallbacks

iface

OnBeforeScopeIn(string slot, bool enable), OnAfterScopeOut(string slot, bool disable). Both run on every scope change, a slot change included. enable is true only on the session's first scope-in and disable only on its last scope-out, so one-time setup and teardown go behind those two. Explained in Save and load.

ISerializationCallbacks

iface

Runs your own code around the save itself: OnBeforeSerialize() just before the object is written, to flatten what the serializer cannot store (a dictionary into two lists, a cache into a field), and OnAfterDeserialize() right after it is read back, to rebuild and clamp it. Unity's ISerializationCallbackReceiver does the same for its own serializer; this one fires whichever serializer the manager uses. Explained in React to load and save.

IDirtyTracked

iface

Skip saves while unchanged. bool IsDirty { get; set; } (reset to false after a successful save). Explained in Save and load.

IDynamicInitialize

iface

void DynamicInitialize() runs once after a load found no save (never overwrites a save, never in edit mode). Explained in Save and load.

IPersistencePolicy

iface

PersistencePolicy Policy { get; }. Every non-null field of the PersistencePolicy struct overrides the matching manager setting. Explained in Save and load.

PersistencePolicy

struct

The manager settings an IPersistencePolicy object can drive; a null field leaves the manager's own setting in charge. Explained in Save and load.

bool? AutoLoad { get; set; } / bool? AutoSave { get; set; }Non-null values override the manager's AutoLoad / AutoSave.
bool? UseSlot { get; set; } / bool? AlwaysGlobal { get; set; }Non-null values override the manager's Use Slot / Always Global toggles, for a type that has no choice (a slot registry cannot itself live in a slot).
static readonly PersistencePolicy DefaultDrives nothing (every field null).

Save slots

Slots

static

Operate on a slot's saved data (copy, rename, delete, snapshot to, load from). Every operation ignores a target whose manager does not use slots, and says so in the console. Explained in Slots and save menus.

Delete / Copy / Rename / SaveTo / LoadFrom (all shapes) throw InvalidOperationException in the editor only when called outside play mode.
Members
string GlobalSlot { get; set; }Slot shared by every manager; setting it switches all of them. A manager whose UseSlot is off ignores it. The setter throws InvalidOperationException in the editor only when set outside play mode.
ClearResult[] DeleteSync(string slot, params ScriptableObject[] targets) / void Delete(...) / Delete(string slot, Action<ClearResult[]> onComplete, ...)Delete a slot's storage for each target: sync, fire-and-forget, callback. Permanently deletes data, no undo. Refused on a target whose manager has that slot current: empty the current slot with ClearSync. Takes its turn in the manager's queue like any other operation, so it reports the manager busy while it runs; the sync shape is refused Busy while anything asynchronous is in flight, and any shape is refused from inside another operation's event handler.
Task<ClearResult[]> DeleteAsync(...) / PersistenceOperation<ClearResult[]> DeleteRoutine(...)Delete a slot's storage: async, coroutine.
SaveResult[] CopySync(string fromSlot, string toSlot, params ScriptableObject[] targets) / void Copy(...) / Copy(string fromSlot, string toSlot, Action<SaveResult[]> onComplete, ...)Copy a slot's storage onto another: sync, fire-and-forget, callback. Overwrites the destination, and copies the saved data, so a loaded target's unsaved changes are not included. Refused when the destination is the manager's current slot; copies nothing when the source holds no save.
Task<SaveResult[]> CopyAsync(...) / PersistenceOperation<SaveResult[]> CopyRoutine(...)Copy a slot's storage: async, coroutine.
SaveResult[] RenameSync(string fromSlot, string toSlot, params ScriptableObject[] targets) / void Rename(...) / Rename(string fromSlot, string toSlot, Action<SaveResult[]> onComplete, ...)Rename a slot (copy then delete, in that order, so a failure half-way leaves a duplicate and never a loss): sync, fire-and-forget, callback. Moves data, no undo. Refused when either slot is the manager's current one; renames nothing when the source holds no save.
Task<SaveResult[]> RenameAsync(...) / PersistenceOperation<SaveResult[]> RenameRoutine(...)Rename a slot's storage: async, coroutine.
SaveResult[] SaveToSync(string slot, params ScriptableObject[] targets) / void SaveTo(...) / SaveTo(string slot, Action<SaveResult[]> onComplete, ...)A real save aimed at another slot: snapshot each target's live data onto that slot's storage (the current slot's save is untouched): sync, fire-and-forget, callback. Raises the save events; overwrites the destination; refused on the current slot, a not-ready target, or when saving is blocked.
Task<SaveResult[]> SaveToAsync(...) / PersistenceOperation<SaveResult[]> SaveToRoutine(...)Snapshot to a slot: async, coroutine.
LoadResult[] LoadFromSync(string slot, params ScriptableObject[] targets) / void LoadFrom(...) / LoadFrom(string slot, Action<LoadResult[]> onComplete, ...)A real load aimed at another slot: bring its saved data into the ongoing game (load events, save upgrades and readiness included); the next save persists it onto the current slot, and the source slot is never modified. Refused on the current slot, with no slot set, or while the manager is protecting a loaded newer-version save (see IUpgradable.Downgrade); does nothing when the source holds no usable save.
Task<LoadResult[]> LoadFromAsync(...) / PersistenceOperation<LoadResult[]> LoadFromRoutine(...)Load from a slot: async, coroutine.
event Action<PersistenceManager, string> OnSlotUpdated / Action<PersistenceManager, string> OnSlotDeleted / Action<PersistenceManager, string, string> OnSlotCopiedRaised as slot storage changes (a SaveTo snapshot raises OnSlotUpdated with the written slot).

PersistentSlotRegistry<TTarget, TInfo>

SOabstract

Always-global object that records one entry per save slot with a summary you define, without loading the saves. TTarget is constrained to ScriptableObject. Build a save-select menu on it.

Public
IReadOnlyList<SlotEntry<TInfo>> Entries { get; }Every recorded slot, each with its summary, save time and optional screenshot, most recently saved first (the natural order for a load-game menu). A fresh snapshot each call: cache it if you iterate a lot.
bool TryGet(string slot, out SlotEntry<TInfo>) / TryGetLatest(...)Look up one slot, or the most recently saved.
string LatestSlot { get; }Slot of the newest save (for a "Continue" button).
IReadOnlyList<string> SavedSlots { get; }Just the slot names, most recently saved first.
string NextRingSlot(string prefix, int count)The slot a quick save writes next: a free one while the ring has room, then the oldest of it.
string NextFreeSlot(string prefix)A slot nothing has been saved to yet, for starting a new game.
PersistencePolicy Policy { get; }Pins four settings: auto-load on, auto-save off, UseSlot off and AlwaysGlobal on. A registry lists the slots, so it can never live inside one, and it must be reachable and current wherever a menu asks. It saves itself on every change instead of on a timer.
For implementers (protected)
TInfo CaptureInfo(TTarget target) (protected abstract)Return the per-slot summary to record on each save.
Awaitable<SaveScreenshot> CaptureScreenshotAsync(TTarget) / SaveScreenshot CaptureScreenshot(TTarget) (protected)Optional per-slot preview image (override one; async is recommended). See SaveScreenshot.
bool ShouldRecord(TTarget) (protected virtual)Whether this registry lists a given object's slots. The default lists every one of the type; override it when a project holds several and each has its own registry.

SlotEntry<TInfo>

struct

Read-only record of one slot in a PersistentSlotRegistry. Explained in Slots and save menus.

string Slot { get; }The slot this entry describes.
TInfo Info { get; }The summary your CaptureInfo returned.
DateTime SavedUtc { get; } / long SavedUtcTicks { get; }When the slot was last saved (UTC). Out-of-range ticks in a tampered or corrupt registry save are clamped into the valid DateTime range rather than throwing.
SaveScreenshot Screenshot { get; }The slot's preview image, empty when none was recorded.

ISlotListing

iface2.0

Nothing to implement: PersistentSlotRegistry already does, and the no-code components use it to read any registry without knowing what it summarizes (SavedSlots, LatestSlot, NextRingSlot, NextFreeSlot). Take it as a parameter type to write a menu script that works with any registry.

SaveScreenshot

struct

A save-slot preview image that serializes as a string, so it rides inside a slot summary. Capture it with the static methods, then decode it on demand for a save-select menu. Its ScreenshotFormat is Jpg (smaller, lossy, no alpha) or Png (lossless).

Needs Unity's Image Conversion module (com.unity.modules.imageconversion), which every project has unless it was removed under Window > Package Manager > Built-in packages. Without it a capture answers an empty screenshot, a decode answers null, and the console says so. CaptureScreen also needs Screen Capture (com.unity.modules.screencapture); the camera and texture captures do not.
static SaveScreenshot Capture(Camera camera, int maxSize = 256, ScreenshotFormat format = Jpg, int jpgQuality = 75)Capture a camera's view at the wanted size (also a Texture overload).
static Awaitable<SaveScreenshot> CaptureAsync(...)The recommended capture: no main-thread stall on the GPU read back. Camera and Texture overloads.
static SaveScreenshot CaptureScreen(int maxSize = 256, ScreenshotFormat format = Jpg, int jpgQuality = 75) / static Awaitable<SaveScreenshot> CaptureScreenAsync(...)Capture the whole screen, sync or async (async recommended).
Texture2D CreateTexture() / Sprite CreateSprite()Decode into a new texture or sprite the caller owns and must Object.Destroy when done. CreateSprite owns both the sprite and its texture: destroy sprite.texture first, then the sprite.
bool HasValue { get; }Whether this holds an image. A slot saved without one hands back a default SaveScreenshot, so a row checks this before calling CreateSprite.
int Width { get; } / Height { get; } / float Aspect { get; }The image's size, without decoding it.

ScreenshotFormat

enum
Jpg / PngEncoding of a SaveScreenshot: JPEG (smaller, lossy, no alpha) or PNG (lossless).

Conflicts

Conflicts

static2.0

Hear about save conflicts and answer them. A conflict is raised when a load finds that this device and another one both saved the same object since they last agreed; the load stays undecided and the target not ready until something answers. Explained in Ask the player.

Members
event Action<PersistenceManager, ConflictInfo> OnAnyA manager's load found a conflict, with that manager and its ConflictInfo. Never raised for an IMergeable target, and a manager set to Ask with nobody subscribed keeps the most recent save instead of waiting.
bool IsAsking(PersistenceManager)Whether that manager is waiting for a conflict to be answered.
ConflictInfo Pending(PersistenceManager)The conflict waiting on that manager, or null.
void Resolve(PersistenceManager, ConflictResolution)Answer it, which finishes the load with the save you kept. A no-op with nothing pending, and a second answer is ignored, so two panels cannot fight.
void ResolveAll(ConflictResolution)Answer every conflict waiting right now the same way, so a game asks the player once rather than per object.

ConflictInfo

sealed2.0

The two sides of a save conflict, for showing the player what they are choosing between: DateTime LocalTime / RemoteTime, and SaveNode LocalData / RemoteData (Missing when the serializer has no node support, never null).

ConflictResolution

enum2.0

KeepLocal, KeepRemote. The answer to Conflicts.Resolve.

Player UI

PersistenceConflictPrompt

MB2.0

Asks the player which of two saves to keep, when a load finds that this device and another one both saved the same object without seeing each other's changes. It draws both sides with their times, marks the more recent one, and answers the conflict with whichever is picked. Add it to a GameObject, or drop in the Persistent Asset Conflict Prompt prefab; it needs no wiring. See Ask the player.

bool IsAsking { get; }Whether a conflict is waiting to be answered, so the panel is on screen. Gate your own input on this: the panel draws over the game without stopping it.
void KeepLocal() / KeepRemote()Answer as the panel's own buttons do. Never throws, and does nothing when no conflict is waiting.
Inspector: Preview FieldsValues read from both saves and shown side by side, each a PreviewField naming the row and the field to read (nested with dots: stats.level), so the player compares "Chapter 5" against "Chapter 3" rather than two dates. Needs a serializer that supports ParseToNode; every row reads as - otherwise.
Inspector: Answer Every ObjectAnswer every object waiting on a conflict with the one choice, rather than asking again per object. On by default.
Inspector: Title / Explanation / Local Heading / Remote Heading / Keep Label / Newest TagEvery word on the panel, for rewording and translating it.
Inspector: Scale MultiplierSize on top of the screen's DPI scaling.
Inspector: Panel Color / Dim Color / Text Color / Highlight ColorThe panel, the wash over the game behind it, its words, and the mark around the save the keyboard is on. Changing one while the game runs is seen on the next frame.
A pad cannot answer it, since IMGUI is never sent pad input: a game played on a pad uses GameObject > Persistent Asset > Conflict Prompt (uGUI) instead, or calls KeepLocal() or KeepRemote() from its own UI. See Ask the player.
The panel only appears on a manager set to Ask. ConflictPolicy, a remote manager setting, defaults to PreferNewest: opt in by overriding it with a serialized auto-property (see Ask the player). One that asks with nothing in the scene to answer keeps the most recent save instead, and the policy is only consulted while CacheMode enables a cache.

PersistenceDrainDisplay

MB2.0

Tells the player why the game is not closing yet: while the shutdown drain waits for a save in flight, it draws "Saving your progress" over everything. Ships in every build, unlike the debug overlay, and appears only once the wait outlasts half a second so a quick save never flickers. Add it to a GameObject, or drop in the Persistent Asset Drain Display prefab; it needs no wiring, it listens to the drain events.

Inspector: MessageWhat it says while it waits. Defaults to WaitingText.
const string WaitingTextThe default wording, "Saving your progress...", so a display of your own says what this one says.
Inspector: Allow Force Quit / Force Quit DelayOffer a button abandoning the wait, and how long the wait must last first. Off by default: a player must never meet a one-tap way to lose their save. The delay defaults to 10 seconds.
Inspector: Force Quit LabelWhat that button says. Defaults to ForceQuitText.
const string ForceQuitTextThe default caption, "Quit anyway".
bool AllowForceQuit { get; set; }Whether that button is offered at all, for a game deciding while it runs.
Inspector: Scale MultiplierSize on top of the screen's DPI scaling.
Inspector: Panel Color / Text Color / Highlight ColorThe panel, its words, and the mark around the button when the keyboard is on it. Changing one while the game runs is seen on the next frame.
Development builds name what is still writing, under the message; a release build shows the message alone. The button answers the mouse, a touch and the keyboard (the first key press only marks it, so no single keystroke abandons a save in flight), but not a pad: call Persistence.ForceQuitDrain() from your own control for that.

Serialization

Serializer

abstractextending

Custom serialization. Must not fail on valid data. Worked example: A custom serializer.

string SerializeToString(object obj, string backReferenceFieldName)Object to string.
bool DeserializeFromString(string str, object obj)String into object; returns success.
bool IsBinary { get; }Whether this serializer writes raw bytes; the package then calls the byte pair below instead of the string pair. Default false.
byte[] SerializeToBytes(object obj, string backReferenceFieldName)Object to bytes; only called while IsBinary.
bool DeserializeFromBytes(byte[] bytes, object obj)Bytes into object; returns success. Only called while IsBinary.
SaveNode ParseToNode(string str)Parse to a neutral tree for upgrades (null if unsupported). Only called while IsBinary is false.
bool SupportsNode { get; }Whether ParseToNode returns a usable tree; override it to true when you implement ParseToNode. Default false.

SaveNode

abstract

Read-only, format-neutral view of a saved payload. It is what IUpgradable.Upgrade reads an old save from, what IMergeable.Merge reads the other device's save from, what IFieldResettable's two events hand back, and what Serializer.ParseToNode produces. Explained in Update a shipped save.

this[string key] / this[int index]Object member / array element (or Missing).
bool Exists { get; } / int Count { get; }Presence and array length.
AsString / AsBool / AsChar / AsEnum<T>Typed readers, each taking the fallback it answers with when the value is missing or will not convert.
AsByte / AsSByte / AsShort / AsUShort / AsInt / AsUInt / AsLong / AsULong / AsFloat / AsDoubleThe number readers, the integer and floating-point readers, with the same fallback rule.
AsVector2 / AsVector3 / AsVector4 / AsVector2Int / AsVector3Int / AsQuaternion / AsColor / AsColor32The Unity-type readers, read from the same member shape the serializers write.
Missing (static) · SaveNode Object(Dictionary<string, SaveNode> members) / Array(List<SaveNode> elements) / Value(string text)Build nodes (for a custom serializer).
IEnumerator<SaveNode> GetEnumerator()SaveNode is IEnumerable<SaveNode>: foreach over an array node's elements.

Asset references

AssetRegistry

static

The project-wide table of assets save data may point at, and the lookup turning a stored id back into its asset. It is what lets a reference to a project asset survive a save. Filled in the editor (see AssetRegistration in the Editor Extension area); a registered asset is included in the build and loaded at startup. Reach for it when writing your own Serializer, which has to save a reference as its id.

bool TryResolve(string id, out Object asset)Resolves a stored id, false when empty or no longer registered.
T Resolve<T>(string id) where T : ObjectThe same, typed: null when the id is empty, no longer registered, or registered to an asset of another type.
bool TryResolve<T>(string id, out T asset) where T : ObjectThe typed pair of the two above, for a custom Serializer branching on whether a reference resolved.
string IdOf(Object asset)The id an asset is registered under, or null. Never derives one from the project on the fly, so an unregistered asset fails the same way in the editor and in a build.
bool IsRegistered(Object asset) / int Count { get; }Whether an asset can be saved, and how many are registered.
string UnsavableMessage(Object asset, string consequence)The message to report when a reference cannot be saved, telling an asset nobody registered apart from an object created at runtime, which is not an asset at all and needs its own data saved instead. Use it in a custom Serializer so both cases read the same everywhere.

Debugging

LogLevel

enum
None / Info / Warning / ErrorSeverity of a persistence log message, used with the manager's Log().

PersistenceDebugOverlay

MB2.0

An on-screen panel showing what persistence is doing in a running game: every active manager, and for the selected one its state, its logs, what its storage holds against what the object holds, and buttons forcing a load, a save or a clear. It is the inspector's information on a device. Add it to a GameObject, or drop in the Persistent Asset Debug Overlay prefab. Editor and development builds only: a release build removes it on Awake unless Keep In Release Builds is on. See Debugging.

bool IsOpen { get; set; } · void Toggle()Whether the panel is showing, and the toggle to bind to your game's own debug key.
KeyCode ToggleKey { get; set; }The key that opens and closes it (F8 by default). Read from the GUI event stream, so it needs neither input backend.
bool KeepInReleaseBuilds { get; }Whether it survives a release build. It then draws no handle, leaving the toggle key and the corner gesture below as the ways in.
Inspector: Release Unlock Corner / Release Unlock TapsThe corner to tap to open it (a DebugOverlayCorner, TopLeft by default) and how many taps inside it, within two seconds, open the panel. The gesture runs in every build, so it can be tried here rather than only after shipping.
Inspector: Scale Multiplier / Open On StartSize on top of the screen's DPI scaling, and whether it starts open.

DebugOverlayCorner

enum2.0

None, TopLeft, TopRight, BottomLeft, BottomRight. Which corner opens a PersistenceDebugOverlay when tapped. None turns the gesture off, leaving the toggle key and your own code as the ways in.

PersistentAssetConsole

staticextending

For code that extends the package: writes to the Unity console with the standard colored "[Persistent Asset ...]" prefix. Pass the sub-module name as module (e.g. nameof(BuiltIns)), or null for Core.

void Log(string module, string message, Object context = null)Informational message.
void LogWarning(string module, string message, Object context = null) / LogError(...)Warning / error message.
void LogException(Exception exception, Object context = null)Log an exception (native entry, no prefix).

Attributes

Attributes

attr
[PersistentScriptableObject(string persistenceManagerFieldName)]
string PersistenceManagerFieldName { get; }
Make a ScriptableObject persistent without inheriting the base class, naming its serialized PersistenceManager field.
[InspectorDisplay(string name, string description = null)]
string Name / Description { get; }, int Order { get; set; }, string Category { get; set; }
Display name, description, ordering and submenu for a manager, serializer or tooling method in the inspector / its menu. The constructor throws ArgumentException when name is null or whitespace.
[Breaking(string warning = null)]
string Warning { get; }
Mark a manager field whose change would orphan existing saves, so the inspector warns and locks it.
[PersistenceManagerInfo]
StorageKind StorageKind { get; set; }
Declare type-level facts of a manager class (inherited by subclasses). StorageKind: Durable (real player saves, the default without the attribute), Transient (nothing recoverable: never lockable, no import copy kept of it), or Test (a fake manager: assigning it always keeps an import copy of the replaced setup, restorable in one click, and it never ships: a non-development build fails while one exists, a development build warns).
[RequiredSerializer(Type serializerType)]
Type SerializerType { get; }
Lock a persistent asset to a specific serializer (inherited by subclasses).
[FolderPath(string emptyHint = null, bool createdOnDemand = false)]
string EmptyHint { get; }, bool CreatedOnDemand { get; }
Draw a string field as a project folder: the path under Assets, typed or picked or dropped in, stored whole as "Assets/...". Anything that is not a folder under Assets is refused. emptyHint is the grayed text an empty field shows ("None" without one), and createdOnDemand says the feature reading the setting creates the folder itself, so a path with nothing behind it is not marked with a warning. Editor only, through PersistentAssetEditorGUI.FolderField.