Skip to content
Edit on GitHub

Persistence

verifiedAgainst f1eaefe · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.

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.

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
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(...)

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 (../).

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 matches TerrainGenerator.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’s worldSeed and generatorVersion match the loaded session. Base chunks are procedurally generated by Burst jobs, and WorldEditOverlay.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):

  1. Temporary write: Data is written to <filename>.tmp. If writing fails, the temp file is deleted and the primary file remains untouched.
  2. 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.
  3. Backup fallback on load: When loading, TryLoad first validates the primary file against a schema/rule delegate. If corrupt or unparseable, it transparently loads the .bak file, surfaces a warning ("loaded the last-known-good backup"), and avoids crashing the game session.

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) and TrySaveVessels<TVesselData>() (:343) are generic methods constrained to class.
  • The caller (GameSessionRuntime in the Player assembly) owns the concrete DTO (PlayerSaveData), captures it via PlayerStateSnapshot.Capture(), and passes it to SaveGameService.
  • Content and schema validation (e.g. verifying formatVersion, hotbar index bounds, and valid inventory items) is performed by PlayerStateSnapshot.TryApply() upon load.

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.

  1. Never serialize full chunk voxel arrays. Storing 16³ voxels per chunk balloons save sizes and breaks world generation upgrades. Save only WorldEditOverlay deltas.
  2. Always write atomically via .tmp and .bak. Never call raw File.WriteAllText or File.WriteAllBytes directly on save files. If the process is killed mid-write, a direct write will corrupt the slot permanently.
  3. World seed and generator version must match. SaveGameService.TrySaveWorldEdits and TryLoadWorldEdits reject save operations if the DTO seed/generator version diverges from WorldMetadata. This prevents cross-slot pollution and corrupted procedural boundaries.
  4. 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.
  5. 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 in ItemCatalog before touching live scene state.
  6. Preserve backward compatibility flags. When adding new fields to WorldMetadata (such as hasEnvironmentState or hasGlobalSoilMoisture), guard them with boolean flags so existing worlds continue to load cleanly without data loss.
  • Upstream: VoxelSandbox.World.Chunks (ChunkData) and VoxelSandbox.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.UI listens to GameSessionRuntime.StatusChanged to 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 .bak recovery without manual hex editing.
  • Tests: Assets/_Game/Tests/EditMode/World/SaveGameServiceTests.cs and EnvironmentPersistenceTests.cs cover slot creation, corruption simulation, binary round-trips, and backward compatibility.
  • Terrain generation — explains the pure procedural generation that makes sparse delta persistence possible.
  • Assemblies & boundaries — details why SaveGameService uses 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.

  1. Loading notes…