1use std::fmt;
34
35use glam::IVec3;
36use roxlap_cavegen::{CaveParams, Generator};
37use roxlap_formats::vxl::Vxl;
38
39use crate::chunks::empty_chunk_vxl;
40use crate::{ChunkGenerator, CHUNK_SIZE_XY};
41
42pub struct CaveChunkGenerator<G> {
53 inner: G,
54 base_params: CaveParams,
55}
56
57impl<G> fmt::Debug for CaveChunkGenerator<G>
58where
59 G: fmt::Debug,
60{
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.debug_struct("CaveChunkGenerator")
63 .field("inner", &self.inner)
64 .field("base_seed", &self.base_params.seed)
65 .field("seed_count", &self.base_params.seed_count)
66 .finish()
67 }
68}
69
70impl<G> CaveChunkGenerator<G> {
71 pub fn new(inner: G, base_params: CaveParams) -> Self {
79 Self { inner, base_params }
80 }
81}
82
83#[must_use]
91pub(crate) fn derive_chunk_seed(base_seed: u64, chunk_idx: IVec3) -> u64 {
92 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
93 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
94 let mut h = FNV_OFFSET ^ base_seed;
95 for byte in chunk_idx.x.to_le_bytes() {
96 h ^= u64::from(byte);
97 h = h.wrapping_mul(FNV_PRIME);
98 }
99 for byte in chunk_idx.y.to_le_bytes() {
100 h ^= u64::from(byte);
101 h = h.wrapping_mul(FNV_PRIME);
102 }
103 h
104}
105
106impl<G> ChunkGenerator for CaveChunkGenerator<G>
107where
108 G: Generator<Params = CaveParams> + fmt::Debug + Send + Sync + 'static,
109{
110 fn generate(&self, chunk_idx: IVec3) -> Vxl {
111 if chunk_idx.z != 0 {
112 return empty_chunk_vxl();
116 }
117 let params = CaveParams {
118 seed: derive_chunk_seed(self.base_params.seed, chunk_idx),
119 ..self.base_params
120 };
121 self.inner.generate(¶ms, CHUNK_SIZE_XY)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::chunks::tests::voxel_is_solid;
129 use crate::{
130 Grid, GridTransform, Scene, StreamRadius, CHUNK_SIZE_XY as CXY, CHUNK_SIZE_Z as CZ,
131 };
132 use glam::DVec3;
133 use roxlap_cavegen::{BlueCaveGenerator, MagCaveGenerator};
134 use std::sync::Arc;
135
136 #[test]
137 fn derive_chunk_seed_distinct_for_neighbours() {
138 let s0 = derive_chunk_seed(7, IVec3::new(0, 0, 0));
139 let sx = derive_chunk_seed(7, IVec3::new(1, 0, 0));
140 let sy = derive_chunk_seed(7, IVec3::new(0, 1, 0));
141 let snx = derive_chunk_seed(7, IVec3::new(-1, 0, 0));
142 assert_ne!(s0, sx);
145 assert_ne!(s0, sy);
146 assert_ne!(s0, snx);
147 assert_ne!(sx, sy);
148 }
149
150 #[test]
151 fn derive_chunk_seed_ignores_z() {
152 let a = derive_chunk_seed(7, IVec3::new(3, -2, 0));
156 let b = derive_chunk_seed(7, IVec3::new(3, -2, 5));
157 assert_eq!(a, b);
158 }
159
160 #[test]
161 fn adapter_returns_chunk_sized_vxl() {
162 let gen = CaveChunkGenerator::new(
163 BlueCaveGenerator,
164 CaveParams {
165 seed_count: 16,
166 ..BlueCaveGenerator::default_params()
167 },
168 );
169 let vxl = gen.generate(IVec3::ZERO);
170 assert_eq!(vxl.vsid, CXY, "adapter must return chunk-VSID output");
171 }
172
173 #[test]
174 fn adapter_is_deterministic_per_chunk_idx() {
175 let mk = || {
176 CaveChunkGenerator::new(
177 BlueCaveGenerator,
178 CaveParams {
179 seed_count: 16,
180 ..BlueCaveGenerator::default_params()
181 },
182 )
183 };
184 let g1 = mk();
185 let g2 = mk();
186 let a = g1.generate(IVec3::new(3, -2, 0));
187 let b = g2.generate(IVec3::new(3, -2, 0));
188 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
191 assert_eq!(a.data.as_ref(), b.data.as_ref());
192 }
193
194 #[test]
195 fn adapter_different_chunks_yield_different_output() {
196 let gen = CaveChunkGenerator::new(
197 BlueCaveGenerator,
198 CaveParams {
199 seed_count: 16,
200 ..BlueCaveGenerator::default_params()
201 },
202 );
203 let a = gen.generate(IVec3::new(0, 0, 0));
204 let b = gen.generate(IVec3::new(1, 0, 0));
205 let mut differing = 0;
207 for col in 0..(CXY * CXY) {
208 if a.column_data(col as usize) != b.column_data(col as usize) {
209 differing += 1;
210 }
211 }
212 assert!(
213 differing > 0,
214 "adjacent chunks should produce differing column data"
215 );
216 }
217
218 #[test]
219 fn adapter_chz_nonzero_returns_implicit_air() {
220 let gen = CaveChunkGenerator::new(BlueCaveGenerator, BlueCaveGenerator::default_params());
224 let vxl = gen.generate(IVec3::new(0, 0, 1));
225 for &(x, y, z) in &[(0u32, 0u32, 0u32), (50, 60, 100), (CXY - 1, CXY - 1, 200)] {
226 assert!(
227 !voxel_is_solid(&vxl, x, y, z),
228 "({x},{y},{z}) should be air for chz=1"
229 );
230 }
231 assert!(voxel_is_solid(&vxl, 0, 0, CZ - 1));
233
234 let vxl_neg = gen.generate(IVec3::new(0, 0, -3));
236 assert!(!voxel_is_solid(&vxl_neg, 50, 60, 100));
237 }
238
239 #[test]
240 fn adapter_chunks_have_mixed_air_and_solid_in_cave_layer() {
241 let gen = CaveChunkGenerator::new(
244 BlueCaveGenerator,
245 CaveParams {
246 seed_count: 32,
247 ..BlueCaveGenerator::default_params()
248 },
249 );
250 let vxl = gen.generate(IVec3::ZERO);
251 let mut any_air = false;
252 let mut any_solid_above_bedrock = false;
253 for y in (0..CXY).step_by(16) {
254 for x in (0..CXY).step_by(16) {
255 for z in (0..(CZ - 1)).step_by(16) {
256 if voxel_is_solid(&vxl, x, y, z) {
257 any_solid_above_bedrock = true;
258 } else {
259 any_air = true;
260 }
261 }
262 }
263 }
264 assert!(any_air, "cave should contain air voxels");
265 assert!(
266 any_solid_above_bedrock,
267 "cave should contain solid voxels above bedrock"
268 );
269 }
270
271 #[test]
272 fn adapter_works_with_mag_preset() {
273 let gen = CaveChunkGenerator::new(
276 MagCaveGenerator,
277 CaveParams {
278 seed_count: 16,
279 ..MagCaveGenerator::default_params()
280 },
281 );
282 let a = gen.generate(IVec3::ZERO);
283 let b = gen.generate(IVec3::ZERO);
284 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
285 assert_eq!(a.data.as_ref(), b.data.as_ref());
286 }
287
288 #[test]
289 fn adapter_integrates_with_pump_streaming_sync() {
290 let mut scene = Scene::new();
295 let id = scene.add_grid(GridTransform::identity());
296 let adapter = CaveChunkGenerator::new(
297 BlueCaveGenerator,
298 CaveParams {
299 seed_count: 16,
300 ..BlueCaveGenerator::default_params()
301 },
302 );
303 let g: &mut Grid = scene.grid_mut(id).unwrap();
304 g.set_generator(Some(Arc::new(adapter)));
305 g.stream_radius = StreamRadius::new(150.0, 300.0);
310 scene.pump_streaming_sync(DVec3::new(64.0, 64.0, 100.0));
311
312 let g = scene.grid(id).unwrap();
313 let vxl = g
314 .chunk(IVec3::ZERO)
315 .expect("chunk (0,0,0) should have streamed");
316 let mut any_air = false;
318 let mut any_solid = false;
319 for &(x, y, z) in &[
320 (40_u32, 40, 50),
321 (80, 80, 100),
322 (20, 90, 150),
323 (100, 30, 200),
324 ] {
325 if voxel_is_solid(vxl, x, y, z) {
326 any_solid = true;
327 } else {
328 any_air = true;
329 }
330 }
331 assert!(any_air, "streamed cave should have air voxels");
332 assert!(any_solid, "streamed cave should have solid voxels");
333 }
334}