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.

Save file
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:

  1. Header: a single line of JSON representing GameData, parsed with JsonUtility.FromJson<GameData>.
  2. Body: all remaining text, parsed as JSON into a structure compatible with ContentData via JsonConvert.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: PlayerData from Player.SavePlayer().
  • scene: a JObject built by iterating every SceneObject entry in sceneObjects and calling ToData.
  • itemData: an array of per-item records.

Each itemData element contains:

  • spawnId: string path key used with Resources.Load("Components/" + spawnId) to locate the prefab.
  • id: integer instance id registered on Main.
  • pos / rot: world transform (saved as nested numeric objects in the anonymous projection; deserializes into ItemData).
  • data: merged ISave payload from all ISave components on that prefab.

Player snapshot (PlayerData)

PlayerData is a struct with:

  • Position x, y, z.
  • Body yaw ry from transform euler angles.
  • Camera pitch rx derived 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:

  1. Read Loader and its GameData.
  2. Set game.version to the running application version.
  3. Copy Main.Money to coin, playTime to playtime, lamp state to light.
  4. If not hardcore, copy air conditioner target temperature and power from AirConditioner.instance into temperature and ac. Hardcore leaves those header fields from the prior state (not overwritten from the instance in this block).
  5. Build ContentData: player snapshot, scene JObject, item list as described.
  6. Assign loader.Content to the Newtonsoft serialization of the aggregate object.
  7. Call loader.WriteToFile(), which writes header, newline, body, XOR, and path from Loader.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.Path is null or empty (in-memory or streaming load without a user path).
  • True if Loader.GameData.sign is 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

  • playTime and money are taken from the header.
  • Lamp on/off follows light.
  • If gravity is false, physics gravity is zeroed for the session.
  • Hardcore: sets Main.hardcore, assigns static AirConditioner.temperature from the header, disables earn button interactability, disables AC trigger collider and switch. Non-hardcore: pushes temperature and AC power into AirConditioner.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: FileMenu lists saves, sorts by last write time, and calls MainMenu.LoadFile, which sets SaveManager.Loader and opens the correct scene index.
  • Examples: MainMenu.LoadExample reads StreamingAssets/Examples/<name>.pc into a DataLoader without necessarily setting a user Path, which triggers read-only behaviour when path is empty.
  • Import: Copies external .pc files into the save folder via GetNewPath, then rescans (see Main Menu).
  • Bitcoin: Not part of the .pc header in the inspected code; Bitcoin uses PlayerPrefs separately. Editors documenting economy should treat wallet balance as orthogonal unless a mod merges them.

Compatibility and errors

  • The import error string in FileMenu references saves from version 1.7.0 and above; older files may fail LoadFromPath or deserialize.
  • Partial load: missing Resources prefabs increment the failure count and log a warning with spawnId.
  • 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, JsonUtility header)
  • GameData.cs, ContentData.cs, ItemData.cs, PlayerData.cs
  • SaveManager.cs (save/load orchestration)
  • ISave.cs, SceneObject.cs
  • Player.cs (SavePlayer / LoadPlayer)