// SPDX-License-Identifier: AGPL-3.0-only //! Sparse, baseline-relative chunk storage. use super::{BlockId, Chunk, ChunkPos}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; /// A modified chunk stored as a sparse set of edits layered on top of a deterministic worldgen baseline. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ChunkData { /// The position of the chunk, identifying which baseline the edits are relative to. pos: ChunkPos, /// The worldgen version the baseline is pinned to, so the same baseline is reproduced across generator updates. worldgen_version: u32, /// The sparse edit set, mapping a local voxel index (`0..CHUNK_VOLUME`) to the block placed there. edits: BTreeMap, } impl ChunkData { /// Initializes an unmodified chunk relative to the baseline at `pos` and `worldgen_version`. #[must_use] pub fn new(pos: ChunkPos, worldgen_version: u32) -> Self { Self { pos, worldgen_version, edits: BTreeMap::new(), } } /// Returns the position identifying which baseline the edits are relative to. #[must_use] pub fn pos(&self) -> ChunkPos { self.pos } /// Returns the worldgen version the baseline is pinned to. #[must_use] pub fn worldgen_version(&self) -> u32 { self.worldgen_version } /// Records `block` at the voxel `local_index`, overwriting any prior edit there. pub fn set(&mut self, local_index: u32, block: BlockId) { self.edits.insert(local_index, block); } /// Returns `true` when no edits are stored, i.e. the chunk matches its baseline and may be omitted from persistence. #[must_use] pub fn is_unmodified(&self) -> bool { self.edits.is_empty() } /// Reconstructs the dense [`Chunk`] by applying the stored edits on top of `baseline`. #[must_use] pub fn materialize(&self, baseline: &Chunk) -> Chunk { let mut chunk = baseline.clone(); for (&local_index, &block) in &self.edits { chunk.blocks[local_index as usize] = block; } chunk } /// Computes the sparse delta of `current` against `baseline`, storing only the voxels that differ. #[must_use] pub fn from_diff( pos: ChunkPos, worldgen_version: u32, baseline: &Chunk, current: &Chunk, ) -> ChunkData { let mut data = ChunkData::new(pos, worldgen_version); for (i, (&base, &cur)) in baseline .blocks .iter() .zip(current.blocks.iter()) .enumerate() { if base != cur { // `i` ranges over `0..CHUNK_VOLUME`, which fits comfortably in a `u32`. #[expect(clippy::cast_possible_truncation)] data.set(i as u32, cur); } } data } } #[cfg(test)] mod tests { use super::*; /// A baseline chunk with a recognisable, non-uniform fill so edits are distinguishable from it. fn baseline() -> Chunk { let mut chunk = Chunk::default(); chunk.set(0, 0, 0, BlockId(1)); chunk.set(1, 2, 3, BlockId(2)); chunk } #[test] fn materialize_reproduces_diffed_chunk() { let base = baseline(); let mut current = base.clone(); current.set(5, 6, 7, BlockId(9)); current.set(0, 0, 0, BlockId(3)); let data = ChunkData::from_diff(ChunkPos::new(0, 0, 0), 0, &base, ¤t); let restored = data.materialize(&base); assert_eq!(restored.blocks, current.blocks); } #[test] fn identical_chunk_diffs_to_nothing() { let base = baseline(); let data = ChunkData::from_diff(ChunkPos::new(0, 0, 0), 0, &base, &base.clone()); assert!(data.is_unmodified()); assert_eq!(data.edits.len(), 0); } #[test] fn reverted_edit_leaves_no_entry() { let base = baseline(); let mut current = base.clone(); // Change a voxel and then change it straight back to its baseline value. current.set(4, 4, 4, BlockId(7)); current.set(4, 4, 4, base.get(4, 4, 4)); let data = ChunkData::from_diff(ChunkPos::new(0, 0, 0), 0, &base, ¤t); assert!(data.is_unmodified()); } #[test] fn diff_stores_only_changed_voxels() { let base = baseline(); let mut current = base.clone(); current.set(1, 1, 1, BlockId(4)); current.set(2, 2, 2, BlockId(5)); current.set(3, 3, 3, BlockId(6)); let data = ChunkData::from_diff(ChunkPos::new(0, 0, 0), 0, &base, ¤t); assert_eq!(data.edits.len(), 3); } #[test] fn set_does_not_reconcile_against_baseline() { // `set` is deliberately dumb: writing a baseline-equal value still records an entry. let mut data = ChunkData::new(ChunkPos::new(0, 0, 0), 0); data.set(42, BlockId::AIR); assert_eq!(data.edits.len(), 1); assert!(!data.is_unmodified()); } }