use std::fmt;
use glam::IVec3;
use roxlap_cavegen::{CaveParams, Generator};
use roxlap_formats::vxl::Vxl;
use crate::chunks::empty_chunk_vxl;
use crate::{ChunkGenerator, CHUNK_SIZE_XY};
pub struct CaveChunkGenerator<G> {
inner: G,
base_params: CaveParams,
}
impl<G> fmt::Debug for CaveChunkGenerator<G>
where
G: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CaveChunkGenerator")
.field("inner", &self.inner)
.field("base_seed", &self.base_params.seed)
.field("seed_count", &self.base_params.seed_count)
.finish()
}
}
impl<G> CaveChunkGenerator<G> {
pub fn new(inner: G, base_params: CaveParams) -> Self {
Self { inner, base_params }
}
}
#[must_use]
pub(crate) fn derive_chunk_seed(base_seed: u64, chunk_idx: IVec3) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h = FNV_OFFSET ^ base_seed;
for byte in chunk_idx.x.to_le_bytes() {
h ^= u64::from(byte);
h = h.wrapping_mul(FNV_PRIME);
}
for byte in chunk_idx.y.to_le_bytes() {
h ^= u64::from(byte);
h = h.wrapping_mul(FNV_PRIME);
}
h
}
impl<G> ChunkGenerator for CaveChunkGenerator<G>
where
G: Generator<Params = CaveParams> + fmt::Debug + Send + Sync + 'static,
{
fn generate(&self, chunk_idx: IVec3) -> Vxl {
if chunk_idx.z != 0 {
return empty_chunk_vxl();
}
let params = CaveParams {
seed: derive_chunk_seed(self.base_params.seed, chunk_idx),
..self.base_params
};
self.inner.generate(¶ms, CHUNK_SIZE_XY)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunks::tests::voxel_is_solid;
use crate::{
Grid, GridTransform, Scene, StreamRadius, CHUNK_SIZE_XY as CXY, CHUNK_SIZE_Z as CZ,
};
use glam::DVec3;
use roxlap_cavegen::{BlueCaveGenerator, MagCaveGenerator};
use std::sync::Arc;
#[test]
fn derive_chunk_seed_distinct_for_neighbours() {
let s0 = derive_chunk_seed(7, IVec3::new(0, 0, 0));
let sx = derive_chunk_seed(7, IVec3::new(1, 0, 0));
let sy = derive_chunk_seed(7, IVec3::new(0, 1, 0));
let snx = derive_chunk_seed(7, IVec3::new(-1, 0, 0));
assert_ne!(s0, sx);
assert_ne!(s0, sy);
assert_ne!(s0, snx);
assert_ne!(sx, sy);
}
#[test]
fn derive_chunk_seed_ignores_z() {
let a = derive_chunk_seed(7, IVec3::new(3, -2, 0));
let b = derive_chunk_seed(7, IVec3::new(3, -2, 5));
assert_eq!(a, b);
}
#[test]
fn adapter_returns_chunk_sized_vxl() {
let gen = CaveChunkGenerator::new(
BlueCaveGenerator,
CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
},
);
let vxl = gen.generate(IVec3::ZERO);
assert_eq!(vxl.vsid, CXY, "adapter must return chunk-VSID output");
}
#[test]
fn adapter_is_deterministic_per_chunk_idx() {
let mk = || {
CaveChunkGenerator::new(
BlueCaveGenerator,
CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
},
)
};
let g1 = mk();
let g2 = mk();
let a = g1.generate(IVec3::new(3, -2, 0));
let b = g2.generate(IVec3::new(3, -2, 0));
assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
assert_eq!(a.data.as_ref(), b.data.as_ref());
}
#[test]
fn adapter_different_chunks_yield_different_output() {
let gen = CaveChunkGenerator::new(
BlueCaveGenerator,
CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
},
);
let a = gen.generate(IVec3::new(0, 0, 0));
let b = gen.generate(IVec3::new(1, 0, 0));
let mut differing = 0;
for col in 0..(CXY * CXY) {
if a.column_data(col as usize) != b.column_data(col as usize) {
differing += 1;
}
}
assert!(
differing > 0,
"adjacent chunks should produce differing column data"
);
}
#[test]
fn adapter_chz_nonzero_returns_implicit_air() {
let gen = CaveChunkGenerator::new(BlueCaveGenerator, BlueCaveGenerator::default_params());
let vxl = gen.generate(IVec3::new(0, 0, 1));
for &(x, y, z) in &[(0u32, 0u32, 0u32), (50, 60, 100), (CXY - 1, CXY - 1, 200)] {
assert!(
!voxel_is_solid(&vxl, x, y, z),
"({x},{y},{z}) should be air for chz=1"
);
}
assert!(voxel_is_solid(&vxl, 0, 0, CZ - 1));
let vxl_neg = gen.generate(IVec3::new(0, 0, -3));
assert!(!voxel_is_solid(&vxl_neg, 50, 60, 100));
}
#[test]
fn adapter_chunks_have_mixed_air_and_solid_in_cave_layer() {
let gen = CaveChunkGenerator::new(
BlueCaveGenerator,
CaveParams {
seed_count: 32,
..BlueCaveGenerator::default_params()
},
);
let vxl = gen.generate(IVec3::ZERO);
let mut any_air = false;
let mut any_solid_above_bedrock = false;
for y in (0..CXY).step_by(16) {
for x in (0..CXY).step_by(16) {
for z in (0..(CZ - 1)).step_by(16) {
if voxel_is_solid(&vxl, x, y, z) {
any_solid_above_bedrock = true;
} else {
any_air = true;
}
}
}
}
assert!(any_air, "cave should contain air voxels");
assert!(
any_solid_above_bedrock,
"cave should contain solid voxels above bedrock"
);
}
#[test]
fn adapter_works_with_mag_preset() {
let gen = CaveChunkGenerator::new(
MagCaveGenerator,
CaveParams {
seed_count: 16,
..MagCaveGenerator::default_params()
},
);
let a = gen.generate(IVec3::ZERO);
let b = gen.generate(IVec3::ZERO);
assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
assert_eq!(a.data.as_ref(), b.data.as_ref());
}
#[test]
fn adapter_integrates_with_pump_streaming_sync() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let adapter = CaveChunkGenerator::new(
BlueCaveGenerator,
CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
},
);
let g: &mut Grid = scene.grid_mut(id).unwrap();
g.set_generator(Some(Arc::new(adapter)));
g.stream_radius = StreamRadius::new(150.0, 300.0);
scene.pump_streaming_sync(DVec3::new(64.0, 64.0, 100.0));
let g = scene.grid(id).unwrap();
let vxl = g
.chunk(IVec3::ZERO)
.expect("chunk (0,0,0) should have streamed");
let mut any_air = false;
let mut any_solid = false;
for &(x, y, z) in &[
(40_u32, 40, 50),
(80, 80, 100),
(20, 90, 150),
(100, 30, 200),
] {
if voxel_is_solid(vxl, x, y, z) {
any_solid = true;
} else {
any_air = true;
}
}
assert!(any_air, "streamed cave should have air voxels");
assert!(any_solid, "streamed cave should have solid voxels");
}
}