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#[derive(Debug, Clone)]
128pub struct CrystalParams {
129 pub color: crate::VoxColor,
133 pub count: usize,
135 pub crystal_radius: u32,
137 pub light_radius: f32,
139 pub light_strength: f32,
142 pub guaranteed: Option<IVec3>,
145 pub salt: u64,
147}
148
149#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
158pub fn plant_crystals(grid: &mut crate::Grid, seed: u64, params: &CrystalParams) {
159 grid.bake_lights.clear();
160 let side = CHUNK_SIZE_XY as i32;
161 let zmax = crate::CHUNK_SIZE_Z as i32;
162 let mut state = seed
165 .wrapping_mul(0x9E37_79B9_7F4A_7C15)
166 .wrapping_add(params.salt)
167 | 1;
168 let mut next = move || {
169 state ^= state << 13;
170 state ^= state >> 7;
171 state ^= state << 17;
172 state.wrapping_mul(0x2545_F491_4F6C_DD1D)
173 };
174 const DIRS: [IVec3; 6] = [
176 IVec3::new(1, 0, 0),
177 IVec3::new(-1, 0, 0),
178 IVec3::new(0, 1, 0),
179 IVec3::new(0, -1, 0),
180 IVec3::new(0, 0, 1),
181 IVec3::new(0, 0, -1),
182 ];
183
184 let plant_at = |grid: &mut crate::Grid, air: IVec3, dir: IVec3| {
185 for k in 1..=14 {
187 let p = air + dir * k;
188 if !(0..side).contains(&p.x)
189 || !(0..side).contains(&p.y)
190 || !(8..zmax - 1).contains(&p.z)
191 {
192 return false;
193 }
194 if grid.voxel_solid(p) {
195 grid.set_sphere(p, params.crystal_radius, Some(params.color));
197 let lp = air + dir * (k - 3).max(0);
200 grid.bake_lights.push(crate::BakeLight {
201 pos: glam::Vec3::new(lp.x as f32, lp.y as f32, lp.z as f32),
202 radius: params.light_radius,
203 strength: params.light_strength,
204 });
205 return true;
207 }
208 }
209 false
210 };
211 let mut planted = 0;
212 if let Some(spawn) = params.guaranteed {
213 if plant_at(grid, spawn, IVec3::new(0, 0, 1)) {
214 planted += 1;
215 }
216 }
217
218 let mut attempts = 0;
219 while planted < params.count && attempts < 800 {
220 attempts += 1;
221 #[allow(clippy::cast_possible_wrap)]
222 let cand = IVec3::new(
223 (8 + next() % (side as u64 - 16)) as i32,
224 (8 + next() % (side as u64 - 16)) as i32,
225 (24 + next() % 200) as i32,
226 );
227 if grid.voxel_solid(cand) {
228 continue; }
230 let too_close = grid.bake_lights.iter().any(|l| {
233 let d = glam::Vec3::new(cand.x as f32, cand.y as f32, cand.z as f32) - l.pos;
234 d.length_squared() < 24.0 * 24.0
235 });
236 if too_close {
237 continue;
238 }
239 let dir = DIRS[(next() % 6) as usize];
240 if plant_at(grid, cand, dir) {
241 planted += 1;
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::chunks::tests::voxel_is_solid;
250 use crate::{
251 Grid, GridTransform, Scene, StreamRadius, CHUNK_SIZE_XY as CXY, CHUNK_SIZE_Z as CZ,
252 };
253 use glam::DVec3;
254 use roxlap_cavegen::{BlueCaveGenerator, MagCaveGenerator};
255 use std::sync::Arc;
256
257 #[test]
258 fn derive_chunk_seed_distinct_for_neighbours() {
259 let s0 = derive_chunk_seed(7, IVec3::new(0, 0, 0));
260 let sx = derive_chunk_seed(7, IVec3::new(1, 0, 0));
261 let sy = derive_chunk_seed(7, IVec3::new(0, 1, 0));
262 let snx = derive_chunk_seed(7, IVec3::new(-1, 0, 0));
263 assert_ne!(s0, sx);
266 assert_ne!(s0, sy);
267 assert_ne!(s0, snx);
268 assert_ne!(sx, sy);
269 }
270
271 #[test]
272 fn derive_chunk_seed_ignores_z() {
273 let a = derive_chunk_seed(7, IVec3::new(3, -2, 0));
277 let b = derive_chunk_seed(7, IVec3::new(3, -2, 5));
278 assert_eq!(a, b);
279 }
280
281 #[test]
282 fn adapter_returns_chunk_sized_vxl() {
283 let gen = CaveChunkGenerator::new(
284 BlueCaveGenerator,
285 CaveParams {
286 seed_count: 16,
287 ..BlueCaveGenerator::default_params()
288 },
289 );
290 let vxl = gen.generate(IVec3::ZERO);
291 assert_eq!(vxl.vsid, CXY, "adapter must return chunk-VSID output");
292 }
293
294 #[test]
295 fn adapter_is_deterministic_per_chunk_idx() {
296 let mk = || {
297 CaveChunkGenerator::new(
298 BlueCaveGenerator,
299 CaveParams {
300 seed_count: 16,
301 ..BlueCaveGenerator::default_params()
302 },
303 )
304 };
305 let g1 = mk();
306 let g2 = mk();
307 let a = g1.generate(IVec3::new(3, -2, 0));
308 let b = g2.generate(IVec3::new(3, -2, 0));
309 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
312 assert_eq!(a.data.as_ref(), b.data.as_ref());
313 }
314
315 #[test]
316 fn adapter_different_chunks_yield_different_output() {
317 let gen = CaveChunkGenerator::new(
318 BlueCaveGenerator,
319 CaveParams {
320 seed_count: 16,
321 ..BlueCaveGenerator::default_params()
322 },
323 );
324 let a = gen.generate(IVec3::new(0, 0, 0));
325 let b = gen.generate(IVec3::new(1, 0, 0));
326 let mut differing = 0;
328 for col in 0..(CXY * CXY) {
329 if a.column_data(col as usize) != b.column_data(col as usize) {
330 differing += 1;
331 }
332 }
333 assert!(
334 differing > 0,
335 "adjacent chunks should produce differing column data"
336 );
337 }
338
339 #[test]
340 fn adapter_chz_nonzero_returns_implicit_air() {
341 let gen = CaveChunkGenerator::new(BlueCaveGenerator, BlueCaveGenerator::default_params());
345 let vxl = gen.generate(IVec3::new(0, 0, 1));
346 for &(x, y, z) in &[(0u32, 0u32, 0u32), (50, 60, 100), (CXY - 1, CXY - 1, 200)] {
347 assert!(
348 !voxel_is_solid(&vxl, x, y, z),
349 "({x},{y},{z}) should be air for chz=1"
350 );
351 }
352 assert!(voxel_is_solid(&vxl, 0, 0, CZ - 1));
354
355 let vxl_neg = gen.generate(IVec3::new(0, 0, -3));
357 assert!(!voxel_is_solid(&vxl_neg, 50, 60, 100));
358 }
359
360 #[test]
361 fn adapter_chunks_have_mixed_air_and_solid_in_cave_layer() {
362 let gen = CaveChunkGenerator::new(
365 BlueCaveGenerator,
366 CaveParams {
367 seed_count: 32,
368 ..BlueCaveGenerator::default_params()
369 },
370 );
371 let vxl = gen.generate(IVec3::ZERO);
372 let mut any_air = false;
373 let mut any_solid_above_bedrock = false;
374 for y in (0..CXY).step_by(16) {
375 for x in (0..CXY).step_by(16) {
376 for z in (0..(CZ - 1)).step_by(16) {
377 if voxel_is_solid(&vxl, x, y, z) {
378 any_solid_above_bedrock = true;
379 } else {
380 any_air = true;
381 }
382 }
383 }
384 }
385 assert!(any_air, "cave should contain air voxels");
386 assert!(
387 any_solid_above_bedrock,
388 "cave should contain solid voxels above bedrock"
389 );
390 }
391
392 #[test]
393 fn adapter_works_with_mag_preset() {
394 let gen = CaveChunkGenerator::new(
397 MagCaveGenerator,
398 CaveParams {
399 seed_count: 16,
400 ..MagCaveGenerator::default_params()
401 },
402 );
403 let a = gen.generate(IVec3::ZERO);
404 let b = gen.generate(IVec3::ZERO);
405 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
406 assert_eq!(a.data.as_ref(), b.data.as_ref());
407 }
408
409 #[test]
410 fn adapter_integrates_with_pump_streaming_sync() {
411 let mut scene = Scene::new();
416 let id = scene.add_grid(GridTransform::identity());
417 let adapter = CaveChunkGenerator::new(
418 BlueCaveGenerator,
419 CaveParams {
420 seed_count: 16,
421 ..BlueCaveGenerator::default_params()
422 },
423 );
424 let g: &mut Grid = scene.grid_mut(id).unwrap();
425 g.set_generator(Some(Arc::new(adapter)));
426 g.stream_radius = StreamRadius::new(150.0, 300.0);
431 scene.pump_streaming_sync(DVec3::new(64.0, 64.0, 100.0));
432
433 let g = scene.grid(id).unwrap();
434 let vxl = g
435 .chunk(IVec3::ZERO)
436 .expect("chunk (0,0,0) should have streamed");
437 let mut any_air = false;
439 let mut any_solid = false;
440 for &(x, y, z) in &[
441 (40_u32, 40, 50),
442 (80, 80, 100),
443 (20, 90, 150),
444 (100, 30, 200),
445 ] {
446 if voxel_is_solid(vxl, x, y, z) {
447 any_solid = true;
448 } else {
449 any_air = true;
450 }
451 }
452 assert!(any_air, "streamed cave should have air voxels");
453 assert!(any_solid, "streamed cave should have solid voxels");
454 }
455}