-
Notifications
You must be signed in to change notification settings - Fork 0
Save File Library
PCEdit.SaveFileHandler is a dependency-free net10.0 library that parses, edits and
re-serializes Planet Crafter saves. It has no knowledge of PCEdit's UI and can be used on its own.
The format it implements is described in Save File Format.
Each has a small interface so it can be substituted in tests or in a host's DI container.
| Interface | Implementation | Responsibility |
|---|---|---|
IPlanetCrafterSaveFileStore |
PlanetCrafterSaveFileStore |
File I/O — Load(path) / Save(path, save) / SaveCopy(source, target, save), including byte-order-mark handling |
IPlanetCrafterSaveFileSerializer |
PlanetCrafterSaveFileSerializer |
Splits / joins the ten sections and the framing characters |
IJsonRecordSerializer |
JsonRecordSerializer |
One section or one list item ⇄ a model, via System.Text.Json
|
flowchart LR
A["Store<br/>bytes ⇄ string"] --> B["Serializer<br/>string ⇄ 10 sections"] --> C["JsonRecordSerializer<br/>section ⇄ model"]
Load reads with File.ReadAllText, which strips any BOM. Save re-checks the target file's
first bytes and writes UTF-8 with a BOM only if the file already had one — or if the path is new
("Save As" semantics). That is what keeps Steam saves BOM'd and Game Pass saves BOM-less.
Save is atomic (v1.3.0). It writes to a sibling .pcedit-tmp file, flushes it to the physical
disk, then swaps it in with File.Move(overwrite: true), so an interrupted write cannot leave a
truncated save behind. The BOM probe therefore has to read the original path, before the swap —
reading the temp file instead would silently regress the Game Pass path.
Deserialize is lenient about whitespace and line endings; Serialize reproduces the game's framing
exactly. The section order is hard-coded and must stay in step with PlanetCrafterSaveFile.
The section split matches an @ only where the framing line breaks bracket it — lookaround
rather than a consuming match, so two separators sharing a line break around an empty section are
both still found. Splitting on the bare character mis-framed any save with an @ in free text; see
Save File Format. A file yielding more than ten sections is now rejected with a
clear error instead of being read past and silently truncated on the next save.
Wraps System.Text.Json with camelCase naming and WhenWritingNull ignore semantics, wraps a
JsonException in an InvalidDataException naming the offending section index, and hosts
GameDecimalConverter (every decimal is written with a fractional part — 1.0, never 1).
var records = new JsonRecordSerializer();
var serializer = new PlanetCrafterSaveFileSerializer(records);
var store = new PlanetCrafterSaveFileStore(serializer);
var save = store.Load(@"C:\...\Standard-2.json");
// Every model is an immutable record — edit with `with`, never by hand-copying.
save = save with
{
Unlocks = save.Unlocks with { TerraTokens = save.Unlocks.TerraTokens + 500 }
};
store.Save(@"C:\...\Standard-2.json", save); // framing preserved byte for byteEverything in PCEdit.SaveFileHandler/Models/ follows the same rules. Getting these wrong is how
data goes missing.
1. Immutable records. Every model — including the root PlanetCrafterSaveFile — is a
sealed record with { get; init; } properties. Change a field with a with expression. Never
hand-copy every property into a new instance: that is exactly how demandGrps / supplyGrps were
once dropped on every save.
2. Required vs nullable. A field the game always writes is a required property; a field the
game may omit is nullable. Get this wrong and either Save throws or the field is silently lost.
3. Version-added fields are nullable. A key introduced by a newer game build must be nullable
(bool?, not bool) so that WhenWritingNull omits it — an older save that never had the key does
not gain it, and still round-trips byte for byte. SaveFileUnlocks.LogisticsPaused is the worked
example.
4. Explicit [JsonPropertyName] only for irregular keys. camelCase is applied automatically.
Name the property explicitly only where the game's key is abbreviated: pos, rot, liId, pnls,
count, linkedWo on WorldObject; woIds, demandGrps, supplyGrps on Inventory. Verify a new
key against the real fixtures — a wrong name no longer loses data (the catch-all preserves the bytes)
but the property will never populate.
5. Every leaf model has a [JsonExtensionData] dictionary. Unknown keys are preserved, not
dropped.
6. WorldObject is converter-driven, not attribute-driven. WorldObjectConverter records each
record's key order on read and replays it on write. Adding a key there means a case in its Read
switch and in WriteKey, plus entries in HasValue and DefaultOrder — or simply leave the key
to ExtensionData, which the converter still positions correctly.
7. Do not compare models for equality. Record value-equality now includes the ExtensionData
dictionary, which compares by reference, so two models parsed from identical bytes are not ==.
Nothing in the codebase does this; don't start.
Unlocks, Metadata and Statistics are singular init-only root properties — changing one means
rebuilding the root with save with { Unlocks = … }.
Terraformations, Players, WorldObjects, Inventories and the rest are List<T>. The list
instance stays mutable even though the property is init-only, so editing one entry means
replacing that element in the existing list — no root rebuild. In the app layer that is
SaveFileWorkspace.ReplaceTerraformation / ReplacePlayer / ReplaceInventory, matched by
PlanetId / Id / Id.
| Type | Purpose |
|---|---|
PlanetHash.Of(planetId) |
Unity's GetStableHashCode — bridges planetId ⇄ WorldObject.planet
|
GameDecimalConverter |
Forces the game's N.0 decimal formatting |
WorldObjectConverter |
Key-order-preserving reader/writer for section 3 |
An unedited load → save is byte-identical on disk. It is asserted two ways in the test suite — a whole-file string comparison and an on-disk byte comparison including the BOM. Any change to this library must keep both green. See Testing.
PCEdit · unofficial fan tool for The Planet Crafter · GPL-3.0-or-later · back up your saves (disclaimer)
Using PCEdit
- Installation
- Save File Locations
- Quick Start
- Worlds & Planets
- Overview & Terraforming
- Inventories
- Logistics Editor
- Terra Tokens
- Teleport
- Languages & Settings
- FAQ & Troubleshooting
Development
- Architecture
- Save File Format
- Save File Library
- App Core
- Desktop UI
- Localization
- Item Catalog
- Testing
Shipping