Copying one saved object
This page uses GameObject from Unity. The glossary says what each one is.
This is the full list of what the API gives you. Text in brackets at the end of a note is extra detail you can skip on a first read.
A savable object is an object the game saves, such as the player’s inventory. Capture copies its entry into text, and Apply puts text back into it. Neither writes to disk.
SaveData copy a saved object to text and back
Capture copies a saved object into text and Apply puts that text back, without touching the disk. CaptureByGuid and ApplyByGuid do the same by GUID, and you should use the overload with an allowed list whenever another player can choose the GUID.
public static class SaveData
{
public static string Capture(UnityEngine.Object target);
public static bool Apply(UnityEngine.Object target, string json);
public static string CaptureByGuid(string guid);
public static bool ApplyByGuid(string guid, string json);
public static bool ApplyByGuid(string guid, string json, ICollection<string> allowedGuids);
public static string SelfCheck();
}
| Method | Returns | What it does |
|---|---|---|
Capture(target) | string | The object’s save entry as compact text. null when there is none. |
Apply(target, json) | bool | Puts a save entry into the object. true when the game took it. |
CaptureByGuid(guid) | string | The same as Capture, for the object with this GUID. null when none. |
ApplyByGuid(guid, json) | bool | Obsolete: trusts any GUID. Use the overload with a list. |
ApplyByGuid(guid, json, allowedGuids) | bool | The same as Apply, only for the GUIDs in the list. |
SelfCheck() | string | Runs a built-in test. |
Things to know
Capturegives the entry exactly as the game would write it in a save. It is compact text in the game’s{"GUID":..,"Data":{..}}form. (Capturecalls the object’s ownOnSavingGame()with the game’s save lists swapped for private ones. The save method’s other side effects run too. For example,Inventorycopies its slots into the fields it saves, and into private fields its load method reads.)Applyputs the entry into the object the way a load does. It givestrueonly when the game took the entry. (it callsOnLoadingGame()with the entry in the load lists. Steps after the deserialize run too: forInventorythat rebuilds the live slots and callsStartInitandForceCharacterItemsUpdate.)Applychecks everything before it changes anything. The text is size and depth checked, then parsed. The GUID must equal the object’s GUID, and the parsed data must have exactly the object’s type. If any check fails, the object is not touched.Applysets the entry’s type to the object’s own type, whatever the entry says. That way the game builds an object of the right type. An entry with no type gets one the same way (the currentGlobals.tnmtwritesInventorythis way). (Applysets the$typeonDatato the target’s type name.)Applyrefuses entries that could make the game build the wrong thing. It looks inside arrays, generic types and base types too, for exampleUnityEngine.GameObject[],List<GameObject>andBehaviorDesigner.Runtime.SharedGameObjectList. Types from the save’s own library and the game’s are allowed, because the game’s own saves use them (for exampleTaskItemBlueprintandWieldableItem.Flashlight).CaptureByGuidandApplyByGuidonly find objects that a load registered. An object created after the load is not found.Applyregisters the object under its GUID, as a load does, soCaptureByGuidfinds it afterwards. (they resolve throughSaveController.GetObjectByGUID.)- When another player chooses the GUID, always use the overload with a list of allowed GUIDs.
ApplyByGuid(guid, json, allowedGuids)returnsfalseunlessguidis in the list. Anulllist refuses every GUID. The list can be anyICollection<string>, such as aHashSet<string>or an array. The two-argument form is marked obsolete (the compiler shows a warning, not an error) and is only for code you fully trust. - Mod ids for saved data are not case sensitive.
SaveData.Register("MyMod", ...)andRegister("mymod", ...)are the same mod, and the second call throwsInvalidOperationException. Item and recipe ids fromRegistryIdsare case sensitive, so the two rules differ. See Registry API. (Registerkeys its savers, loaders and the save file name without regard to case.)
Limits
Capturegivesnullwhen the object is not savable, a save or load is running, the object wrote no entry, or the capture failed (the failure is logged). The object must be one the game saves and must have a GUID. Anything else givesnullfromCaptureandfalsefromApply. (the target must be aSavableScriptwith a non-empty publicGUIDfield, the same test the game’sSaveController.SerializeDatauses.)- Nothing happens while a save is loading or saving.
CapturegivesnullandApplygivesfalse. (both checkSaveData.Busy.) Applygivesfalsefor text it cannot parse, a GUID that does not match, a type that does not match, text over the size or depth limit, a type name it refuses, a refused state, or a failure (logged). It never throws.Applyrefuses aTypeandDatapair anywhere in the entry (the game would turn it into a type). It also refuses a type name belowDatathat is not text, is over 1,024 characters, names no type the game can find, names a Unity type from another assembly, or is made of more than 64 parts. A refusal is written to the log once assave data apply refused. (“a Unity type from another assembly” includes one used as an array element, as a generic argument of the type or of a base type, or of anIEnumerable<T>orIDictionary<K,V>it implements.)Applylimits the size of the text. It refuses text over 4,194,304 characters, nested more than 256 levels deep, that closes more brackets than it opens, or that has a/outside a string. Nothing is parsed or changed, and the refusal is logged once per session assave data apply refused. (depth counts[and{outside strings. The game’s parser skips//and/* */comments and loops forever on a/followed by neither/nor*, so every/outside a string is refused.)Applyis not a true undo if it fails. After the checks pass,Applycopies the object’s current state first. If the load fails, it puts that copy back and returnsfalse. That brings back the fields the save holds, but not what the first run did outside them (item lookups, events and screen changes are not undone, and the inventory’s setup runs a second time). Treat afalsefromApplyas possibly half applied. If no copy can be made,Applyrefuses. (the copy is aCapture, so the object’s save method runs on the live object on everyApplythat passes the checks, before the load runs. ForInventorythe limit is wider:SlotsandCharacterSlotsare[fsIgnore], so the copy does not hold them, and the load copies a saved slot only when its amount is above 0 and its item id is above -1, never clearing a slot. A failedApplycan therefore leave items in slots the copy left empty.)ApplyByGuid(guid, json)can reach any object the game registered on load, including the inventory. A player who sends the GUIDInventorycould replace the host’s inventory.