Skip to main content

steel_worldgen/noise/
aquifer.rs

1//! Noise-based aquifer system for underground fluid placement.
2//!
3//! Matches vanilla's `Aquifer.NoiseBasedAquifer`. Divides the world into a
4//! 16×12×16 grid of aquifer cells, each with a randomly-jittered center point.
5//! For each non-solid block, finds the 4 nearest aquifer centers and computes
6//! fluid status (water vs lava, surface level) based on noise functions.
7//! Barrier pressure between neighboring aquifer cells creates solid rock
8//! walls between fluid pockets.
9
10use std::simd::i32x4;
11
12use rustc_hash::FxHashMap;
13
14use crate::density::{ColumnCache, DimensionNoises, NoiseSettings};
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
21/// Deferred [`Aquifer`]. Used by `create_structures` so chunks where no structure
22/// queries the aquifer skip its (expensive) `max_preliminary_surface_level` scan.
23pub struct LazyAquifer<'a, N: DimensionNoises> {
24    chunk_min_x: i32,
25    chunk_min_z: i32,
26    splitter: &'a RandomSplitter,
27    noises: &'a N,
28    inner: Option<Aquifer<N>>,
29}
30
31impl<'a, N: DimensionNoises> LazyAquifer<'a, N> {
32    /// Deferred aquifer for the given chunk.
33    #[must_use]
34    pub const fn new(
35        chunk_min_x: i32,
36        chunk_min_z: i32,
37        splitter: &'a RandomSplitter,
38        noises: &'a N,
39    ) -> Self {
40        Self {
41            chunk_min_x,
42            chunk_min_z,
43            splitter,
44            noises,
45            inner: None,
46        }
47    }
48
49    /// Build on first call; `height_cache` is cloned into the aquifer's own cache.
50    ///
51    /// # Panics
52    /// Never — `inner` is initialized above if it was `None`.
53    pub fn ensure(&mut self, height_cache: &N::ColumnCache) -> &mut Aquifer<N> {
54        if self.inner.is_none() {
55            self.inner = Some(Aquifer::<N>::new(
56                self.chunk_min_x,
57                self.chunk_min_z,
58                <N::Settings as NoiseSettings>::MIN_Y,
59                <N::Settings as NoiseSettings>::HEIGHT,
60                self.splitter,
61                self.noises,
62                height_cache.clone(),
63            ));
64        }
65        #[expect(clippy::unwrap_used, reason = "just initialized above")]
66        self.inner.as_mut().unwrap()
67    }
68}
69
70// Grid spacing
71const Y_SPACING: i32 = 12;
72
73// Jitter range per cell center
74const X_RANGE: i32 = 10;
75const Y_RANGE: i32 = 9;
76const Z_RANGE: i32 = 10;
77
78// Anchor offsets for neighborhood lookup
79const SAMPLE_OFFSET_X: i32 = -5;
80const SAMPLE_OFFSET_Y: i32 = 1;
81const SAMPLE_OFFSET_Z: i32 = -5;
82
83const LAVA_LEVEL: i32 = -54;
84/// Sentinel for "no fluid" — well below any real Y coordinate.
85const WAY_BELOW_MIN_Y: i32 = -32512;
86const FLOWING_UPDATE_SIMILARITY: f64 = 1.0 - ((12 * 12 - 10 * 10) as f64) / 25.0;
87
88/// Chunk offsets (in chunks, ×16 for blocks) used when sampling
89/// preliminary surface levels around an aquifer cell center.
90const SURFACE_SAMPLING_OFFSETS: [[i32; 2]; 13] = [
91    [0, 0],
92    [-2, -1],
93    [-1, -1],
94    [0, -1],
95    [1, -1],
96    [-3, 0],
97    [-2, 0],
98    [-1, 0],
99    [1, 0],
100    [-2, 1],
101    [-1, 1],
102    [0, 1],
103    [1, 1],
104];
105
106/// Fluid status at an aquifer cell center.
107///
108/// Matches vanilla's `Aquifer.FluidStatus` — stores the actual fluid block state
109/// rather than a boolean flag, so the aquifer is agnostic to which fluids exist.
110#[derive(Clone, Copy, PartialEq, Eq)]
111struct FluidStatus {
112    /// Y level of the fluid surface (exclusive upper bound).
113    fluid_level: i32,
114    /// Block state placed below `fluid_level`.
115    fluid_type: BlockStateId,
116}
117
118impl FluidStatus {
119    /// What block is at `block_y`? Returns the fluid type if below the surface,
120    /// or `None` for air above the surface.
121    const fn at(self, block_y: i32) -> Option<BlockStateId> {
122        if block_y < self.fluid_level {
123            Some(self.fluid_type)
124        } else {
125            None
126        }
127    }
128}
129
130/// Result of the aquifer substance check.
131pub enum AquiferResult {
132    /// Solid block (density > 0 or barrier makes it solid).
133    Solid,
134    /// Air (no block placed).
135    Air,
136    /// Fluid block to place.
137    Fluid(BlockStateId),
138}
139
140/// Column-scan state for the 12 aquifer-neighborhood cells, stored `SoA` so the
141/// per-Y distance computation can be SIMD-batched as 3× `i32x4`.
142///
143/// `compute_substance` is called many times with the same `(world_x, world_z)`
144/// and decreasing `world_y` (innermost loop in `noise_chunk::fill`). Within a
145/// stable `y_anchor` window (`Y_SPACING` blocks tall) the 12 cells of the
146/// neighborhood don't change, and `dx*dx + dz*dz` only depends on x/z — only
147/// `dy = loc_y - world_y` varies per call.
148///
149/// `y_anchor = i32::MIN` marks the cache as invalid (forces a refill).
150struct AquiferColumnCache {
151    world_x: i32,
152    world_z: i32,
153    y_anchor: i32,
154    /// Per-cell unpacked Y of the aquifer-cell center (constant while cached).
155    /// Padded to 16 entries so the 12 valid cells fit cleanly into 3× i32x4
156    /// SIMD batches; trailing slots stay at default `0`.
157    cell_loc_y: [i32; 16],
158    /// Per-cell `(dx + fx)² + (dz + fz)²` (Y-independent component of distance).
159    cell_xz_dist_sq: [i32; 16],
160    /// Per-cell index into `location_cache` / `status_cache`.
161    cell_idx: [u32; 12],
162}
163
164impl Default for AquiferColumnCache {
165    fn default() -> Self {
166        Self {
167            world_x: 0,
168            world_z: 0,
169            y_anchor: i32::MIN,
170            cell_loc_y: [0; 16],
171            cell_xz_dist_sq: [0; 16],
172            cell_idx: [0; 12],
173        }
174    }
175}
176
177/// Noise-based aquifer for a single chunk.
178///
179/// Constructed once per chunk, used throughout the fill loop.
180pub struct Aquifer<N: DimensionNoises> {
181    /// Packed (x, y, z) locations of aquifer cell centers.
182    /// `i64::MAX` = not yet computed.
183    location_cache: Vec<i64>,
184    /// Lazily computed fluid statuses per grid cell.
185    status_cache: Vec<Option<FluidStatus>>,
186    /// Positional random for grid cell jitter.
187    splitter: RandomSplitter,
188    /// Column cache owned by the aquifer for density function evaluation.
189    cache: N::ColumnCache,
190    /// Grid bounds.
191    min_grid_x: i32,
192    min_grid_y: i32,
193    min_grid_z: i32,
194    grid_size_x: i32,
195    grid_size_z: i32,
196    /// Skip aquifer sampling above this Y (optimization).
197    skip_sampling_above_y: i32,
198    /// Sea level for this dimension.
199    sea_level: i32,
200    /// Precomputed `min(LAVA_LEVEL, sea_level)`. Hoisted out of `global_fluid`
201    /// (which is on the per-block hot path) so we don't recompute it millions
202    /// of times per chunk.
203    lava_floor: i32,
204    /// Block state IDs.
205    water_id: BlockStateId,
206    lava_id: BlockStateId,
207    /// The dimension's default fluid (water for overworld, lava for nether).
208    default_fluid_id: BlockStateId,
209    /// Vanilla's `shouldScheduleFluidUpdate` flag from the most recent substance lookup.
210    should_schedule_fluid_update: bool,
211    /// 12-cell neighborhood snapshot for the current Y-column.
212    /// Placed at the end so dimensions with disabled aquifers (nether/end)
213    /// keep the hot fluid-id fields earlier in the struct's cache lines.
214    col_cache: AquiferColumnCache,
215    /// Per-quart-column cache of `preliminary_surface_level` results, matching
216    /// vanilla's `NoiseBasedAquifer.preliminarySurfaceLevel` `Long2IntMap`.
217    /// `compute_fluid` samples surface level 13× per aquifer cell, and each miss
218    /// recomputes the entire flat `NormalNoise` router for that column via
219    /// `cache.ensure`. Memoizing the `i32` result per column collapses that to
220    /// one evaluation per unique column for the chunk.
221    prelim_cache: FxHashMap<(i32, i32), i32>,
222}
223
224// Grid coordinate conversions
225#[inline]
226const fn grid_x(block: i32) -> i32 {
227    block >> 4
228}
229#[inline]
230const fn grid_z(block: i32) -> i32 {
231    block >> 4
232}
233#[inline]
234const fn grid_y(block: i32) -> i32 {
235    block.div_euclid(Y_SPACING)
236}
237#[inline]
238const fn from_grid_x(grid: i32, offset: i32) -> i32 {
239    (grid << 4) + offset
240}
241#[inline]
242const fn from_grid_y(grid: i32, offset: i32) -> i32 {
243    grid * Y_SPACING + offset
244}
245#[inline]
246const fn from_grid_z(grid: i32, offset: i32) -> i32 {
247    (grid << 4) + offset
248}
249
250// BlockPos packing (matches vanilla's BlockPos.asLong / getX / getY / getZ)
251const PACKED_X_MASK: i64 = 0x3FF_FFFF; // 26 bits
252const PACKED_Y_MASK: i64 = 0xFFF; // 12 bits
253const PACKED_Z_MASK: i64 = 0x3FF_FFFF; // 26 bits
254const X_OFFSET: i32 = 38;
255const Z_OFFSET: i32 = 12;
256
257#[inline]
258fn pack_pos(x: i32, y: i32, z: i32) -> i64 {
259    ((i64::from(x) & PACKED_X_MASK) << X_OFFSET)
260        | (i64::from(y) & PACKED_Y_MASK)
261        | ((i64::from(z) & PACKED_Z_MASK) << Z_OFFSET)
262}
263
264#[inline]
265const fn unpack_x(packed: i64) -> i32 {
266    (packed >> X_OFFSET) as i32
267}
268
269#[inline]
270const fn unpack_y(packed: i64) -> i32 {
271    ((packed << 52) >> 52) as i32
272}
273
274#[inline]
275const fn unpack_z(packed: i64) -> i32 {
276    ((packed << 26) >> X_OFFSET) as i32
277}
278
279/// Similarity between two squared distances. Positive when the two nearest
280/// aquifer cells are close together (near a boundary).
281#[inline]
282fn similarity(dist_sq1: i32, dist_sq2: i32) -> f64 {
283    1.0 - f64::from(dist_sq2 - dist_sq1) / 25.0
284}
285
286/// Deep dark region check matching `OverworldBiomeBuilder.isDeepDarkRegion`.
287fn is_deep_dark_region<N: DimensionNoises>(
288    noises: &N,
289    cache: &mut N::ColumnCache,
290    x: i32,
291    y: i32,
292    z: i32,
293) -> bool {
294    cache.ensure(x, z, noises);
295    let erosion = noises.router_erosion(cache, x, y, z);
296    let depth = noises.router_depth(cache, x, y, z);
297    erosion < -0.225 && depth > 0.9
298}
299
300/// Global fluid picker matching vanilla's `NoiseBasedChunkGenerator.createFluidPicker`.
301///
302/// Below `min(-54, sea_level)` → lava at Y=-54. Otherwise → the dimension's
303/// default fluid at sea level (water for overworld, lava for nether).
304const fn global_fluid(
305    y: i32,
306    lava_floor: i32,
307    sea_level: i32,
308    lava_id: BlockStateId,
309    default_fluid_id: BlockStateId,
310) -> FluidStatus {
311    if y < lava_floor {
312        FluidStatus {
313            fluid_level: LAVA_LEVEL,
314            fluid_type: lava_id,
315        }
316    } else {
317        FluidStatus {
318            fluid_level: sea_level,
319            fluid_type: default_fluid_id,
320        }
321    }
322}
323
324impl<N: DimensionNoises> Aquifer<N> {
325    /// Create an aquifer for a full 16×16 chunk.
326    ///
327    /// `chunk_min_x/z` are the block coordinates of the chunk's NW corner.
328    /// `min_block_y` and `y_block_size` define the vertical range.
329    /// `splitter` is the seed's positional splitter.
330    /// `cache` should be a pre-initialized column cache for this chunk
331    /// (avoids a redundant `init_grid` call).
332    #[must_use]
333    pub fn new(
334        chunk_min_x: i32,
335        chunk_min_z: i32,
336        min_block_y: i32,
337        y_block_size: i32,
338        splitter: &RandomSplitter,
339        noises: &N,
340        cache: N::ColumnCache,
341    ) -> Self {
342        Self::new_sized(
343            chunk_min_x,
344            chunk_min_z,
345            16,
346            16,
347            min_block_y,
348            y_block_size,
349            splitter,
350            noises,
351            cache,
352        )
353    }
354
355    /// Create an aquifer with custom XZ extent (in blocks).
356    /// Vanilla's iterateNoiseColumn uses width=cellWidth (4) for single-column queries.
357    #[expect(
358        clippy::too_many_arguments,
359        reason = "mirrors vanilla's Aquifer constructor shape"
360    )]
361    pub fn new_sized(
362        chunk_min_x: i32,
363        chunk_min_z: i32,
364        width_x: i32,
365        width_z: i32,
366        min_block_y: i32,
367        y_block_size: i32,
368        splitter: &RandomSplitter,
369        noises: &N,
370        mut cache: N::ColumnCache,
371    ) -> Self {
372        const AQUIFER_HASH: NameHash = NameHash::new("minecraft:aquifer");
373
374        let sea_level = N::Settings::SEA_LEVEL;
375        let water_id = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
376        let lava_id = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::LAVA);
377        let default_fluid_id = N::Settings::default_fluid_id();
378
379        let mut aquifer_rng = splitter.with_hash_of(&AQUIFER_HASH);
380        let splitter = aquifer_rng.next_positional();
381
382        // When aquifers are disabled (nether/end), compute_substance uses only
383        // the global fluid picker — skip grid allocation and surface sampling.
384        if !N::Settings::AQUIFERS_ENABLED {
385            return Self {
386                location_cache: Vec::new(),
387                status_cache: Vec::new(),
388                splitter,
389                cache,
390                col_cache: AquiferColumnCache::default(),
391                min_grid_x: 0,
392                min_grid_y: 0,
393                min_grid_z: 0,
394                grid_size_x: 0,
395                grid_size_z: 0,
396                skip_sampling_above_y: 0,
397                sea_level,
398                lava_floor: LAVA_LEVEL.min(sea_level),
399                water_id,
400                lava_id,
401                default_fluid_id,
402                should_schedule_fluid_update: false,
403                prelim_cache: FxHashMap::default(),
404            };
405        }
406
407        let chunk_max_x = chunk_min_x + width_x - 1;
408        let chunk_max_z = chunk_min_z + width_z - 1;
409
410        let min_grid_x = grid_x(chunk_min_x + SAMPLE_OFFSET_X);
411        let max_grid_x = grid_x(chunk_max_x + SAMPLE_OFFSET_X) + 1;
412        let grid_size_x = max_grid_x - min_grid_x + 1;
413
414        let min_grid_y = grid_y(min_block_y + SAMPLE_OFFSET_Y) - 1;
415        let max_grid_y = grid_y(min_block_y + y_block_size + SAMPLE_OFFSET_Y) + 1;
416        let grid_size_y = max_grid_y - min_grid_y + 1;
417
418        let min_grid_z = grid_z(chunk_min_z + SAMPLE_OFFSET_Z);
419        let max_grid_z = grid_z(chunk_max_z + SAMPLE_OFFSET_Z) + 1;
420        let grid_size_z = max_grid_z - min_grid_z + 1;
421
422        let total = (grid_size_x * grid_size_y * grid_size_z) as usize;
423        let location_cache = vec![i64::MAX; total];
424        let status_cache = vec![None; total];
425
426        // Compute skip_sampling_above_y from max preliminary surface level.
427        // The scan primes `prelim_cache` for the columns `compute_fluid` reuses.
428        let mut prelim_cache = FxHashMap::default();
429        let max_surface = Self::max_preliminary_surface_level(
430            noises,
431            &mut cache,
432            &mut prelim_cache,
433            from_grid_x(min_grid_x, 0),
434            from_grid_z(min_grid_z, 0),
435            from_grid_x(max_grid_x, X_RANGE - 1),
436            from_grid_z(max_grid_z, Z_RANGE - 1),
437        );
438        let adjusted = max_surface + 8;
439        let skip_grid_y = grid_y(adjusted + 12) + 1;
440        let skip_sampling_above_y = from_grid_y(skip_grid_y, Y_RANGE + 2) - 1;
441
442        Self {
443            location_cache,
444            status_cache,
445            splitter,
446            cache,
447            col_cache: AquiferColumnCache::default(),
448            min_grid_x,
449            min_grid_y,
450            min_grid_z,
451            grid_size_x,
452            grid_size_z,
453            skip_sampling_above_y,
454            sea_level,
455            lava_floor: LAVA_LEVEL.min(sea_level),
456            water_id,
457            lava_id,
458            default_fluid_id,
459            should_schedule_fluid_update: false,
460            prelim_cache,
461        }
462    }
463
464    fn max_preliminary_surface_level(
465        noises: &N,
466        cache: &mut N::ColumnCache,
467        prelim_cache: &mut FxHashMap<(i32, i32), i32>,
468        min_x: i32,
469        min_z: i32,
470        max_x: i32,
471        max_z: i32,
472    ) -> i32 {
473        let mut max_level = i32::MIN;
474        // Sample at 4-block intervals (quart-block resolution) across the chunk area
475        let mut z = min_z;
476        while z <= max_z {
477            let mut x = min_x;
478            while x <= max_x {
479                let level = cached_preliminary_surface_level(noises, cache, prelim_cache, x, z);
480                if level > max_level {
481                    max_level = level;
482                }
483                x += 4;
484            }
485            z += 4;
486        }
487        max_level
488    }
489
490    #[inline]
491    const fn get_index(&self, gx: i32, gy: i32, gz: i32) -> usize {
492        let x = gx - self.min_grid_x;
493        let y = gy - self.min_grid_y;
494        let z = gz - self.min_grid_z;
495        ((y * self.grid_size_z + z) * self.grid_size_x + x) as usize
496    }
497
498    /// Refill the 12-cell column cache for the current `(world_x, world_z, y_anchor)`.
499    ///
500    /// Iterates the same `(x1, y1, z1)` order as the inline scan so cells are
501    /// stored at consistent indices, preserving tie-breaking when the per-Y
502    /// `new_dist` values are compared in `compute_substance`.
503    fn refill_col_cache(&mut self, world_x: i32, world_y: i32, world_z: i32) -> i32 {
504        let x_anchor = grid_x(world_x + SAMPLE_OFFSET_X);
505        let y_anchor = grid_y(world_y + SAMPLE_OFFSET_Y);
506        let z_anchor = grid_z(world_z + SAMPLE_OFFSET_Z);
507
508        let mut i = 0;
509        for x1 in 0..=1i32 {
510            for y1 in -1..=1i32 {
511                for z1 in 0..=1i32 {
512                    let gx = x_anchor + x1;
513                    let gy = y_anchor + y1;
514                    let gz = z_anchor + z1;
515                    let idx = self.get_index(gx, gy, gz);
516
517                    let loc = self.location_cache[idx];
518                    let loc = if loc == i64::MAX {
519                        let mut rng = self.splitter.at(gx, gy, gz);
520                        let packed = pack_pos(
521                            from_grid_x(gx, rng.next_i32_bounded(X_RANGE)),
522                            from_grid_y(gy, rng.next_i32_bounded(Y_RANGE)),
523                            from_grid_z(gz, rng.next_i32_bounded(Z_RANGE)),
524                        );
525                        self.location_cache[idx] = packed;
526                        packed
527                    } else {
528                        loc
529                    };
530
531                    let dx = unpack_x(loc) - world_x;
532                    let dz = unpack_z(loc) - world_z;
533                    self.col_cache.cell_loc_y[i] = unpack_y(loc);
534                    self.col_cache.cell_xz_dist_sq[i] = dx * dx + dz * dz;
535                    self.col_cache.cell_idx[i] = idx as u32;
536                    i += 1;
537                }
538            }
539        }
540
541        self.col_cache.world_x = world_x;
542        self.col_cache.world_z = world_z;
543        self.col_cache.y_anchor = y_anchor;
544        y_anchor
545    }
546
547    /// Compute what block to place at this position given the interpolated density.
548    #[expect(
549        clippy::too_many_lines,
550        reason = "splitting would hurt readability of the aquifer sampling logic"
551    )]
552    pub fn compute_substance(
553        &mut self,
554        noises: &N,
555        world_x: i32,
556        world_y: i32,
557        world_z: i32,
558        density: f64,
559    ) -> AquiferResult {
560        // Solid block — let the caller decide (stone or ore)
561        if density > 0.0 {
562            self.should_schedule_fluid_update = false;
563            return AquiferResult::Solid;
564        }
565
566        // Disabled aquifers (nether/end): use global fluid picker directly,
567        // matching vanilla's `Aquifer.createDisabled`.
568        if !N::Settings::AQUIFERS_ENABLED {
569            self.should_schedule_fluid_update = false;
570            let gf = global_fluid(
571                world_y,
572                self.lava_floor,
573                self.sea_level,
574                self.lava_id,
575                self.default_fluid_id,
576            );
577            return match gf.at(world_y) {
578                Some(id) => AquiferResult::Fluid(id),
579                None => AquiferResult::Air,
580            };
581        }
582
583        let gf = global_fluid(
584            world_y,
585            self.lava_floor,
586            self.sea_level,
587            self.lava_id,
588            self.default_fluid_id,
589        );
590
591        // Above the skip threshold: use global fluid directly
592        if world_y > self.skip_sampling_above_y {
593            self.should_schedule_fluid_update = false;
594            return match gf.at(world_y) {
595                Some(id) => AquiferResult::Fluid(id),
596                None => AquiferResult::Air,
597            };
598        }
599
600        // If global fluid is lava here, return lava
601        if gf.fluid_type == self.lava_id && world_y < gf.fluid_level {
602            self.should_schedule_fluid_update = false;
603            return AquiferResult::Fluid(self.lava_id);
604        }
605
606        // Find 4 nearest aquifer cell centers from the 2×3×2 neighborhood.
607        // Within a Y-column scan, the 12 cells and their `xz_dist_sq` values
608        // are constant for a given `y_anchor`; only `dy` varies. The column
609        // cache amortizes location lookup, splitter calls, and i64 unpacking.
610        let y_anchor = grid_y(world_y + SAMPLE_OFFSET_Y);
611        if self.col_cache.world_x != world_x
612            || self.col_cache.world_z != world_z
613            || self.col_cache.y_anchor != y_anchor
614        {
615            self.refill_col_cache(world_x, world_y, world_z);
616        }
617
618        // SIMD-batch the per-cell distance computation: `loc_y - world_y` then
619        // `xz_dist_sq + dy²`, processing 4 cells per `i32x4` op. The 12 valid
620        // cells fit in 3 batches; the 4-slot tail of `cell_loc_y` /
621        // `cell_xz_dist_sq` is harmless padding (we ignore the trailing slot).
622        let world_y_v = i32x4::splat(world_y);
623        let mut dists = [0i32; 12];
624        for batch in 0..3 {
625            let base = batch * 4;
626            let loc_y_v = i32x4::from_slice(&self.col_cache.cell_loc_y[base..base + 4]);
627            let xz_v = i32x4::from_slice(&self.col_cache.cell_xz_dist_sq[base..base + 4]);
628            let dy = loc_y_v - world_y_v;
629            let dist_v = xz_v + dy * dy;
630            dists[base..base + 4].copy_from_slice(&dist_v.to_array());
631        }
632
633        let mut dist_sq = [i32::MAX; 4];
634        let mut closest_idx = [0usize; 4];
635
636        for (i, &new_dist) in dists.iter().enumerate() {
637            let index = self.col_cache.cell_idx[i] as usize;
638
639            // Insert into sorted top-4
640            if dist_sq[0] >= new_dist {
641                dist_sq[3] = dist_sq[2];
642                closest_idx[3] = closest_idx[2];
643                dist_sq[2] = dist_sq[1];
644                closest_idx[2] = closest_idx[1];
645                dist_sq[1] = dist_sq[0];
646                closest_idx[1] = closest_idx[0];
647                dist_sq[0] = new_dist;
648                closest_idx[0] = index;
649            } else if dist_sq[1] >= new_dist {
650                dist_sq[3] = dist_sq[2];
651                closest_idx[3] = closest_idx[2];
652                dist_sq[2] = dist_sq[1];
653                closest_idx[2] = closest_idx[1];
654                dist_sq[1] = new_dist;
655                closest_idx[1] = index;
656            } else if dist_sq[2] >= new_dist {
657                dist_sq[3] = dist_sq[2];
658                closest_idx[3] = closest_idx[2];
659                dist_sq[2] = new_dist;
660                closest_idx[2] = index;
661            } else if dist_sq[3] >= new_dist {
662                dist_sq[3] = new_dist;
663                closest_idx[3] = index;
664            }
665        }
666
667        let status1 = self.get_aquifer_status(closest_idx[0], noises);
668        let fluid_at = status1.at(world_y);
669
670        // `similarity(d1, d2) = 1 - (d2 - d1) / 25`, so `sim12 <= 0.0` is exactly
671        // `d2 - d1 >= 25` in i32. Defer the f64 conversion + divide until after
672        // the early-return check. Fluid-update scheduling still matches vanilla:
673        // `sim12 >= FLOWING_UPDATE_SIMILARITY` is exactly `d2 - d1 <= 44`.
674        let dist12_delta = dist_sq[1] - dist_sq[0];
675        if dist12_delta >= 25 {
676            if dist12_delta <= 12 * 12 - 10 * 10 {
677                let status2 = self.get_aquifer_status(closest_idx[1], noises);
678                self.should_schedule_fluid_update = status1 != status2;
679            } else {
680                self.should_schedule_fluid_update = false;
681            }
682            return match fluid_at {
683                Some(id) => AquiferResult::Fluid(id),
684                None => AquiferResult::Air,
685            };
686        }
687        let sim12 = similarity(dist_sq[0], dist_sq[1]);
688
689        // Water adjacent to global lava below → return water
690        if let Some(id) = fluid_at
691            && id == self.water_id
692        {
693            let below = global_fluid(
694                world_y - 1,
695                self.lava_floor,
696                self.sea_level,
697                self.lava_id,
698                self.default_fluid_id,
699            );
700            if below.fluid_type == self.lava_id && (world_y - 1) < below.fluid_level {
701                self.should_schedule_fluid_update = true;
702                return AquiferResult::Fluid(id);
703            }
704        }
705
706        // Compute barrier pressure between closest pairs
707        let mut barrier_noise = f64::NAN;
708        let status2 = self.get_aquifer_status(closest_idx[1], noises);
709        let barrier12 = sim12
710            * self.calculate_pressure(
711                noises,
712                world_x,
713                world_y,
714                world_z,
715                &mut barrier_noise,
716                status1,
717                status2,
718            );
719        if density + barrier12 > 0.0 {
720            self.should_schedule_fluid_update = false;
721            return AquiferResult::Solid;
722        }
723
724        let status3 = self.get_aquifer_status(closest_idx[2], noises);
725        let sim13 = similarity(dist_sq[0], dist_sq[2]);
726        if sim13 > 0.0 {
727            let barrier13 = sim12
728                * sim13
729                * self.calculate_pressure(
730                    noises,
731                    world_x,
732                    world_y,
733                    world_z,
734                    &mut barrier_noise,
735                    status1,
736                    status3,
737                );
738            if density + barrier13 > 0.0 {
739                self.should_schedule_fluid_update = false;
740                return AquiferResult::Solid;
741            }
742        }
743
744        let sim23 = similarity(dist_sq[1], dist_sq[2]);
745        if sim23 > 0.0 {
746            let barrier23 = sim12
747                * sim23
748                * self.calculate_pressure(
749                    noises,
750                    world_x,
751                    world_y,
752                    world_z,
753                    &mut barrier_noise,
754                    status2,
755                    status3,
756                );
757            if density + barrier23 > 0.0 {
758                self.should_schedule_fluid_update = false;
759                return AquiferResult::Solid;
760            }
761        }
762
763        let may_flow12 = status1 != status2;
764        let may_flow23 = sim23 >= FLOWING_UPDATE_SIMILARITY && status2 != status3;
765        let may_flow13 = sim13 >= FLOWING_UPDATE_SIMILARITY && status1 != status3;
766        if may_flow12 || may_flow23 || may_flow13 {
767            self.should_schedule_fluid_update = true;
768        } else {
769            self.should_schedule_fluid_update = sim13 >= FLOWING_UPDATE_SIMILARITY
770                && similarity(dist_sq[0], dist_sq[3]) >= FLOWING_UPDATE_SIMILARITY
771                && status1 != self.get_aquifer_status(closest_idx[3], noises);
772        }
773
774        // Return the closest fluid
775        match fluid_at {
776            Some(id) => AquiferResult::Fluid(id),
777            None => AquiferResult::Air,
778        }
779    }
780
781    /// Returns whether the most recent substance lookup needs postprocessing for placed fluids.
782    #[must_use]
783    pub const fn should_schedule_fluid_update(&self) -> bool {
784        self.should_schedule_fluid_update
785    }
786
787    /// Returns the quart-quantized preliminary surface level, reusing this aquifer's
788    /// density-column and result caches.
789    pub fn preliminary_surface_level(&mut self, noises: &N, x: i32, z: i32) -> i32 {
790        cached_preliminary_surface_level(noises, &mut self.cache, &mut self.prelim_cache, x, z)
791    }
792
793    /// Get or compute the fluid status for the aquifer cell at the given cache index.
794    fn get_aquifer_status(&mut self, index: usize, noises: &N) -> FluidStatus {
795        if let Some(status) = self.status_cache[index] {
796            return status;
797        }
798
799        let loc = self.location_cache[index];
800        let x = unpack_x(loc);
801        let y = unpack_y(loc);
802        let z = unpack_z(loc);
803        let status = self.compute_fluid(x, y, z, noises);
804        self.status_cache[index] = Some(status);
805        status
806    }
807
808    /// Compute the fluid status for an aquifer cell centered at (x, y, z).
809    fn compute_fluid(&mut self, x: i32, y: i32, z: i32, noises: &N) -> FluidStatus {
810        let gf = global_fluid(
811            y,
812            self.lava_floor,
813            self.sea_level,
814            self.lava_id,
815            self.default_fluid_id,
816        );
817        let mut lowest_surface = i32::MAX;
818        let top_of_cell = y + Y_SPACING;
819        let bottom_of_cell = y - Y_SPACING;
820        let mut surface_under_global = false;
821
822        for offset in &SURFACE_SAMPLING_OFFSETS {
823            let sx = x + offset[0] * 16; // sectionToBlockCoord
824            let sz = z + offset[1] * 16;
825
826            let preliminary = cached_preliminary_surface_level(
827                noises,
828                &mut self.cache,
829                &mut self.prelim_cache,
830                sx,
831                sz,
832            );
833            let adjusted = preliminary + 8;
834
835            let is_center = offset[0] == 0 && offset[1] == 0;
836
837            if is_center && bottom_of_cell > adjusted {
838                return gf;
839            }
840
841            let top_pokes_above = top_of_cell > adjusted;
842            if top_pokes_above || is_center {
843                let gf_at_surface = global_fluid(
844                    adjusted,
845                    self.lava_floor,
846                    self.sea_level,
847                    self.lava_id,
848                    self.default_fluid_id,
849                );
850                let has_fluid = adjusted < gf_at_surface.fluid_level;
851                if has_fluid {
852                    if is_center {
853                        surface_under_global = true;
854                    }
855                    if top_pokes_above {
856                        return gf_at_surface;
857                    }
858                }
859            }
860
861            if preliminary < lowest_surface {
862                lowest_surface = preliminary;
863            }
864        }
865
866        let fluid_level =
867            self.compute_surface_level(x, y, z, noises, gf, lowest_surface, surface_under_global);
868        let fluid_type = self.compute_fluid_type(x, y, z, noises, gf, fluid_level);
869        FluidStatus {
870            fluid_level,
871            fluid_type,
872        }
873    }
874
875    #[expect(
876        clippy::too_many_arguments,
877        reason = "matches vanilla NoiseBasedAquifer.computeSurface signature"
878    )]
879    fn compute_surface_level(
880        &mut self,
881        x: i32,
882        y: i32,
883        z: i32,
884        noises: &N,
885        gf: FluidStatus,
886        lowest_surface: i32,
887        surface_under_global: bool,
888    ) -> i32 {
889        let (partially_flooded, fully_flooded) =
890            if is_deep_dark_region(noises, &mut self.cache, x, y, z) {
891                (-1.0, -1.0)
892            } else {
893                let dist_below = lowest_surface + 8 - y;
894                let floodedness_factor = if surface_under_global {
895                    map_clamped(f64::from(dist_below), 0.0, 64.0, 1.0, 0.0)
896                } else {
897                    0.0
898                };
899
900                self.cache.ensure(x, z, noises);
901                let floodedness_noise = clamp(
902                    noises.router_fluid_level_floodedness(&mut self.cache, x, y, z),
903                    -1.0,
904                    1.0,
905                );
906
907                let fully_threshold = map(floodedness_factor, 1.0, 0.0, -0.3, 0.8);
908                let partially_threshold = map(floodedness_factor, 1.0, 0.0, -0.8, 0.4);
909
910                (
911                    floodedness_noise - partially_threshold,
912                    floodedness_noise - fully_threshold,
913                )
914            };
915
916        if fully_flooded > 0.0 {
917            gf.fluid_level
918        } else if partially_flooded > 0.0 {
919            self.compute_randomized_fluid_surface_level(x, y, z, noises, lowest_surface)
920        } else {
921            WAY_BELOW_MIN_Y
922        }
923    }
924
925    fn compute_randomized_fluid_surface_level(
926        &mut self,
927        x: i32,
928        y: i32,
929        z: i32,
930        noises: &N,
931        lowest_surface: i32,
932    ) -> i32 {
933        let cell_x = x.div_euclid(16);
934        let cell_y = y.div_euclid(40);
935        let cell_z = z.div_euclid(16);
936        let cell_middle_y = cell_y * 40 + 20;
937
938        // fluid_level_spread is evaluated at grid coordinates (not block coordinates)
939        self.cache.ensure(cell_x, cell_z, noises);
940        let spread =
941            noises.router_fluid_level_spread(&mut self.cache, cell_x, cell_y, cell_z) * 10.0;
942        let spread_quantized = quantize(spread, 3);
943        let target = cell_middle_y + spread_quantized;
944
945        lowest_surface.min(target)
946    }
947
948    fn compute_fluid_type(
949        &mut self,
950        x: i32,
951        y: i32,
952        z: i32,
953        noises: &N,
954        gf: FluidStatus,
955        fluid_level: i32,
956    ) -> BlockStateId {
957        if fluid_level <= -10 && fluid_level != WAY_BELOW_MIN_Y && gf.fluid_type != self.lava_id {
958            let cell_x = x.div_euclid(64);
959            let cell_y = y.div_euclid(40);
960            let cell_z = z.div_euclid(64);
961            self.cache.ensure(cell_x, cell_z, noises);
962            let lava_noise = noises.router_lava(&mut self.cache, cell_x, cell_y, cell_z);
963            if lava_noise.abs() > 0.3 {
964                return self.lava_id;
965            }
966        }
967        gf.fluid_type
968    }
969
970    /// Calculate barrier pressure between two aquifer cells.
971    ///
972    /// Matches vanilla's check: if lava meets water at this Y, return max pressure.
973    #[expect(
974        clippy::too_many_arguments,
975        reason = "matches vanilla NoiseBasedAquifer.calculatePressure signature"
976    )]
977    fn calculate_pressure(
978        &mut self,
979        noises: &N,
980        x: i32,
981        y: i32,
982        z: i32,
983        barrier_noise: &mut f64,
984        s1: FluidStatus,
985        s2: FluidStatus,
986    ) -> f64 {
987        let f1 = s1.at(y);
988        let f2 = s2.at(y);
989        let f1_is_lava = f1 == Some(self.lava_id);
990        let f2_is_lava = f2 == Some(self.lava_id);
991        let f1_is_water = f1 == Some(self.water_id);
992        let f2_is_water = f2 == Some(self.water_id);
993
994        // Lava–water interface → max pressure
995        if (f1_is_lava && f2_is_water) || (f1_is_water && f2_is_lava) {
996            return 2.0;
997        }
998
999        let fluid_y_diff = (s1.fluid_level - s2.fluid_level).abs();
1000        if fluid_y_diff == 0 {
1001            return 0.0;
1002        }
1003
1004        let avg_fluid_y = 0.5 * f64::from(s1.fluid_level + s2.fluid_level);
1005        let above_avg = f64::from(y) + 0.5 - avg_fluid_y;
1006        let base = f64::from(fluid_y_diff) / 2.0;
1007        let edge_dist = base - above_avg.abs();
1008
1009        let gradient = if above_avg > 0.0 {
1010            if edge_dist > 0.0 {
1011                edge_dist / 1.5
1012            } else {
1013                edge_dist / 2.5
1014            }
1015        } else {
1016            let center = 3.0 + edge_dist;
1017            if center > 0.0 {
1018                center / 3.0
1019            } else {
1020                center / 10.0
1021            }
1022        };
1023
1024        let noise_val = if !(-2.0..=2.0).contains(&gradient) {
1025            0.0
1026        } else if barrier_noise.is_nan() {
1027            self.cache.ensure(x, z, noises);
1028            let n = noises.router_barrier(&mut self.cache, x, y, z);
1029            *barrier_noise = n;
1030            n
1031        } else {
1032            *barrier_noise
1033        };
1034
1035        2.0 * (noise_val + gradient)
1036    }
1037}
1038
1039/// Quantize: snap value down to the nearest multiple of `quantum`.
1040#[inline]
1041fn quantize(value: f64, quantum: i32) -> i32 {
1042    let q = f64::from(quantum);
1043    (value / q).floor() as i32 * quantum
1044}
1045
1046/// Evaluate preliminary surface level at quart-quantized coordinates.
1047///
1048/// Vanilla's `NoiseChunk.preliminarySurfaceLevel()` quantizes X/Z to quart
1049/// positions before lookup, matching `FlatCache`'s 4-block grid.
1050pub fn preliminary_surface_level<N: DimensionNoises>(
1051    noises: &N,
1052    cache: &mut N::ColumnCache,
1053    x: i32,
1054    z: i32,
1055) -> i32 {
1056    // Quantize to quart positions: (x >> 2) << 2
1057    let qx = (x >> 2) << 2;
1058    let qz = (z >> 2) << 2;
1059    cache.ensure(qx, qz, noises);
1060    // Vanilla uses Mth.floor(), not truncation
1061    noises
1062        .router_preliminary_surface_level(cache, qx, 0, qz)
1063        .floor() as i32
1064}
1065
1066/// [`preliminary_surface_level`] with a per-quart-column result cache (vanilla's
1067/// `Long2IntMap`). On a hit it returns the memoized `i32` and skips the
1068/// expensive flat-router recompute in `cache.ensure`. Bit-identical: the cached
1069/// value is the same deterministic function of the quart column.
1070fn cached_preliminary_surface_level<N: DimensionNoises>(
1071    noises: &N,
1072    cache: &mut N::ColumnCache,
1073    prelim_cache: &mut FxHashMap<(i32, i32), i32>,
1074    x: i32,
1075    z: i32,
1076) -> i32 {
1077    let key = ((x >> 2) << 2, (z >> 2) << 2);
1078    if let Some(&level) = prelim_cache.get(&key) {
1079        return level;
1080    }
1081    let level = preliminary_surface_level(noises, cache, x, z);
1082    prelim_cache.insert(key, level);
1083    level
1084}