No edit summary
No edit summary
Line 1: Line 1:
A '''save''' is a file that contains data about the current state of the game.
= Save =


== Inside a save ==
In '''[[PC Simulator]]''', a '''save''' (also called a '''save file''') is a persistent snapshot of a play session. Saves use the <code>.pc</code> 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]].
In a save there's 2 JSON objects, in this particular order:<syntaxhighlight lang="json">
 
{"version":"1.8.0","roomName":"WHAT","coin":20,"room":3,"gravity":true,"hardcore":false,"playtime":393.96258544921877,"temperature":20.0,"ac":false,"light":true,"sign":""}
{| class="infobox" style="width: 280px; font-size: 90%; float: right; margin-left: 1em; border: 1px solid #a2a9b1; background-color: #f8f9fa; padding: 5px;"
</syntaxhighlight>This is the metadata of the save, the version, the name, the money...<syntaxhighlight lang="json">
|+ style="font-size: 125%; font-weight: bold;" | Save file
{"playerData":{"x":18.179142,"y":53.66691,"z":-19.0156765,"ry":45.9237671,"rx":3.599485},"itemData":[{"spawnId":"FlatMonitor","id":599758571,"pos":{"x":-12.6691809,"y":-5.67894745,"z":-6.1120553},"rot":{"x":0.301219523,"y":0.6397389,"z":0.639739752,"w":-0.3012209},"data":{"damaged":false,"glue":false}},{"spawnId":"Crate_Case_ATX(Black)","id":-1533475361,"pos":{"x":-20.67389,"y":-4.288955,"z":-4.298242},"rot":{"x":-0.694708645,"y":-0.6947081,"z":0.131834447,"w":0.131834432},"data":{"glue":false}},{"spawnId":"Crate_Case_ATX(Black)","id":-152446665,"pos":{"x":9.498936,"y":-3.28893352,"z":-14.94713},"rot":{"x":0.466130942,"y":0.5317162,"z":-0.5317156,"w":0.46613133},"data":{"glue":false}},{"spawnId":"Crate_FlatMonitor","id":2054203474,"pos":{"x":-9.163823,"y":-4.288947,"z":3.95957255},"rot":{"x":-0.420856327,"y":0.4208559,"z":0.568225265,"w":-0.5682257},"data":{"glue":false}},{"spawnId":"Crate_FlatMonitor","id":304328838,"pos":{"x":-25.4728374,"y":-3.538967,"z":-2.117688},"rot":{"x":-0.999964237,"y":1.62052629E-08,"z":0.008463376,"w":4.45753869E-07},"data":{"glue":false}},{"spawnId":"Crate_FlatMonitor","id":-186328891,"pos":{"x":-16.6291122,"y":-3.53895044,"z":4.95441628},"rot":{"x":0.966063857,"y":-7.191433E-08,"z":-0.258303642,"w":-4.358661E-07},"data":{"glue":false}},{"spawnId":"Crate_FlatMonitor","id":1943010128,"pos":{"x":10.1579437,"y":-3.538948,"z":-11.09863},"rot":{"x":-2.79101926E-07,"y":0.861216545,"z":-2.9261443E-08,"w":0.5082382},"data":{"glue":false}},{"spawnId":"Part_4","id":1403376016,"pos":{"x":-21.9800987,"y":-5.738934,"z":9.090099},"rot":{"x":-0.363654733,"y":0.606421649,"z":-0.606433451,"w":-0.363657117},"data":{"glue":false}},{"spawnId":"FlatMonitor","id":454197252,"pos":{"x":23.9229488,"y":50.6969147,"z":18.5413284},"rot":{"x":-0.6095349,"y":0.358423084,"z":-0.358423173,"w":-0.609535},"data":{"damaged":true,"glue":false}},{"spawnId":"CPU Celeron G3920","id":485677439,"pos":{"x":-7.07684851,"y":-5.76832533,"z":20.71779},"rot":{"x":-5.5949954E-07,"y":-0.9114966,"z":-7.67247E-07,"w":-0.411307633},"data":{"damaged":false,"glue":false}}],"scene":{"elv":{"pos":{"x":6.491913,"y":64.27802,"z":18.4224415},"goA":false}}}
|-
</syntaxhighlight>This is where the scene objects are stored (e.g. monitors). Additionally if your save is a Factory one there is a special '''scene''' object, containing the position of the elevator and if it should go up or not.
! scope="row" | Extension
| <code>.pc</code>
|-
! scope="row" | Default folder
| <code>&lt;persistentDataPath&gt;/saves/</code>
|-
! scope="row" | Line encoding
| First newline separates header and body (before XOR)
|-
! scope="row" | Header serializer
| Unity <code>JsonUtility</code> (<code>GameData</code>)
|-
! scope="row" | Body serializer
| Newtonsoft <code>JsonConvert</code> (<code>ContentData</code> aggregate)
|-
! scope="row" | Obfuscation
| Byte-wise XOR with key <code>129</code>
|}
 
<div style="clear:both;"></div>
 
== 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 <code>SceneObject</code> instances, and every world <code>Item</code> that participates in <code>ISave</code>. Loading assigns static <code>SaveManager.Loader</code> before the gameplay scene runs so <code>SaveManager.LoadData()</code> can rebuild the world.
 
== Storage location and naming ==
 
The helper <code>SaveUtility.GetFolderPath()</code> returns <code>Application.persistentDataPath + "/saves/"</code> and creates the directory if missing. The extension constant is <code>SaveUtility.extension</code>, defined as <code>".pc"</code>.
 
<code>SaveUtility.GetNewPath(string name)</code> builds a path inside that folder. If <code>name + ".pc"</code> already exists, it appends <code> (n)</code> with increasing <code>n</code> until a free name appears. It understands names that already end in <code> (digits)</code> 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. <code>SaveUtility.EncryptDecrypt</code> maps each character through XOR with the integer <code>129</code> (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 <code>GameData</code>, parsed with <code>JsonUtility.FromJson&lt;GameData&gt;</code>.
# '''Body:''' all remaining text, parsed as JSON into a structure compatible with <code>ContentData</code> via <code>JsonConvert.DeserializeObject&lt;ContentData&gt;</code>.
 
When writing, <code>DataLoader.WriteToFile</code> concatenates <code>JsonUtility.ToJson(GameData)</code>, a newline, and the body string, then runs the XOR pass and writes with <code>File.WriteAllText</code>.
 
If parsing the header yields null, <code>LoadFromString</code> throws <code>Invalid file!</code>
 
== Header schema (<code>GameData</code>) ==
 
The C# type <code>GameData</code> defines the header. Fields and typical meaning:
 
{| class="wikitable"
! Field !! Type !! Role
|-
| <code>version</code> || <code>string</code> || Set from <code>Application.version</code> on save; identifies the build that wrote the file.
|-
| <code>roomName</code> || <code>string</code> || Display name shown in the file list on [[Main Menu]].
|-
| <code>coin</code> || <code>int</code> || Player cash (<code>Main.Money</code>) at save time.
|-
| <code>room</code> || <code>int</code> || Logical room index added to the menu’s <code>startRoomSceneIndex</code> when loading the scene.
|-
| <code>gravity</code> || <code>bool</code> || When false at load, <code>Physics.gravity</code> is set to zero.
|-
| <code>hardcore</code> || <code>bool</code> || Enables hardcore-specific load behaviour (see below).
|-
| <code>playtime</code> || <code>float</code> || Elapsed play time (<code>Main.playTime</code>), used for timed achievements such as Lightspeed.
|-
| <code>temperature</code> || <code>float</code> || Air conditioner target or static temperature depending on mode.
|-
| <code>ac</code> || <code>bool</code> || Air conditioner power state when not hardcore.
|-
| <code>light</code> || <code>bool</code> || Lamp switch state (<code>Switch lamp</code> on <code>SaveManager</code>); defaults true in the type definition.
|-
| <code>sign</code> || <code>string</code> || When non-empty, forces read-only example mode (see below).
|}
 
Boolean defaults in JSON follow Unity’s <code>JsonUtility</code> rules when absent.
 
== Body schema (aggregate JSON) ==
 
<code>SaveManager.SaveData</code> builds an anonymous object serialized by Newtonsoft with three top-level properties:
 
* <code>playerData</code>: <code>PlayerData</code> from <code>Player.SavePlayer()</code>.
* <code>scene</code>: a <code>JObject</code> built by iterating every <code>SceneObject</code> entry in <code>sceneObjects</code> and calling <code>ToData</code>.
* <code>itemData</code>: an array of per-item records.
 
Each <code>itemData</code> element contains:
 
* <code>spawnId</code>: string path key used with <code>Resources.Load("Components/" + spawnId)</code> to locate the prefab.
* <code>id</code>: integer instance id registered on <code>Main</code>.
* <code>pos</code> / <code>rot</code>: world transform (saved as nested numeric objects in the anonymous projection; deserializes into <code>ItemData</code>).
* <code>data</code>: merged <code>ISave</code> payload from all <code>ISave</code> components on that prefab.
 
=== Player snapshot (<code>PlayerData</code>) ===
 
<code>PlayerData</code> is a struct with:
 
* Position <code>x</code>, <code>y</code>, <code>z</code>.
* Body yaw <code>ry</code> from transform euler angles.
* Camera pitch <code>rx</code> derived from the local camera euler angle with normalization so values stay in a usable range.
 
<code>LoadPlayer</code> applies position only if <code>y</code> is between <code>-100</code> and <code>100</code>; otherwise it keeps the default spawn height behaviour. It restores yaw and pitch, re-enables the character controller on the next frame.
 
=== Scene objects ===
 
<code>SceneObject</code> is an abstract <code>MonoBehaviour</code> implementing <code>ISave</code> with string <code>id</code> and <code>ToData</code> / <code>FromData</code> on <code>JObject</code>. Concrete types populate arbitrary keys under the shared <code>scene</code> object during save; on load, each registered <code>sceneObjects</code> entry receives <code>FromData</code>.
 
=== Items and <code>ISave</code> ===
 
During save, every <code>Item</code> in the scene is considered. Items with <code>transform.position.y &lt; -20</code> are '''skipped''' (treated as fallen out of world). For each remaining item, a <code>JObject</code> collects data from every <code>ISave</code> on that GameObject.
 
During load, each record instantiates the resource prefab, places it at the saved transform, assigns <code>Item.Id</code> and registers it on <code>Main</code>, then applies each <code>ISave.FromData</code> inside a try block. Failures increment a failure counter.
 
== Save pipeline (<code>SaveManager.SaveData</code>) ==
 
Order of operations:
 
# Read <code>Loader</code> and its <code>GameData</code>.
# Set <code>game.version</code> to the running application version.
# Copy <code>Main.Money</code> to <code>coin</code>, <code>playTime</code> to <code>playtime</code>, lamp state to <code>light</code>.
# If not hardcore, copy air conditioner target temperature and power from <code>AirConditioner.instance</code> into <code>temperature</code> and <code>ac</code>. Hardcore leaves those header fields from the prior state (not overwritten from the instance in this block).
# Build <code>ContentData</code>: player snapshot, scene <code>JObject</code>, item list as described.
# Assign <code>loader.Content</code> to the Newtonsoft serialization of the aggregate object.
# Call <code>loader.WriteToFile()</code>, which writes header, newline, body, XOR, and path from <code>Loader.Path</code>.
 
The method returns <code>true</code> on success; it does not catch file I/O exceptions in the excerpt (platform-dependent failure modes apply).
 
== Load pipeline (<code>SaveManager.LoadData</code>) ==
 
=== Read-only and example mode ===
 
Before world reconstruction, the manager determines <code>readOnly</code>:
 
* True if <code>Loader.Path</code> is null or empty (in-memory or streaming load without a user path).
* True if <code>Loader.GameData.sign</code> is non-empty.
 
When read-only, <code>Main.Instance.example</code> is set true and the serialized <code>saveButton</code> reference is deactivated so the session cannot write back to disk through that UI path.
 
=== Global state ===
 
* <code>playTime</code> and money are taken from the header.
* Lamp on/off follows <code>light</code>.
* If <code>gravity</code> is false, physics gravity is zeroed for the session.
* Hardcore: sets <code>Main.hardcore</code>, assigns static <code>AirConditioner.temperature</code> from the header, disables earn button interactability, disables AC trigger collider and switch. Non-hardcore: pushes temperature and AC power into <code>AirConditioner.instance</code>.
 
=== Empty body ===
 
If <code>Loader.Content</code> is null or empty, the method activates a <code>preset</code> GameObject (designer-defined default layout) and returns zero failures without deserializing items.
 
=== Deserialize and apply ===
 
Otherwise <code>ContentData</code> is deserialized. Player data loads first, then each <code>SceneObject</code> <code>FromData</code>, then item instantiation and <code>ISave</code> application. The returned integer is the count of item-related failures (missing prefab or deserialization exceptions).
 
On <code>OnDestroy</code>, <code>SaveManager</code> resets global physics gravity to default <code>(0, -9.81, 0)</code> and restores <code>AirConditioner.temperature</code> to <code>NormalTemperature</code> to avoid leaking session state between scene lifetimes.
 
== Integration points ==
 
* '''[[Main Menu]]:''' <code>FileMenu</code> lists saves, sorts by last write time, and calls <code>MainMenu.LoadFile</code>, which sets <code>SaveManager.Loader</code> and opens the correct scene index.
* '''Examples:''' <code>MainMenu.LoadExample</code> reads <code>StreamingAssets/Examples/&lt;name&gt;.pc</code> into a <code>DataLoader</code> without necessarily setting a user <code>Path</code>, which triggers read-only behaviour when path is empty.
* '''Import:''' Copies external <code>.pc</code> files into the save folder via <code>GetNewPath</code>, then rescans (see [[Main Menu]]).
* '''[[Bitcoin]]:''' Not part of the <code>.pc</code> header in the inspected code; [[Bitcoin]] uses <code>PlayerPrefs</code> separately. Editors documenting economy should treat wallet balance as orthogonal unless a mod merges them.
 
== Compatibility and errors ==
 
* The import error string in <code>FileMenu</code> references saves from version '''1.7.0''' and above; older files may fail <code>LoadFromPath</code> or deserialize.
* Partial load: missing <code>Resources</code> prefabs increment the failure count and log a warning with <code>spawnId</code>.
* 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 <code>GameData</code> beyond optional use of <code>sign</code> as a read-only flag.
 
== See also ==
 
* [[PC Simulator]]
* [[Save Editor]]
* [[Main Menu]]
* [[Modding]]
* [[Decompile]]
* [[Secrets]]
* [[Achievements:The Hidden Room]]
* [[Tutorial]]
 
== References ==
 
* <code>SaveManagement/SaveUtility.cs</code> (path, extension, XOR, <code>GetNewPath</code>)
* <code>SaveManagement/DataLoader.cs</code> (read/write split, <code>JsonUtility</code> header)
* <code>GameData.cs</code>, <code>ContentData.cs</code>, <code>ItemData.cs</code>, <code>PlayerData.cs</code>
* <code>SaveManager.cs</code> (save/load orchestration)
* <code>ISave.cs</code>, <code>SceneObject.cs</code>
* <code>Player.cs</code> (<code>SavePlayer</code> / <code>LoadPlayer</code>)

Revision as of 18:25, 22 March 2026

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)