the symptom, in your words
"Save/load without drift"
✓ verified on engine 26.07lane: Writing gameplayposted
▸ SYMPTOM
- Load places buildings slightly wrong, duplicates props, or skips ones that existed at save.
- Old saves crash or silently zero important fields after you add properties.
- You saved full tree positions instead of "which trees were chopped" and the file is huge / drifts from regen.
▸ CAUSE
Drift comes from two spawn paths (editor/bootstrap vs load) or from treating missing JSON as meaningful zeros. Static world should be deterministic; only deltas belong in the save.
▸ FIX
DTO + FileSystem.Data
snippet
var json = Json.Serialize(saveDto);
FileSystem.Data.WriteAllText("save.json", json);
if (FileSystem.Data.FileExists("save.json"))
{
var loaded = Json.Deserialize<SaveDto>(FileSystem.Data.ReadAllText("save.json"));
}Use Sandbox's Json.Serialize / Json.Deserialize<T>.
One spawn path
- Mark runtime-placed structures with a small component, e.g.
PlacedBuildable { BuildId }. - Save loop: read component state per GameObject into DTOs.
- Load loop: respawn through the same factory used for live building — no parallel "load-only" constructor.
Deterministic static world
Fixed RNG seed for trees/rocks/scatter. Save only deltas (chopped-tree indices, not every tree pose).
Schema evolution
Missing JSON fields → C# defaults. Guard restored values:
snippet
health = loaded.Health <= 0 ? 1f : loaded.Health;Keep enums append-only (new values at the end). Autosave on a natural checkpoint (sleep / day boundary).
▸ WHY IT WORKS
One factory means load cannot invent a different collider/facing/scale than play. Deterministic regen shrinks the save to true deltas. Append-only enums + guarded defaults make old files load into new builds without treating "missing" as "zero forever."
✓
Verified on engine 26.07 — seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
related gotchas
Runtime world-building helpers
FlatBox/Prop/Deco/Wire helpers plus Obstacle records as plain data beat physics queries for build validity.
s&box component lifecycle in practice
OnAwake runs synchronously inside Components.Create — set singletons there; derive from [Property] in OnStart after spawn helpers assign.
The singleton pattern that removes reference-wiring
static Instance set in OnAwake, cleared in OnDestroy — everything reads Foo.Instance with null-guards, no inspector wiring.