// 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 { #[expect( clippy::cast_possible_truncation, reason = "i ranges over 0..CHUNK_VOLUME, which fits in u32" )] data.set(i as u32, cur); } } data } } #[cfg(test)] #[path = "../tests/chunk_data.rs"] mod tests;