1use std::simd::i32x4;
11use std::sync::Arc;
12
13use crate::density::{ColumnCache, DimensionNoises, NoiseSettings};
14use crate::noise::PreliminarySurfaceStore;
15use steel_math::{clamp, map, map_clamped};
16use steel_registry::{REGISTRY, vanilla_blocks};
17use steel_utils::BlockStateId;
18use steel_utils::random::name_hash::NameHash;
19use steel_utils::random::{PositionalRandom, Random, RandomSplitter};
20
21pub struct LazyAquifer<'a, N: DimensionNoises> {
24 chunk_min_x: i32,
25 chunk_min_z: i32,
26 splitter: &'a RandomSplitter,
27 noises: &'a N,
28 preliminary_surface: &'a Arc<PreliminarySurfaceStore>,
29 inner: Option<Aquifer<N>>,
30}
31
32impl<'a, N: DimensionNoises> LazyAquifer<'a, N> {
33 #[must_use]
35 pub const fn new(
36 chunk_min_x: i32,
37 chunk_min_z: i32,
38 splitter: &'a RandomSplitter,
39 noises: &'a N,
40 preliminary_surface: &'a Arc<PreliminarySurfaceStore>,
41 ) -> Self {
42 Self {
43 chunk_min_x,
44 chunk_min_z,
45 splitter,
46 noises,
47 preliminary_surface,
48 inner: None,
49 }
50 }
51
52 pub fn ensure(&mut self, height_cache: &N::ColumnCache) -> &mut Aquifer<N> {
57 if self.inner.is_none() {
58 self.inner = Some(Aquifer::<N>::new(
59 self.chunk_min_x,
60 self.chunk_min_z,
61 <N::Settings as NoiseSettings>::MIN_Y,
62 <N::Settings as NoiseSettings>::HEIGHT,
63 self.splitter,
64 self.noises,
65 self.preliminary_surface,
66 height_cache.clone(),
67 ));
68 }
69 #[expect(clippy::unwrap_used, reason = "just initialized above")]
70 self.inner.as_mut().unwrap()
71 }
72}
73
74const Y_SPACING: i32 = 12;
76
77const X_RANGE: i32 = 10;
79const Y_RANGE: i32 = 9;
80const Z_RANGE: i32 = 10;
81
82const SAMPLE_OFFSET_X: i32 = -5;
84const SAMPLE_OFFSET_Y: i32 = 1;
85const SAMPLE_OFFSET_Z: i32 = -5;
86
87const LAVA_LEVEL: i32 = -54;
88const WAY_BELOW_MIN_Y: i32 = -32512;
90const FLOWING_UPDATE_SIMILARITY: f64 = 1.0 - ((12 * 12 - 10 * 10) as f64) / 25.0;
91
92const SURFACE_SAMPLING_OFFSETS: [[i32; 2]; 13] = [
95 [0, 0],
96 [-2, -1],
97 [-1, -1],
98 [0, -1],
99 [1, -1],
100 [-3, 0],
101 [-2, 0],
102 [-1, 0],
103 [1, 0],
104 [-2, 1],
105 [-1, 1],
106 [0, 1],
107 [1, 1],
108];
109
110#[derive(Clone, Copy, PartialEq, Eq)]
115struct FluidStatus {
116 fluid_level: i32,
118 fluid_type: BlockStateId,
120}
121
122impl FluidStatus {
123 const fn at(self, block_y: i32) -> Option<BlockStateId> {
126 if block_y < self.fluid_level {
127 Some(self.fluid_type)
128 } else {
129 None
130 }
131 }
132}
133
134pub enum AquiferResult {
136 Solid,
138 Air,
140 Fluid(BlockStateId),
142}
143
144struct AquiferColumnCache {
155 world_x: i32,
156 world_z: i32,
157 y_anchor: i32,
158 cell_loc_y: [i32; 16],
162 cell_xz_dist_sq: [i32; 16],
164 cell_idx: [u32; 12],
166 uniform_status: Option<FluidStatus>,
168}
169
170impl Default for AquiferColumnCache {
171 fn default() -> Self {
172 Self {
173 world_x: 0,
174 world_z: 0,
175 y_anchor: i32::MIN,
176 cell_loc_y: [0; 16],
177 cell_xz_dist_sq: [0; 16],
178 cell_idx: [0; 12],
179 uniform_status: None,
180 }
181 }
182}
183
184pub struct Aquifer<N: DimensionNoises> {
188 location_cache: Vec<i64>,
191 status_cache: Vec<Option<FluidStatus>>,
193 splitter: RandomSplitter,
195 cache: N::ColumnCache,
197 min_grid_x: i32,
199 min_grid_y: i32,
200 min_grid_z: i32,
201 grid_size_x: i32,
202 grid_size_z: i32,
203 skip_sampling_above_y: i32,
205 sea_level: i32,
207 lava_floor: i32,
211 water_id: BlockStateId,
213 lava_id: BlockStateId,
214 default_fluid_id: BlockStateId,
216 should_schedule_fluid_update: bool,
218 col_cache: AquiferColumnCache,
222 preliminary_surface: Arc<PreliminarySurfaceStore>,
226}
227
228#[inline]
230const fn grid_x(block: i32) -> i32 {
231 block >> 4
232}
233#[inline]
234const fn grid_z(block: i32) -> i32 {
235 block >> 4
236}
237#[inline]
238const fn grid_y(block: i32) -> i32 {
239 block.div_euclid(Y_SPACING)
240}
241#[inline]
242const fn from_grid_x(grid: i32, offset: i32) -> i32 {
243 (grid << 4) + offset
244}
245#[inline]
246const fn from_grid_y(grid: i32, offset: i32) -> i32 {
247 grid * Y_SPACING + offset
248}
249#[inline]
250const fn from_grid_z(grid: i32, offset: i32) -> i32 {
251 (grid << 4) + offset
252}
253
254const PACKED_X_MASK: i64 = 0x3FF_FFFF; const PACKED_Y_MASK: i64 = 0xFFF; const PACKED_Z_MASK: i64 = 0x3FF_FFFF; const X_OFFSET: i32 = 38;
259const Z_OFFSET: i32 = 12;
260
261#[inline]
262fn pack_pos(x: i32, y: i32, z: i32) -> i64 {
263 ((i64::from(x) & PACKED_X_MASK) << X_OFFSET)
264 | (i64::from(y) & PACKED_Y_MASK)
265 | ((i64::from(z) & PACKED_Z_MASK) << Z_OFFSET)
266}
267
268#[inline]
269const fn unpack_x(packed: i64) -> i32 {
270 (packed >> X_OFFSET) as i32
271}
272
273#[inline]
274const fn unpack_y(packed: i64) -> i32 {
275 ((packed << 52) >> 52) as i32
276}
277
278#[inline]
279const fn unpack_z(packed: i64) -> i32 {
280 ((packed << 26) >> X_OFFSET) as i32
281}
282
283#[inline]
286fn similarity(dist_sq1: i32, dist_sq2: i32) -> f64 {
287 1.0 - f64::from(dist_sq2 - dist_sq1) / 25.0
288}
289
290fn is_deep_dark_region<N: DimensionNoises>(
292 noises: &N,
293 cache: &mut N::ColumnCache,
294 x: i32,
295 y: i32,
296 z: i32,
297) -> bool {
298 cache.ensure(x, z, noises);
299 let erosion = f64::from(noises.router_erosion(cache, x, y, z));
300 let depth = f64::from(noises.router_depth(cache, x, y, z));
301 erosion < -0.225 && depth > 0.9
302}
303
304const fn global_fluid(
309 y: i32,
310 lava_floor: i32,
311 sea_level: i32,
312 lava_id: BlockStateId,
313 default_fluid_id: BlockStateId,
314) -> FluidStatus {
315 if y < lava_floor {
316 FluidStatus {
317 fluid_level: LAVA_LEVEL,
318 fluid_type: lava_id,
319 }
320 } else {
321 FluidStatus {
322 fluid_level: sea_level,
323 fluid_type: default_fluid_id,
324 }
325 }
326}
327
328impl<N: DimensionNoises> Aquifer<N> {
329 #[must_use]
337 #[expect(
338 clippy::too_many_arguments,
339 reason = "mirrors vanilla's Aquifer constructor shape"
340 )]
341 pub fn new(
342 chunk_min_x: i32,
343 chunk_min_z: i32,
344 min_block_y: i32,
345 y_block_size: i32,
346 splitter: &RandomSplitter,
347 noises: &N,
348 preliminary_surface: &Arc<PreliminarySurfaceStore>,
349 cache: N::ColumnCache,
350 ) -> Self {
351 Self::new_sized(
352 chunk_min_x,
353 chunk_min_z,
354 16,
355 16,
356 min_block_y,
357 y_block_size,
358 splitter,
359 noises,
360 preliminary_surface,
361 cache,
362 )
363 }
364
365 #[expect(
368 clippy::too_many_arguments,
369 reason = "mirrors vanilla's Aquifer constructor shape"
370 )]
371 pub fn new_sized(
372 chunk_min_x: i32,
373 chunk_min_z: i32,
374 width_x: i32,
375 width_z: i32,
376 min_block_y: i32,
377 y_block_size: i32,
378 splitter: &RandomSplitter,
379 noises: &N,
380 preliminary_surface: &Arc<PreliminarySurfaceStore>,
381 mut cache: N::ColumnCache,
382 ) -> Self {
383 const AQUIFER_HASH: NameHash = NameHash::new("minecraft:aquifer");
384
385 let sea_level = N::Settings::SEA_LEVEL;
386 let water_id = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
387 let lava_id = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::LAVA);
388 let default_fluid_id = N::Settings::default_fluid_id();
389
390 let mut aquifer_rng = splitter.with_hash_of(&AQUIFER_HASH);
391 let splitter = aquifer_rng.next_positional();
392
393 if !N::Settings::AQUIFERS_ENABLED {
396 return Self {
397 location_cache: Vec::new(),
398 status_cache: Vec::new(),
399 splitter,
400 cache,
401 col_cache: AquiferColumnCache::default(),
402 min_grid_x: 0,
403 min_grid_y: 0,
404 min_grid_z: 0,
405 grid_size_x: 0,
406 grid_size_z: 0,
407 skip_sampling_above_y: 0,
408 sea_level,
409 lava_floor: LAVA_LEVEL.min(sea_level),
410 water_id,
411 lava_id,
412 default_fluid_id,
413 should_schedule_fluid_update: false,
414 preliminary_surface: Arc::clone(preliminary_surface),
415 };
416 }
417
418 let chunk_max_x = chunk_min_x + width_x - 1;
419 let chunk_max_z = chunk_min_z + width_z - 1;
420
421 let min_grid_x = grid_x(chunk_min_x + SAMPLE_OFFSET_X);
422 let max_grid_x = grid_x(chunk_max_x + SAMPLE_OFFSET_X) + 1;
423 let grid_size_x = max_grid_x - min_grid_x + 1;
424
425 let min_grid_y = grid_y(min_block_y + SAMPLE_OFFSET_Y) - 1;
426 let max_grid_y = grid_y(min_block_y + y_block_size + SAMPLE_OFFSET_Y) + 1;
427 let grid_size_y = max_grid_y - min_grid_y + 1;
428
429 let min_grid_z = grid_z(chunk_min_z + SAMPLE_OFFSET_Z);
430 let max_grid_z = grid_z(chunk_max_z + SAMPLE_OFFSET_Z) + 1;
431 let grid_size_z = max_grid_z - min_grid_z + 1;
432
433 let total = (grid_size_x * grid_size_y * grid_size_z) as usize;
434 let location_cache = vec![i64::MAX; total];
435 let status_cache = vec![None; total];
436
437 let max_surface = Self::max_preliminary_surface_level(
440 noises,
441 &mut cache,
442 preliminary_surface,
443 from_grid_x(min_grid_x, 0),
444 from_grid_z(min_grid_z, 0),
445 from_grid_x(max_grid_x, X_RANGE - 1),
446 from_grid_z(max_grid_z, Z_RANGE - 1),
447 );
448 let adjusted = max_surface + 8;
449 let skip_grid_y = grid_y(adjusted + 12) + 1;
450 let skip_sampling_above_y = from_grid_y(skip_grid_y, Y_RANGE + 2) - 1;
451
452 Self {
453 location_cache,
454 status_cache,
455 splitter,
456 cache,
457 col_cache: AquiferColumnCache::default(),
458 min_grid_x,
459 min_grid_y,
460 min_grid_z,
461 grid_size_x,
462 grid_size_z,
463 skip_sampling_above_y,
464 sea_level,
465 lava_floor: LAVA_LEVEL.min(sea_level),
466 water_id,
467 lava_id,
468 default_fluid_id,
469 should_schedule_fluid_update: false,
470 preliminary_surface: Arc::clone(preliminary_surface),
471 }
472 }
473
474 fn max_preliminary_surface_level(
475 noises: &N,
476 cache: &mut N::ColumnCache,
477 preliminary_surface: &PreliminarySurfaceStore,
478 min_x: i32,
479 min_z: i32,
480 max_x: i32,
481 max_z: i32,
482 ) -> i32 {
483 let mut max_level = i32::MIN;
484 let mut z = min_z;
486 while z <= max_z {
487 let mut x = min_x;
488 while x <= max_x {
489 let level = preliminary_surface.level(noises, cache, x, z);
490 if level > max_level {
491 max_level = level;
492 }
493 x += 4;
494 }
495 z += 4;
496 }
497 max_level
498 }
499
500 #[inline]
501 const fn get_index(&self, gx: i32, gy: i32, gz: i32) -> usize {
502 let x = gx - self.min_grid_x;
503 let y = gy - self.min_grid_y;
504 let z = gz - self.min_grid_z;
505 ((y * self.grid_size_z + z) * self.grid_size_x + x) as usize
506 }
507
508 fn uniform_cell_status(&mut self, noises: &N) -> Option<FluidStatus> {
510 let first = self.get_aquifer_status(self.col_cache.cell_idx[0] as usize, noises);
511 for i in 1..self.col_cache.cell_idx.len() {
512 let index = self.col_cache.cell_idx[i] as usize;
513 if self.get_aquifer_status(index, noises) != first {
514 return None;
515 }
516 }
517
518 Some(first)
519 }
520
521 fn is_water_over_global_lava(&self, fluid_at: Option<BlockStateId>, world_y: i32) -> bool {
523 if fluid_at != Some(self.water_id) {
524 return false;
525 }
526 let below = global_fluid(
527 world_y - 1,
528 self.lava_floor,
529 self.sea_level,
530 self.lava_id,
531 self.default_fluid_id,
532 );
533 below.fluid_type == self.lava_id && (world_y - 1) < below.fluid_level
534 }
535
536 fn refill_col_cache(&mut self, world_x: i32, world_y: i32, world_z: i32) -> i32 {
538 let x_anchor = grid_x(world_x + SAMPLE_OFFSET_X);
539 let y_anchor = grid_y(world_y + SAMPLE_OFFSET_Y);
540 let z_anchor = grid_z(world_z + SAMPLE_OFFSET_Z);
541
542 let mut i = 0;
543 for x1 in 0..=1i32 {
544 for y1 in -1..=1i32 {
545 for z1 in 0..=1i32 {
546 let gx = x_anchor + x1;
547 let gy = y_anchor + y1;
548 let gz = z_anchor + z1;
549 let idx = self.get_index(gx, gy, gz);
550
551 let loc = self.location_cache[idx];
552 let loc = if loc == i64::MAX {
553 let mut rng = self.splitter.at(gx, gy, gz);
554 let packed = pack_pos(
555 from_grid_x(gx, rng.next_i32_bounded(X_RANGE)),
556 from_grid_y(gy, rng.next_i32_bounded(Y_RANGE)),
557 from_grid_z(gz, rng.next_i32_bounded(Z_RANGE)),
558 );
559 self.location_cache[idx] = packed;
560 packed
561 } else {
562 loc
563 };
564
565 let dx = unpack_x(loc) - world_x;
566 let dz = unpack_z(loc) - world_z;
567 self.col_cache.cell_loc_y[i] = unpack_y(loc);
568 self.col_cache.cell_xz_dist_sq[i] = dx * dx + dz * dz;
569 self.col_cache.cell_idx[i] = idx as u32;
570 i += 1;
571 }
572 }
573 }
574
575 self.col_cache.world_x = world_x;
576 self.col_cache.world_z = world_z;
577 self.col_cache.y_anchor = y_anchor;
578 y_anchor
579 }
580
581 pub fn compute_substance(
583 &mut self,
584 noises: &N,
585 world_x: i32,
586 world_y: i32,
587 world_z: i32,
588 density: f64,
589 ) -> AquiferResult {
590 if density > 0.0 {
592 self.should_schedule_fluid_update = false;
593 return AquiferResult::Solid;
594 }
595
596 if !N::Settings::AQUIFERS_ENABLED {
599 self.should_schedule_fluid_update = false;
600 let gf = global_fluid(
601 world_y,
602 self.lava_floor,
603 self.sea_level,
604 self.lava_id,
605 self.default_fluid_id,
606 );
607 return match gf.at(world_y) {
608 Some(id) => AquiferResult::Fluid(id),
609 None => AquiferResult::Air,
610 };
611 }
612
613 let gf = global_fluid(
614 world_y,
615 self.lava_floor,
616 self.sea_level,
617 self.lava_id,
618 self.default_fluid_id,
619 );
620
621 if world_y > self.skip_sampling_above_y {
623 self.should_schedule_fluid_update = false;
624 return match gf.at(world_y) {
625 Some(id) => AquiferResult::Fluid(id),
626 None => AquiferResult::Air,
627 };
628 }
629
630 if gf.fluid_type == self.lava_id && world_y < gf.fluid_level {
632 self.should_schedule_fluid_update = false;
633 return AquiferResult::Fluid(self.lava_id);
634 }
635
636 let y_anchor = grid_y(world_y + SAMPLE_OFFSET_Y);
641 if self.col_cache.world_x != world_x
642 || self.col_cache.world_z != world_z
643 || self.col_cache.y_anchor != y_anchor
644 {
645 self.refill_col_cache(world_x, world_y, world_z);
646 self.col_cache.uniform_status = self.uniform_cell_status(noises);
647 }
648
649 if let Some(status) = self.col_cache.uniform_status {
652 let fluid_at = status.at(world_y);
653 if !self.is_water_over_global_lava(fluid_at, world_y) {
654 self.should_schedule_fluid_update = false;
655 return match fluid_at {
656 Some(id) => AquiferResult::Fluid(id),
657 None => AquiferResult::Air,
658 };
659 }
660 }
661
662 self.compute_substance_sampled(noises, world_x, world_y, world_z, density)
663 }
664
665 #[expect(
667 clippy::too_many_lines,
668 reason = "splitting would hurt readability of the aquifer sampling logic"
669 )]
670 fn compute_substance_sampled(
671 &mut self,
672 noises: &N,
673 world_x: i32,
674 world_y: i32,
675 world_z: i32,
676 density: f64,
677 ) -> AquiferResult {
678 let world_y_v = i32x4::splat(world_y);
683 let mut dists = [0i32; 12];
684 for batch in 0..3 {
685 let base = batch * 4;
686 let loc_y_v = i32x4::from_slice(&self.col_cache.cell_loc_y[base..base + 4]);
687 let xz_v = i32x4::from_slice(&self.col_cache.cell_xz_dist_sq[base..base + 4]);
688 let dy = loc_y_v - world_y_v;
689 let dist_v = xz_v + dy * dy;
690 dists[base..base + 4].copy_from_slice(&dist_v.to_array());
691 }
692
693 let mut dist_sq = [i32::MAX; 4];
694 let mut closest_idx = [0usize; 4];
695
696 for (i, &new_dist) in dists.iter().enumerate() {
697 let index = self.col_cache.cell_idx[i] as usize;
698
699 if dist_sq[0] >= new_dist {
701 dist_sq[3] = dist_sq[2];
702 closest_idx[3] = closest_idx[2];
703 dist_sq[2] = dist_sq[1];
704 closest_idx[2] = closest_idx[1];
705 dist_sq[1] = dist_sq[0];
706 closest_idx[1] = closest_idx[0];
707 dist_sq[0] = new_dist;
708 closest_idx[0] = index;
709 } else if dist_sq[1] >= new_dist {
710 dist_sq[3] = dist_sq[2];
711 closest_idx[3] = closest_idx[2];
712 dist_sq[2] = dist_sq[1];
713 closest_idx[2] = closest_idx[1];
714 dist_sq[1] = new_dist;
715 closest_idx[1] = index;
716 } else if dist_sq[2] >= new_dist {
717 dist_sq[3] = dist_sq[2];
718 closest_idx[3] = closest_idx[2];
719 dist_sq[2] = new_dist;
720 closest_idx[2] = index;
721 } else if dist_sq[3] >= new_dist {
722 dist_sq[3] = new_dist;
723 closest_idx[3] = index;
724 }
725 }
726
727 let status1 = self.get_aquifer_status(closest_idx[0], noises);
728 let fluid_at = status1.at(world_y);
729
730 let dist12_delta = dist_sq[1] - dist_sq[0];
735 if dist12_delta >= 25 {
736 if dist12_delta <= 12 * 12 - 10 * 10 {
737 let status2 = self.get_aquifer_status(closest_idx[1], noises);
738 self.should_schedule_fluid_update = status1 != status2;
739 } else {
740 self.should_schedule_fluid_update = false;
741 }
742 return match fluid_at {
743 Some(id) => AquiferResult::Fluid(id),
744 None => AquiferResult::Air,
745 };
746 }
747 let sim12 = similarity(dist_sq[0], dist_sq[1]);
748
749 if let Some(id) = fluid_at
751 && self.is_water_over_global_lava(fluid_at, world_y)
752 {
753 self.should_schedule_fluid_update = true;
754 return AquiferResult::Fluid(id);
755 }
756
757 let mut barrier_noise = f64::NAN;
759 let status2 = self.get_aquifer_status(closest_idx[1], noises);
760 let barrier12 = sim12
761 * self.calculate_pressure(
762 noises,
763 world_x,
764 world_y,
765 world_z,
766 &mut barrier_noise,
767 status1,
768 status2,
769 );
770 if density + barrier12 > 0.0 {
771 self.should_schedule_fluid_update = false;
772 return AquiferResult::Solid;
773 }
774
775 let status3 = self.get_aquifer_status(closest_idx[2], noises);
776 let sim13 = similarity(dist_sq[0], dist_sq[2]);
777 if sim13 > 0.0 {
778 let barrier13 = sim12
779 * sim13
780 * self.calculate_pressure(
781 noises,
782 world_x,
783 world_y,
784 world_z,
785 &mut barrier_noise,
786 status1,
787 status3,
788 );
789 if density + barrier13 > 0.0 {
790 self.should_schedule_fluid_update = false;
791 return AquiferResult::Solid;
792 }
793 }
794
795 let sim23 = similarity(dist_sq[1], dist_sq[2]);
796 if sim23 > 0.0 {
797 let barrier23 = sim12
798 * sim23
799 * self.calculate_pressure(
800 noises,
801 world_x,
802 world_y,
803 world_z,
804 &mut barrier_noise,
805 status2,
806 status3,
807 );
808 if density + barrier23 > 0.0 {
809 self.should_schedule_fluid_update = false;
810 return AquiferResult::Solid;
811 }
812 }
813
814 let may_flow12 = status1 != status2;
815 let may_flow23 = sim23 >= FLOWING_UPDATE_SIMILARITY && status2 != status3;
816 let may_flow13 = sim13 >= FLOWING_UPDATE_SIMILARITY && status1 != status3;
817 if may_flow12 || may_flow23 || may_flow13 {
818 self.should_schedule_fluid_update = true;
819 } else {
820 self.should_schedule_fluid_update = sim13 >= FLOWING_UPDATE_SIMILARITY
821 && similarity(dist_sq[0], dist_sq[3]) >= FLOWING_UPDATE_SIMILARITY
822 && status1 != self.get_aquifer_status(closest_idx[3], noises);
823 }
824
825 match fluid_at {
827 Some(id) => AquiferResult::Fluid(id),
828 None => AquiferResult::Air,
829 }
830 }
831
832 #[must_use]
835 pub fn unsampled_air_min_y(&self) -> i32 {
836 if N::Settings::AQUIFERS_ENABLED {
840 self.sea_level.max(self.skip_sampling_above_y + 1)
841 } else {
842 self.sea_level
843 }
844 }
845
846 #[must_use]
848 pub const fn should_schedule_fluid_update(&self) -> bool {
849 self.should_schedule_fluid_update
850 }
851
852 pub fn preliminary_surface_level(&mut self, noises: &N, x: i32, z: i32) -> i32 {
855 self.preliminary_surface
856 .level(noises, &mut self.cache, x, z)
857 }
858
859 fn get_aquifer_status(&mut self, index: usize, noises: &N) -> FluidStatus {
861 if let Some(status) = self.status_cache[index] {
862 return status;
863 }
864
865 let loc = self.location_cache[index];
866 let x = unpack_x(loc);
867 let y = unpack_y(loc);
868 let z = unpack_z(loc);
869 let status = self.compute_fluid(x, y, z, noises);
870 self.status_cache[index] = Some(status);
871 status
872 }
873
874 fn compute_fluid(&mut self, x: i32, y: i32, z: i32, noises: &N) -> FluidStatus {
876 let gf = global_fluid(
877 y,
878 self.lava_floor,
879 self.sea_level,
880 self.lava_id,
881 self.default_fluid_id,
882 );
883 let mut lowest_surface = i32::MAX;
884 let top_of_cell = y + Y_SPACING;
885 let bottom_of_cell = y - Y_SPACING;
886 let mut surface_under_global = false;
887
888 for offset in &SURFACE_SAMPLING_OFFSETS {
889 let sx = x + offset[0] * 16; let sz = z + offset[1] * 16;
891
892 let preliminary = self
893 .preliminary_surface
894 .level(noises, &mut self.cache, sx, sz);
895 let adjusted = preliminary + 8;
896
897 let is_center = offset[0] == 0 && offset[1] == 0;
898
899 if is_center && bottom_of_cell > adjusted {
900 return gf;
901 }
902
903 let top_pokes_above = top_of_cell > adjusted;
904 if top_pokes_above || is_center {
905 let gf_at_surface = global_fluid(
906 adjusted,
907 self.lava_floor,
908 self.sea_level,
909 self.lava_id,
910 self.default_fluid_id,
911 );
912 let has_fluid = adjusted < gf_at_surface.fluid_level;
913 if has_fluid {
914 if is_center {
915 surface_under_global = true;
916 }
917 if top_pokes_above {
918 return gf_at_surface;
919 }
920 }
921 }
922
923 if preliminary < lowest_surface {
924 lowest_surface = preliminary;
925 }
926 }
927
928 let fluid_level =
929 self.compute_surface_level(x, y, z, noises, gf, lowest_surface, surface_under_global);
930 let fluid_type = self.compute_fluid_type(x, y, z, noises, gf, fluid_level);
931 FluidStatus {
932 fluid_level,
933 fluid_type,
934 }
935 }
936
937 #[expect(
938 clippy::too_many_arguments,
939 reason = "matches vanilla NoiseBasedAquifer.computeSurface signature"
940 )]
941 fn compute_surface_level(
942 &mut self,
943 x: i32,
944 y: i32,
945 z: i32,
946 noises: &N,
947 gf: FluidStatus,
948 lowest_surface: i32,
949 surface_under_global: bool,
950 ) -> i32 {
951 let (partially_flooded, fully_flooded) =
952 if is_deep_dark_region(noises, &mut self.cache, x, y, z) {
953 (-1.0, -1.0)
954 } else {
955 let dist_below = lowest_surface + 8 - y;
956 let floodedness_factor = if surface_under_global {
957 map_clamped(f64::from(dist_below), 0.0, 64.0, 1.0, 0.0)
958 } else {
959 0.0
960 };
961
962 if N::aquifer_needs_column_cache() {
963 self.cache.ensure(x, z, noises);
964 }
965 let floodedness_noise = clamp(
966 f64::from(noises.router_fluid_level_floodedness(&mut self.cache, x, y, z)),
967 -1.0,
968 1.0,
969 );
970
971 let fully_threshold = map(floodedness_factor, 1.0, 0.0, -0.3, 0.8);
972 let partially_threshold = map(floodedness_factor, 1.0, 0.0, -0.8, 0.4);
973
974 (
975 floodedness_noise - partially_threshold,
976 floodedness_noise - fully_threshold,
977 )
978 };
979
980 if fully_flooded > 0.0 {
981 gf.fluid_level
982 } else if partially_flooded > 0.0 {
983 self.compute_randomized_fluid_surface_level(x, y, z, noises, lowest_surface)
984 } else {
985 WAY_BELOW_MIN_Y
986 }
987 }
988
989 fn compute_randomized_fluid_surface_level(
990 &mut self,
991 x: i32,
992 y: i32,
993 z: i32,
994 noises: &N,
995 lowest_surface: i32,
996 ) -> i32 {
997 let cell_x = x.div_euclid(16);
998 let cell_y = y.div_euclid(40);
999 let cell_z = z.div_euclid(16);
1000 let cell_middle_y = cell_y * 40 + 20;
1001
1002 if N::aquifer_needs_column_cache() {
1004 self.cache.ensure(cell_x, cell_z, noises);
1005 }
1006 let spread = f64::from(
1007 noises.router_fluid_level_spread(&mut self.cache, cell_x, cell_y, cell_z) * 10.0_f32,
1008 );
1009 let spread_quantized = quantize(spread, 3);
1010 let target = cell_middle_y + spread_quantized;
1011
1012 lowest_surface.min(target)
1013 }
1014
1015 fn compute_fluid_type(
1016 &mut self,
1017 x: i32,
1018 y: i32,
1019 z: i32,
1020 noises: &N,
1021 gf: FluidStatus,
1022 fluid_level: i32,
1023 ) -> BlockStateId {
1024 if fluid_level <= -10 && fluid_level != WAY_BELOW_MIN_Y && gf.fluid_type != self.lava_id {
1025 let cell_x = x.div_euclid(64);
1026 let cell_y = y.div_euclid(40);
1027 let cell_z = z.div_euclid(64);
1028 if N::aquifer_needs_column_cache() {
1029 self.cache.ensure(cell_x, cell_z, noises);
1030 }
1031 let lava_noise = f64::from(noises.router_lava(&mut self.cache, cell_x, cell_y, cell_z));
1032 if lava_noise.abs() > 0.3 {
1033 return self.lava_id;
1034 }
1035 }
1036 gf.fluid_type
1037 }
1038
1039 #[expect(
1043 clippy::too_many_arguments,
1044 reason = "matches vanilla NoiseBasedAquifer.calculatePressure signature"
1045 )]
1046 fn calculate_pressure(
1047 &mut self,
1048 noises: &N,
1049 x: i32,
1050 y: i32,
1051 z: i32,
1052 barrier_noise: &mut f64,
1053 s1: FluidStatus,
1054 s2: FluidStatus,
1055 ) -> f64 {
1056 let f1 = s1.at(y);
1057 let f2 = s2.at(y);
1058 let f1_is_lava = f1 == Some(self.lava_id);
1059 let f2_is_lava = f2 == Some(self.lava_id);
1060 let f1_is_water = f1 == Some(self.water_id);
1061 let f2_is_water = f2 == Some(self.water_id);
1062
1063 if (f1_is_lava && f2_is_water) || (f1_is_water && f2_is_lava) {
1065 return 2.0;
1066 }
1067
1068 let fluid_y_diff = (s1.fluid_level - s2.fluid_level).abs();
1069 if fluid_y_diff == 0 {
1070 return 0.0;
1071 }
1072
1073 let avg_fluid_y = 0.5 * f64::from(s1.fluid_level + s2.fluid_level);
1074 let above_avg = f64::from(y) + 0.5 - avg_fluid_y;
1075 let base = f64::from(fluid_y_diff) / 2.0;
1076 let edge_dist = base - above_avg.abs();
1077
1078 let gradient = if above_avg > 0.0 {
1079 if edge_dist > 0.0 {
1080 edge_dist / 1.5
1081 } else {
1082 edge_dist / 2.5
1083 }
1084 } else {
1085 let center = 3.0 + edge_dist;
1086 if center > 0.0 {
1087 center / 3.0
1088 } else {
1089 center / 10.0
1090 }
1091 };
1092
1093 let noise_val = if !(-2.0..=2.0).contains(&gradient) {
1094 0.0
1095 } else if barrier_noise.is_nan() {
1096 if N::aquifer_needs_column_cache() {
1097 self.cache.ensure(x, z, noises);
1098 }
1099 let n = f64::from(noises.router_barrier(&mut self.cache, x, y, z));
1100 *barrier_noise = n;
1101 n
1102 } else {
1103 *barrier_noise
1104 };
1105
1106 2.0 * (noise_val + gradient)
1107 }
1108}
1109
1110#[inline]
1112fn quantize(value: f64, quantum: i32) -> i32 {
1113 let q = f64::from(quantum);
1114 (value / q).floor() as i32 * quantum
1115}
1116
1117pub fn preliminary_surface_level<N: DimensionNoises>(
1122 noises: &N,
1123 cache: &mut N::ColumnCache,
1124 x: i32,
1125 z: i32,
1126) -> i32 {
1127 let qx = (x >> 2) << 2;
1129 let qz = (z >> 2) << 2;
1130 cache.ensure(qx, qz, noises);
1131 noises
1133 .router_preliminary_surface_level(cache, qx, 0, qz)
1134 .floor() as i32
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use std::sync::Arc;
1140
1141 use steel_utils::random::{Random, xoroshiro::Xoroshiro};
1142
1143 use super::{Aquifer, AquiferResult, SAMPLE_OFFSET_Y, grid_y};
1144 use crate::density::DimensionNoises;
1145 use crate::density_functions::overworld::OverworldNoises;
1146 use crate::noise::PreliminarySurfaceStore;
1147 use crate::noise_parameters::get_noise_parameters;
1148
1149 fn outcome(result: &AquiferResult) -> (u8, u16) {
1150 match result {
1151 AquiferResult::Solid => (0, 0),
1152 AquiferResult::Air => (1, 0),
1153 AquiferResult::Fluid(id) => (2, id.0),
1154 }
1155 }
1156
1157 #[test]
1160 fn uniform_status_shortcut_matches_sampled_path() {
1161 type Settings = <OverworldNoises as DimensionNoises>::Settings;
1162 steel_registry::init_vanilla_registry();
1163 let seed = 1;
1164 let splitter = Xoroshiro::from_seed(seed).next_positional();
1165 let noises = OverworldNoises::create(seed, &splitter, &get_noise_parameters());
1166 let store = Arc::new(PreliminarySurfaceStore::default());
1167
1168 let (mut shortcut, mut compared) = (0, 0);
1169 for (chunk_x, chunk_z) in [(0, 0), (-9, 4), (37, -21), (-150, 88)] {
1170 let (min_x, min_z) = (chunk_x * 16, chunk_z * 16);
1171 let mut cache = <OverworldNoises as DimensionNoises>::ColumnCache::default();
1172 cache.init_grid(min_x, min_z, &noises);
1173 let mut aquifer = Aquifer::<OverworldNoises>::new(
1174 min_x,
1175 min_z,
1176 Settings::MIN_Y,
1177 Settings::HEIGHT,
1178 &splitter,
1179 &noises,
1180 &store,
1181 cache,
1182 );
1183 for x in min_x..min_x + 16 {
1184 for z in min_z..min_z + 16 {
1185 for y in (Settings::MIN_Y..Settings::MIN_Y + Settings::HEIGHT).rev() {
1186 for density in [-0.3, -0.004] {
1187 let result = aquifer.compute_substance(&noises, x, y, z, density);
1188 let flag = aquifer.should_schedule_fluid_update();
1189 let cache = &aquifer.col_cache;
1190 let current = cache.world_x == x
1191 && cache.world_z == z
1192 && cache.y_anchor == grid_y(y + SAMPLE_OFFSET_Y);
1193 if !current || cache.uniform_status.is_none() {
1194 continue;
1195 }
1196 shortcut += 1;
1197 let sampled =
1198 aquifer.compute_substance_sampled(&noises, x, y, z, density);
1199 assert_eq!(
1200 (outcome(&result), flag),
1201 (outcome(&sampled), aquifer.should_schedule_fluid_update()),
1202 "block ({x}, {y}, {z}) density {density}"
1203 );
1204 compared += 1;
1205 }
1206 }
1207 }
1208 }
1209 }
1210 assert!(shortcut > 0, "shortcut never applied");
1211 assert_eq!(shortcut, compared);
1212 }
1213}