Summary
Implement a background save manager that orchestrates chunk serialization and region file writes. This ties together the region file format (#372) and chunk serializer (#377) into a working save/load system.
Depends on: #372 (region file format), #377 (chunk serialization)
Current State
No chunk data leaves the process. When the player closes the game, all changes are lost. The save manager needs to:
- Track which chunks have been modified (dirty flag)
- Periodically serialize and write dirty chunks to region files
- Auto-save on world close
- Load chunks from region files when they're needed (before generating fresh terrain)
Implementation Plan
Step 1: Save directory structure
~/.config/zigcraft/saves/
<world-name>/
level.dat — world metadata (seed, generator, created, modified)
regions/
r.0.0.mca
r.-1.0.mca
...
Step 2: Level metadata
const LevelData = struct {
seed: u64,
generator_name: []const u8, // "overworld", "flat"
created_timestamp: i64,
last_played_timestamp: i64,
spawn_x: i32,
spawn_z: i32,
// Future: game mode, time of day, player position
};
- Serialize as JSON (simple, human-readable, stdlib support)
- Write on world create, update on save
Step 3: Dirty tracking
- Add
dirty: bool field to ChunkData in chunk_storage.zig
- Set dirty on:
- Block break (
player.breakTargetBlock())
- Block place (
player.placeBlock())
- Light updates (future)
- Clear dirty after successful save
Step 4: Save thread
const SaveManager = struct {
thread: Thread,
queue: SaveQueue, // chunks pending save
region_cache: RegionCache, // open region files, LRU-evicted
allocator: Allocator,
running: bool,
pub fn start(allocator: Allocator, save_dir: []const u8) !*SaveManager;
pub fn stop(self: *SaveManager) void; // flush remaining, join thread
pub fn saveChunk(self: *SaveManager, chunk: *const Chunk) void; // enqueue
pub fn loadChunk(self: *SaveManager, cx: i32, cz: i32) ?Chunk; // sync load
};
- Background thread polls queue, dequeues dirty chunks, serializes, writes to region file
- Region files opened lazily, cached (max ~16 open at once, LRU close)
saveChunk() is non-blocking: enqueues and returns immediately
loadChunk() is synchronous: blocks until read + deserialize complete
Step 5: Integration with WorldStreamer
WorldStreamer.generateChunk(): try SaveManager.loadChunk() first, fall back to worldgen if not found
WorldStreamer.update(): periodically scan for dirty chunks and enqueue saves
GameSession.deinit(): call SaveManager.stop() which flushes all pending saves
Step 6: Auto-save
- Every 60 seconds: scan all loaded chunks for dirty flag, enqueue saves
- On session close: force-save all dirty chunks
- On game exit:
stop() ensures thread completes before process exits
Files to Create
src/world/persistence/save_manager.zig — main orchestrator
src/world/persistence/level_data.zig — level metadata
Files to Modify
src/world/chunk_storage.zig — dirty flag on ChunkData
src/world/world_streamer.zig — load-from-save integration, periodic save triggers
src/game/session.zig — create SaveManager on init, stop on deinit
Testing
Roadmap: docs/PERFORMANCE_ROADMAP.md — Batch 3, Issue 1B-3
Summary
Implement a background save manager that orchestrates chunk serialization and region file writes. This ties together the region file format (#372) and chunk serializer (#377) into a working save/load system.
Depends on: #372 (region file format), #377 (chunk serialization)
Current State
No chunk data leaves the process. When the player closes the game, all changes are lost. The save manager needs to:
Implementation Plan
Step 1: Save directory structure
Step 2: Level metadata
Step 3: Dirty tracking
dirty: boolfield toChunkDatainchunk_storage.zigplayer.breakTargetBlock())player.placeBlock())Step 4: Save thread
saveChunk()is non-blocking: enqueues and returns immediatelyloadChunk()is synchronous: blocks until read + deserialize completeStep 5: Integration with WorldStreamer
WorldStreamer.generateChunk(): trySaveManager.loadChunk()first, fall back to worldgen if not foundWorldStreamer.update(): periodically scan for dirty chunks and enqueue savesGameSession.deinit(): callSaveManager.stop()which flushes all pending savesStep 6: Auto-save
stop()ensures thread completes before process exitsFiles to Create
src/world/persistence/save_manager.zig— main orchestratorsrc/world/persistence/level_data.zig— level metadataFiles to Modify
src/world/chunk_storage.zig— dirty flag on ChunkDatasrc/world/world_streamer.zig— load-from-save integration, periodic save triggerssrc/game/session.zig— create SaveManager on init, stop on deinitTesting
Roadmap:
docs/PERFORMANCE_ROADMAP.md— Batch 3, Issue 1B-3