feat(shared): add sparse baseline-relative ChunkData
This commit is contained in:
parent
b196dc3152
commit
9a3224d102
154
crates/shared/src/world/chunk_data.rs
Normal file
154
crates/shared/src/world/chunk_data.rs
Normal 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, ¤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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,13 @@
|
||||||
|
|
||||||
mod block;
|
mod block;
|
||||||
mod chunk;
|
mod chunk;
|
||||||
|
mod chunk_data;
|
||||||
mod coords;
|
mod coords;
|
||||||
mod entity;
|
mod entity;
|
||||||
|
|
||||||
pub use block::BlockId;
|
pub use block::BlockId;
|
||||||
pub use chunk::{Chunk, PalettedChunk};
|
pub use chunk::{Chunk, PalettedChunk};
|
||||||
|
pub use chunk_data::ChunkData;
|
||||||
pub use coords::ChunkPos;
|
pub use coords::ChunkPos;
|
||||||
pub use entity::EntityPos;
|
pub use entity::EntityPos;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue