diff --git a/crates/shared/src/world/chunk_data.rs b/crates/shared/src/world/chunk_data.rs new file mode 100644 index 0000000..86a6b9e --- /dev/null +++ b/crates/shared/src/world/chunk_data.rs @@ -0,0 +1,154 @@ +// 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()); + } +} diff --git a/crates/shared/src/world/mod.rs b/crates/shared/src/world/mod.rs index 90db047..2d85aa0 100644 --- a/crates/shared/src/world/mod.rs +++ b/crates/shared/src/world/mod.rs @@ -6,11 +6,13 @@ mod block; mod chunk; +mod chunk_data; mod coords; mod entity; pub use block::BlockId; pub use chunk::{Chunk, PalettedChunk}; +pub use chunk_data::ChunkData; pub use coords::ChunkPos; pub use entity::EntityPos;