Persistence
verifiedAgainst f1eaefe · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.
In one paragraph
Section titled “In one paragraph”Persistence in Voxel Sandbox is slot-based, atomic, and differential. Base terrain is never written to disk; instead, the world generator reproduces it deterministically from a pinned seed and generator version, while WorldEditOverlay records only the player’s sparse modifications (WorldSaveData). When a block is restored to its naturally-generated type, its override is removed entirely, keeping world-edit files tiny. Every file on disk is written through AtomicJsonFile or AtomicBinaryFile: writes go to a .tmp file first and are atomically replaced into place while creating a .bak copy, guaranteeing that a sudden crash never leaves a truncated primary file without a last-known-good recovery option. Furthermore, because SaveGameService lives in VoxelSandbox.World, it enforces strict assembly layering by saving higher-level systems (Player state, Industry vessels) through generic JSON methods without ever referencing their concrete types.
The pieces
Section titled “The pieces”| Symbol | File | Responsibility |
|---|---|---|
SaveGameService |
World/Persistence/SaveGameService.cs:12 |
Local single-player save-slot boundary: owns slot paths, metadata, binary world-edits, and generic bridges for player state, environment, entities, and vessels |
AtomicJsonFile |
World/Persistence/AtomicJsonFile.cs:12 |
Atomic JSON persistence: temp-write (.tmp), atomic File.Replace, automatic .bak fallback on read, and validation callback gates |
AtomicBinaryFile |
World/Persistence/AtomicBinaryFile.cs:12 |
Binary equivalent of AtomicJsonFile: same atomic swap and backup discipline, driven by caller-supplied encode/decode delegates |
AtomicBinaryWorldSaveFile |
World/Persistence/AtomicBinaryWorldSaveFile.cs:9 |
Specializes AtomicBinaryFile using WorldSaveDataBinaryCodec and pre-validates decode via WorldEditOverlay.FromSaveData |
WorldMetadata |
World/Persistence/WorldMetadata.cs:10 |
Human-facing slot identity: slot ID, display name, seeds, generator version, timestamps, and optional environment/moisture clocks |
WorldSaveData |
World/Persistence/WorldSaveData.cs:11 |
DTO representing sparse block overrides: list of ChunkEditRecord coordinates containing BlockOverrideRecord deltas |
WorldEditOverlay |
World/Persistence/WorldEditOverlay.cs:13 |
In-memory authority for modified voxels: tracks coordinate dictionaries, deletes overrides when reverting to base generation, applies deltas onto ChunkData |
AutosaveScheduler |
World/Persistence/AutosaveScheduler.cs:6 |
Pure cadence gate (IsDue, MarkSaved) governing autosave intervals without owning file I/O or MonoBehaviours |
PlayerStateSnapshot |
Player/Persistence/PlayerStateSnapshot.cs:12 |
Captures transform, vitals, hotbar, and inventory into PlayerSaveData; validates format version, quaternion normalization, item IDs, and stack limits on load |
GameSessionRuntime |
Player/Persistence/GameSessionRuntime.cs:28 |
The runtime orchestrator: configures seed on startup, drives LoadOrCreateSlot(), triggers autosaves on cadence and quit, and yields local write authority when multiplayer starts |
The flow
Section titled “The flow”sequenceDiagram participant R as GameSessionRuntime\nsession orchestrator participant S as SaveGameService\nslot & file boundary participant D as WorldEditOverlay / Player\nstate models participant A as AtomicJsonFile / BinaryFile\natomic I/O participant F as Disk Storage\nprimary + .bak Note over R,F: Save Pipeline R->>D: capture snapshots (world edits, player, env) R->>S: TrySaveWorldEdits(slotId, worldData) S->>S: validate seed & generatorVersion vs metadata S->>A: TrySave(path, data, codec) A->>F: write to .tmp file A->>F: File.Replace(.tmp -> .bin, backup .bak) S->>A: update metadata (lastSavedUtcTicks) A->>F: atomic write metadata.json Note over R,F: Load Pipeline R->>S: TryLoadMetadata(slotId) S->>A: TryLoad(metadataPath, validate) A->>F: read primary; fallback to .bak on corruption S->>R: validated WorldMetadata (seed, version) R->>S: TryLoadWorldEdits(slotId) S->>A: TryLoad binary edits S-->>R: WorldSaveData (sparse deltas) R->>D: WorldEditOverlay.FromSaveData(data) R->>S: TryLoadPlayerState(slotId) S-->>R: PlayerSaveData R->>D: PlayerStateSnapshot.TryApply(...)
1. Save slot initialization
Section titled “1. Save slot initialization”Each save slot is stored in its own folder under Application.persistentDataPath/Worlds/<slotId>. SaveGameService.TryCreateSlot() (SaveGameService.cs:43) enforces slug hygiene: slot IDs must be 1–32 lowercase alphanumeric characters, dashes, or underscores (WorldMetadata.IsValidSlotId, WorldMetadata.cs:201). It writes world-metadata.json, which stores both the raw user seed (e.g. text-hashed or long.MinValue) and the 32-bit internal integer seed used by terrain generation. Slot creation fails immediately if the target folder exists or if the ID is invalid, preventing path traversal attacks (../).
2. Differential terrain persistence
Section titled “2. Differential terrain persistence”Unlike classic voxel games that serialize full chunk volumes, Voxamine persists only differences from procedural generation:
- When a block is edited via
BlockEditService,WorldEditOverlay.SetOverride()(WorldEditOverlay.cs:21) checks if the new block matchesTerrainGenerator.GetGeneratedBlock(). If the player places dirt back into a hole they dug, the override is removed rather than saved. WorldEditOverlay.ToSaveData()(WorldEditOverlay.cs:73) sorts chunks and flattened voxel coordinates deterministically before writing.- On load,
ChunkStreamingRuntime.TryRestoreWorldSaveData()verifies that the save data’sworldSeedandgeneratorVersionmatch the loaded session. Base chunks are procedurally generated by Burst jobs, andWorldEditOverlay.ApplyTo()overrides the modified voxels in memory before meshing.
3. Atomic file transactions and automatic recovery
Section titled “3. Atomic file transactions and automatic recovery”Power outages, game crashes, and write interruptions are guarded by a two-tier discipline in AtomicJsonFile (AtomicJsonFile.cs:14) and AtomicBinaryFile (AtomicBinaryFile.cs:14):
- Temporary write: Data is written to
<filename>.tmp. If writing fails, the temp file is deleted and the primary file remains untouched. - Atomic swap: If the primary file already exists,
File.Replace(temporaryPath, filePath, backupPath)replaces the destination file atomically on the filesystem and preserves the previous primary as<filename>.bak. - Backup fallback on load: When loading,
TryLoadfirst validates the primary file against a schema/rule delegate. If corrupt or unparseable, it transparently loads the.bakfile, surfaces a warning ("loaded the last-known-good backup"), and avoids crashing the game session.
4. Cross-assembly generic serialization
Section titled “4. Cross-assembly generic serialization”The assembly graph specifies that VoxelSandbox.World cannot reference VoxelSandbox.Player or VoxelSandbox.Industry. To persist player state, vessels, and entities without cycles:
SaveGameService.TrySavePlayerState<TPlayerData>()(SaveGameService.cs:194) andTrySaveVessels<TVesselData>()(:343) are generic methods constrained toclass.- The caller (
GameSessionRuntimein the Player assembly) owns the concrete DTO (PlayerSaveData), captures it viaPlayerStateSnapshot.Capture(), and passes it toSaveGameService. - Content and schema validation (e.g. verifying
formatVersion, hotbar index bounds, and valid inventory items) is performed byPlayerStateSnapshot.TryApply()upon load.
5. Multiplayer authority handoff
Section titled “5. Multiplayer authority handoff”GameSessionRuntime (GameSessionRuntime.cs:104) continually monitors whether a multiplayer connection has become active (HasJoinedMultiplayerServer). When joining a UDP server, local autosave executes one final save snapshot and then immediately disables localAuthorityActive. This guarantees that server-authoritative updates never silently overwrite local single-player progress.
Invariants you must not break
Section titled “Invariants you must not break”- Never serialize full chunk voxel arrays. Storing 16³ voxels per chunk balloons save sizes and breaks world generation upgrades. Save only
WorldEditOverlaydeltas. - Always write atomically via
.tmpand.bak. Never call rawFile.WriteAllTextorFile.WriteAllBytesdirectly on save files. If the process is killed mid-write, a direct write will corrupt the slot permanently. - World seed and generator version must match.
SaveGameService.TrySaveWorldEditsandTryLoadWorldEditsreject save operations if the DTO seed/generator version diverges fromWorldMetadata. This prevents cross-slot pollution and corrupted procedural boundaries. - Slot IDs must satisfy slug rules. Only
a-z,0-9,-, and_up to 32 characters are permitted. Never allow path separators, spaces, or uppercase letters. - Validate before applying loaded state. Never assume JSON deserialization produced valid data.
PlayerStateSnapshot.TryApply()checks quaternion magnitudes ($|q|^2 \ge 0.0001$), hotbar bounds, and item existence inItemCatalogbefore touching live scene state. - Preserve backward compatibility flags. When adding new fields to
WorldMetadata(such ashasEnvironmentStateorhasGlobalSoilMoisture), guard them with boolean flags so existing worlds continue to load cleanly without data loss.
Where it connects
Section titled “Where it connects”- Upstream:
VoxelSandbox.World.Chunks(ChunkData) andVoxelSandbox.World.Generation(TerrainGenerator). Persistence relies on base generation being deterministic and coordinate-pure. - Downstream:
VoxelSandbox.Player.Persistence(GameSessionRuntime,PlayerStateSnapshot) coordinates all session saves and loads;VoxelSandbox.UIlistens toGameSessionRuntime.StatusChangedto present save/load feedback in the HUD and menus. - Diagnostics: The Persistence Lab module in the Voxel Workshop allows developers to inspect save slots, simulate file corruption, and verify atomic
.bakrecovery without manual hex editing. - Tests:
Assets/_Game/Tests/EditMode/World/SaveGameServiceTests.csandEnvironmentPersistenceTests.cscover slot creation, corruption simulation, binary round-trips, and backward compatibility.
See also
Section titled “See also”- Terrain generation — explains the pure procedural generation that makes sparse delta persistence possible.
- Assemblies & boundaries — details why
SaveGameServiceuses generic DTO methods to bridge the Player and Industry layers. Docs/MASTERPLAN.md§36.4 (vessel persistence) and §39.9 (save format stability).
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.