No edit summary
No edit summary
 
Line 1: Line 1:
= Save =
<div class="infobox-wrap">
 
{| class="infobox"
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]].
|+ | '''Save file'''
 
{| class="infobox" style="width: 280px; font-size: 90%; float: right; margin-left: 1em; border: 1px solid #a2a9b1; background-color: #f8f9fa; padding: 5px;"
|+ style="font-size: 125%; font-weight: bold;" | Save file
|-
|-
! scope="row" | Extension
! scope="row" | Extension
| <code>.pc</code>
| <code>.pc</code>
|-
|-
! scope="row" | Default folder
! scope="row" | Folder
| <code>&lt;persistentDataPath&gt;/saves/</code>
| <code>&lt;persistentDataPath&gt;/saves/</code>
|-
|-
! scope="row" | Line encoding
! scope="row" | Header
| First newline separates header and body (before XOR)
| Unity <code>JsonUtility</code> (<code>GameData</code>), one line
|-
! scope="row" | Header serializer
| Unity <code>JsonUtility</code> (<code>GameData</code>)
|-
|-
! scope="row" | Body serializer
! scope="row" | Body
| Newtonsoft <code>JsonConvert</code> (<code>ContentData</code> aggregate)
| Newtonsoft JSON (<code>ContentData</code> aggregate)
|-
|-
! scope="row" | Obfuscation
! scope="row" | File obfuscation
| Byte-wise XOR with key <code>129</code>
| XOR every character with <code>129</code>
|}
|}


<div style="clear:both;"></div>
In '''[[PC Simulator]]''', a '''save''' (a '''save file''') is a persistent snapshot of a play session. Saves use the <code>.pc</code> extension, are stored under the application persistent data directory, and combine a small Unity JSON header with a larger Newtonsoft JSON body. The stored file is XOR-masked with a fixed key. [[Save Editor]] covers external editing; [[Main Menu]] covers listing and importing files.
 
Saves are written through <code>SaveManager</code> after <code>SaveManager.Loader</code> has been set (usually from the menu or an example load). Loading runs inside the gameplay scene and rebuilds money, time, difficulty, environment, the player transform, registered scene objects, and world items that implement <code>ISave</code>.
</div>
<div class="infobox-clear"></div>


== Purpose ==
== 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.
A save records enough state to restore a room after exit: global session fields (money, timers, difficulty flags), the player pose, data from each configured <code>SceneObject</code>, and each world <code>Item</code> that participates in <code>ISave</code>. <code>SaveManager.LoadData()</code> consumes the static <code>SaveManager.Loader</code> instance prepared before the scene loads.


== Storage location and naming ==
== 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.GetFolderPath()</code> returns <code>Application.persistentDataPath + "/saves/"</code> and creates the directory if it is missing. The extension 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.
<code>SaveUtility.GetNewPath(string name)</code> places a file in that folder. If <code>name + ".pc"</code> exists, the routine appends <code> (n)</code> with increasing <code>n</code>. Names that already end with <code> (digits)</code> continue numbering from that index. Import uses this path to avoid overwriting existing files.


== On-disk format ==
== On-disk format ==
Line 41: Line 39:
=== Obfuscation ===
=== 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.
<code>SaveUtility.EncryptDecrypt</code> XORs each UTF-16 code unit with the integer <code>129</code>. The same function is used on read and write. This is reversible and is not strong encryption; it is adequate only as a light obfuscator.


=== Logical layout ===
=== Logical layout ===


After decryption, the payload is split on the '''first''' newline into two parts:
After decryption, the text is split on the '''first''' newline:


# '''Header:''' a single line of JSON representing <code>GameData</code>, parsed with <code>JsonUtility.FromJson&lt;GameData&gt;</code>.
# '''Header:''' one line of JSON for <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>.
# '''Body:''' remaining text, deserialized with <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>.
<code>DataLoader.WriteToFile</code> writes <code>JsonUtility.ToJson(GameData)</code>, one newline, then the body string, then applies XOR and writes with <code>File.WriteAllText</code>. A null header parse throws <code>Invalid file!</code>
 
If parsing the header yields null, <code>LoadFromString</code> throws <code>Invalid file!</code>


== Header schema (<code>GameData</code>) ==
== Header schema (<code>GameData</code>) ==
The C# type <code>GameData</code> defines the header. Fields and typical meaning:


{| class="wikitable"
{| class="wikitable"
! Field !! Type !! Role
! 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>version</code> || <code>string</code> || Set from <code>Application.version</code> on save.
|-
|-
| <code>roomName</code> || <code>string</code> || Display name shown in the file list on [[Main Menu]].
| <code>roomName</code> || <code>string</code> || Label in the [[Main Menu]] file list.
|-
|-
| <code>coin</code> || <code>int</code> || Player cash (<code>Main.Money</code>) at save time.
| <code>coin</code> || <code>int</code> || Cash (<code>Main.Money</code>).
|-
|-
| <code>room</code> || <code>int</code> || Logical room index added to the menu’s <code>startRoomSceneIndex</code> when loading the scene.
| <code>room</code> || <code>int</code> || Added to menu <code>startRoomSceneIndex</code> when opening the scene.
|-
|-
| <code>gravity</code> || <code>bool</code> || When false at load, <code>Physics.gravity</code> is set to zero.
| <code>gravity</code> || <code>bool</code> || If 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>hardcore</code> || <code>bool</code> || Selects hardcore load behaviour.
|-
|-
| <code>playtime</code> || <code>float</code> || Elapsed play time (<code>Main.playTime</code>), used for timed achievements such as Lightspeed.
| <code>playtime</code> || <code>float</code> || <code>Main.playTime</code> (timed achievements).
|-
|-
| <code>temperature</code> || <code>float</code> || Air conditioner target or static temperature depending on mode.
| <code>temperature</code> || <code>float</code> || AC-related value depending on mode.
|-
|-
| <code>ac</code> || <code>bool</code> || Air conditioner power state when not hardcore.
| <code>ac</code> || <code>bool</code> || AC power 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>light</code> || <code>bool</code> || Lamp switch; type default is true.
|-
|-
| <code>sign</code> || <code>string</code> || When non-empty, forces read-only example mode (see below).
| <code>sign</code> || <code>string</code> || Non-empty forces read-only example mode.
|}
|}


Boolean defaults in JSON follow Unity’s <code>JsonUtility</code> rules when absent.
== Body schema ==
 
== Body schema (aggregate JSON) ==


<code>SaveManager.SaveData</code> builds an anonymous object serialized by Newtonsoft with three top-level properties:
<code>SaveManager.SaveData</code> serializes an object with:


* <code>playerData</code>: <code>PlayerData</code> from <code>Player.SavePlayer()</code>.
* <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>scene</code>: a <code>JObject</code> filled by each <code>SceneObject.ToData</code>.
* <code>itemData</code>: an array of per-item records.
* <code>itemData</code>: array of records with <code>spawnId</code>, <code>id</code>, position, rotation, and <code>data</code> for <code>ISave</code> components.
 
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>spawnId</code> loads <code>Resources.Load("Components/" + spawnId)</code>. Items with <code>position.y &lt; -20</code> are omitted on save. Load failures (missing prefab or <code>FromData</code> exceptions) increment an error count.


* <code>playTime</code> and money are taken from the header.
=== <code>PlayerData</code> ===
* 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 ===
Position <code>x</code>, <code>y</code>, <code>z</code>; body yaw <code>ry</code>; camera pitch <code>rx</code> (normalized from local euler angles). <code>LoadPlayer</code> applies position only if <code>y</code> is between <code>-100</code> and <code>100</code>.


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.
== Save pipeline ==


=== Deserialize and apply ===
On save, <code>SaveManager</code> copies version, money, play time, lamp state; if not hardcore, copies AC target and power from <code>AirConditioner.instance</code>; builds JSON content; assigns <code>Loader.Content</code>; calls <code>WriteToFile()</code>.


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).
== Load pipeline ==


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.
'''Read-only''' when <code>Loader.Path</code> is empty or <code>GameData.sign</code> is non-empty: sets <code>Main.example</code>, hides the configured save button.


== Integration points ==
Header restores money, play time, lamp, gravity, hardcore versus normal AC wiring. Empty <code>Loader.Content</code> activates a designer <code>preset</code> object and skips item deserialize.


* '''[[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.
<code>OnDestroy</code> resets default gravity and <code>AirConditioner.temperature</code> to normal constants.
* '''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 ==
== Integration ==


* 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.
* [[Main Menu]]: file list, load, import.
* Partial load: missing <code>Resources</code> prefabs increment the failure count and log a warning with <code>spawnId</code>.
* Examples: <code>StreamingAssets/Examples/*.pc</code> may load without a user path (read-only).
* Severe header failure throws before gameplay stabilizes; the menu path catches some failures when populating slots.
* [[Bitcoin]]: stored in <code>PlayerPrefs</code>, not in the inspected <code>GameData</code> fields.


== Security and integrity ==
== Compatibility ==


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.
Import UI text references saves from version '''1.7.0''' and above. Invalid or truncated files throw or fail individual slots.


== See also ==
== See also ==
Line 188: Line 122:
* [[Decompile]]
* [[Decompile]]
* [[Secrets]]
* [[Secrets]]
* [[Achievements:The Hidden Room]]
* [[Tutorial]]


== References ==
== References ==


* <code>SaveManagement/SaveUtility.cs</code> (path, extension, XOR, <code>GetNewPath</code>)
* <code>SaveUtility.cs</code>, <code>DataLoader.cs</code>, <code>GameData.cs</code>, <code>ContentData.cs</code>, <code>ItemData.cs</code>, <code>PlayerData.cs</code>, <code>SaveManager.cs</code>, <code>ISave.cs</code>, <code>SceneObject.cs</code>, <code>Player.cs</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>)

Latest revision as of 18:29, 22 March 2026

Save file
Extension .pc
Folder <persistentDataPath>/saves/
Header Unity JsonUtility (GameData), one line
Body Newtonsoft JSON (ContentData aggregate)
File obfuscation XOR every character with 129

In PC Simulator, a save (a save file) is a persistent snapshot of a play session. Saves use the .pc extension, are stored under the application persistent data directory, and combine a small Unity JSON header with a larger Newtonsoft JSON body. The stored file is XOR-masked with a fixed key. Save Editor covers external editing; Main Menu covers listing and importing files.

Saves are written through SaveManager after SaveManager.Loader has been set (usually from the menu or an example load). Loading runs inside the gameplay scene and rebuilds money, time, difficulty, environment, the player transform, registered scene objects, and world items that implement ISave.

Purpose

A save records enough state to restore a room after exit: global session fields (money, timers, difficulty flags), the player pose, data from each configured SceneObject, and each world Item that participates in ISave. SaveManager.LoadData() consumes the static SaveManager.Loader instance prepared before the scene loads.

Storage location and naming

SaveUtility.GetFolderPath() returns Application.persistentDataPath + "/saves/" and creates the directory if it is missing. The extension is SaveUtility.extension, defined as ".pc".

SaveUtility.GetNewPath(string name) places a file in that folder. If name + ".pc" exists, the routine appends (n) with increasing n. Names that already end with (digits) continue numbering from that index. Import uses this path to avoid overwriting existing files.

On-disk format

Obfuscation

SaveUtility.EncryptDecrypt XORs each UTF-16 code unit with the integer 129. The same function is used on read and write. This is reversible and is not strong encryption; it is adequate only as a light obfuscator.

Logical layout

After decryption, the text is split on the first newline:

  1. Header: one line of JSON for GameData, parsed with JsonUtility.FromJson<GameData>.
  2. Body: remaining text, deserialized with JsonConvert.DeserializeObject<ContentData>.

DataLoader.WriteToFile writes JsonUtility.ToJson(GameData), one newline, then the body string, then applies XOR and writes with File.WriteAllText. A null header parse throws Invalid file!

Header schema (GameData)

Field Type Role
version string Set from Application.version on save.
roomName string Label in the Main Menu file list.
coin int Cash (Main.Money).
room int Added to menu startRoomSceneIndex when opening the scene.
gravity bool If false at load, Physics.gravity is set to zero.
hardcore bool Selects hardcore load behaviour.
playtime float Main.playTime (timed achievements).
temperature float AC-related value depending on mode.
ac bool AC power when not hardcore.
light bool Lamp switch; type default is true.
sign string Non-empty forces read-only example mode.

Body schema

SaveManager.SaveData serializes an object with:

  • playerData: PlayerData from Player.SavePlayer().
  • scene: a JObject filled by each SceneObject.ToData.
  • itemData: array of records with spawnId, id, position, rotation, and data for ISave components.

spawnId loads Resources.Load("Components/" + spawnId). Items with position.y < -20 are omitted on save. Load failures (missing prefab or FromData exceptions) increment an error count.

PlayerData

Position x, y, z; body yaw ry; camera pitch rx (normalized from local euler angles). LoadPlayer applies position only if y is between -100 and 100.

Save pipeline

On save, SaveManager copies version, money, play time, lamp state; if not hardcore, copies AC target and power from AirConditioner.instance; builds JSON content; assigns Loader.Content; calls WriteToFile().

Load pipeline

Read-only when Loader.Path is empty or GameData.sign is non-empty: sets Main.example, hides the configured save button.

Header restores money, play time, lamp, gravity, hardcore versus normal AC wiring. Empty Loader.Content activates a designer preset object and skips item deserialize.

OnDestroy resets default gravity and AirConditioner.temperature to normal constants.

Integration

  • Main Menu: file list, load, import.
  • Examples: StreamingAssets/Examples/*.pc may load without a user path (read-only).
  • Bitcoin: stored in PlayerPrefs, not in the inspected GameData fields.

Compatibility

Import UI text references saves from version 1.7.0 and above. Invalid or truncated files throw or fail individual slots.

See also

References

  • SaveUtility.cs, DataLoader.cs, GameData.cs, ContentData.cs, ItemData.cs, PlayerData.cs, SaveManager.cs, ISave.cs, SceneObject.cs, Player.cs