synvael/crates/shared/src/world/chunk_data.rs

157 lines
5 KiB
Rust

// 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<u32, BlockId>,
}
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)]
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, &current);
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, &current);
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, &current);
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());
}
}