Save
In PC Simulator, a save (also called a save file) is a persistent snapshot of a play session. Saves use the .pc extension, live under the application persistent data directory, and combine a small Unity JSON header with a larger Newtonsoft JSON body. The on-disk form is obfuscated with a fixed XOR mask. External tools and format notes are covered at Save Editor; menu behaviour that lists and imports files is covered at Main Menu.
| Extension | .pc
|
|---|---|
| Default folder | <persistentDataPath>/saves/
|
| Line encoding | First newline separates header and body (before XOR) |
| Header serializer | Unity JsonUtility (GameData)
|
| Body serializer | Newtonsoft JsonConvert (ContentData aggregate)
|
| Obfuscation | Byte-wise XOR with key 129
|
Purpose
Save files record enough state to restore a room after exit: global session fields (money, timers, difficulty flags), the player transform, serialized scene hooks registered as SceneObject instances, and every world Item that participates in ISave. Loading assigns static SaveManager.Loader before the gameplay scene runs so SaveManager.LoadData() can rebuild the world.
Storage location and naming
The helper SaveUtility.GetFolderPath() returns Application.persistentDataPath + "/saves/" and creates the directory if missing. The extension constant is SaveUtility.extension, defined as ".pc".
SaveUtility.GetNewPath(string name) builds a path inside that folder. If name + ".pc" already exists, it appends (n) with increasing n until a free name appears. It understands names that already end in (digits) and continues numbering from that index. This routine is used when importing external saves so copies do not overwrite existing files.
On-disk format
Obfuscation
The file is not stored as plain text. SaveUtility.EncryptDecrypt maps each character through XOR with the integer 129 (the same function encrypts on write and decrypts on read). This is reversible and trivial to strip for Modding or Decompile research; it should not be treated as cryptographic protection.
Logical layout
After decryption, the payload is split on the first newline into two parts:
- Header: a single line of JSON representing
GameData, parsed withJsonUtility.FromJson<GameData>. - Body: all remaining text, parsed as JSON into a structure compatible with
ContentDataviaJsonConvert.DeserializeObject<ContentData>.
When writing, DataLoader.WriteToFile concatenates JsonUtility.ToJson(GameData), a newline, and the body string, then runs the XOR pass and writes with File.WriteAllText.
If parsing the header yields null, LoadFromString throws Invalid file!
Header schema (GameData)
The C# type GameData defines the header. Fields and typical meaning:
| Field | Type | Role |
|---|---|---|
version |
string |
Set from Application.version on save; identifies the build that wrote the file.
|
roomName |
string |
Display name shown in the file list on Main Menu. |
coin |
int |
Player cash (Main.Money) at save time.
|
room |
int |
Logical room index added to the menu’s startRoomSceneIndex when loading the scene.
|
gravity |
bool |
When false at load, Physics.gravity is set to zero.
|
hardcore |
bool |
Enables hardcore-specific load behaviour (see below). |
playtime |
float |
Elapsed play time (Main.playTime), used for timed achievements such as Lightspeed.
|
temperature |
float |
Air conditioner target or static temperature depending on mode. |
ac |
bool |
Air conditioner power state when not hardcore. |
light |
bool |
Lamp switch state (Switch lamp on SaveManager); defaults true in the type definition.
|
sign |
string |
When non-empty, forces read-only example mode (see below). |
Boolean defaults in JSON follow Unity’s JsonUtility rules when absent.
Body schema (aggregate JSON)
SaveManager.SaveData builds an anonymous object serialized by Newtonsoft with three top-level properties:
playerData:PlayerDatafromPlayer.SavePlayer().scene: aJObjectbuilt by iterating everySceneObjectentry insceneObjectsand callingToData.itemData: an array of per-item records.
Each itemData element contains:
spawnId: string path key used withResources.Load("Components/" + spawnId)to locate the prefab.id: integer instance id registered onMain.pos/rot: world transform (saved as nested numeric objects in the anonymous projection; deserializes intoItemData).data: mergedISavepayload from allISavecomponents on that prefab.
Player snapshot (PlayerData)
PlayerData is a struct with:
- Position
x,y,z. - Body yaw
ryfrom transform euler angles. - Camera pitch
rxderived from the local camera euler angle with normalization so values stay in a usable range.
LoadPlayer applies position only if y is between -100 and 100; otherwise it keeps the default spawn height behaviour. It restores yaw and pitch, re-enables the character controller on the next frame.
Scene objects
SceneObject is an abstract MonoBehaviour implementing ISave with string id and ToData / FromData on JObject. Concrete types populate arbitrary keys under the shared scene object during save; on load, each registered sceneObjects entry receives FromData.
Items and ISave
During save, every Item in the scene is considered. Items with transform.position.y < -20 are skipped (treated as fallen out of world). For each remaining item, a JObject collects data from every ISave on that GameObject.
During load, each record instantiates the resource prefab, places it at the saved transform, assigns Item.Id and registers it on Main, then applies each ISave.FromData inside a try block. Failures increment a failure counter.
Save pipeline (SaveManager.SaveData)
Order of operations:
- Read
Loaderand itsGameData. - Set
game.versionto the running application version. - Copy
Main.Moneytocoin,playTimetoplaytime, lamp state tolight. - If not hardcore, copy air conditioner target temperature and power from
AirConditioner.instanceintotemperatureandac. Hardcore leaves those header fields from the prior state (not overwritten from the instance in this block). - Build
ContentData: player snapshot, sceneJObject, item list as described. - Assign
loader.Contentto the Newtonsoft serialization of the aggregate object. - Call
loader.WriteToFile(), which writes header, newline, body, XOR, and path fromLoader.Path.
The method returns true on success; it does not catch file I/O exceptions in the excerpt (platform-dependent failure modes apply).
Load pipeline (SaveManager.LoadData)
Read-only and example mode
Before world reconstruction, the manager determines readOnly:
- True if
Loader.Pathis null or empty (in-memory or streaming load without a user path). - True if
Loader.GameData.signis non-empty.
When read-only, Main.Instance.example is set true and the serialized saveButton reference is deactivated so the session cannot write back to disk through that UI path.
Global state
playTimeand money are taken from the header.- Lamp on/off follows
light. - If
gravityis false, physics gravity is zeroed for the session. - Hardcore: sets
Main.hardcore, assigns staticAirConditioner.temperaturefrom the header, disables earn button interactability, disables AC trigger collider and switch. Non-hardcore: pushes temperature and AC power intoAirConditioner.instance.
Empty body
If Loader.Content is null or empty, the method activates a preset GameObject (designer-defined default layout) and returns zero failures without deserializing items.
Deserialize and apply
Otherwise ContentData is deserialized. Player data loads first, then each SceneObject FromData, then item instantiation and ISave application. The returned integer is the count of item-related failures (missing prefab or deserialization exceptions).
On OnDestroy, SaveManager resets global physics gravity to default (0, -9.81, 0) and restores AirConditioner.temperature to NormalTemperature to avoid leaking session state between scene lifetimes.
Integration points
- Main Menu:
FileMenulists saves, sorts by last write time, and callsMainMenu.LoadFile, which setsSaveManager.Loaderand opens the correct scene index. - Examples:
MainMenu.LoadExamplereadsStreamingAssets/Examples/<name>.pcinto aDataLoaderwithout necessarily setting a userPath, which triggers read-only behaviour when path is empty. - Import: Copies external
.pcfiles into the save folder viaGetNewPath, then rescans (see Main Menu). - Bitcoin: Not part of the
.pcheader in the inspected code; Bitcoin usesPlayerPrefsseparately. Editors documenting economy should treat wallet balance as orthogonal unless a mod merges them.
Compatibility and errors
- The import error string in
FileMenureferences saves from version 1.7.0 and above; older files may failLoadFromPathor deserialize. - Partial load: missing
Resourcesprefabs increment the failure count and log a warning withspawnId. - Severe header failure throws before gameplay stabilizes; the menu path catches some failures when populating slots.
Security and integrity
The XOR step is a simple obfuscator. Anyone with file access can XOR again with 129 to recover JSON. Tampering the body can desynchronize items; invalid JSON causes exceptions during import or load. There is no embedded cryptographic signature in the reviewed GameData beyond optional use of sign as a read-only flag.
See also
References
SaveManagement/SaveUtility.cs(path, extension, XOR,GetNewPath)SaveManagement/DataLoader.cs(read/write split,JsonUtilityheader)GameData.cs,ContentData.cs,ItemData.cs,PlayerData.csSaveManager.cs(save/load orchestration)ISave.cs,SceneObject.csPlayer.cs(SavePlayer/LoadPlayer)