feat(shared): add sparse baseline-relative ChunkData

This commit is contained in:
Serkyo 2026-07-08 14:16:02 +02:00
parent b196dc3152
commit 9a3224d102
2 changed files with 156 additions and 0 deletions

View file

@ -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<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 {
// `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, &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());
}
}

View file

@ -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;