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;
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
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    preliminary_surface: &'a Arc<PreliminarySurfaceStore>,
29    inner: Option<Aquifer<N>>,
30}
31
32impl<'a, N: DimensionNoises> LazyAquifer<'a, N> {
33    /// Deferred aquifer for the given chunk.
34    #[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    /// Build on first call; `height_cache` is cloned into the aquifer's own cache.
53    ///
54    /// # Panics
55    /// Never — `inner` is initialized above if it was `None`.
56    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
74// Grid spacing
75const Y_SPACING: i32 = 12;
76
77// Jitter range per cell center
78const X_RANGE: i32 = 10;
79const Y_RANGE: i32 = 9;
80const Z_RANGE: i32 = 10;
81
82// Anchor offsets for neighborhood lookup
83const SAMPLE_OFFSET_X: i32 = -5;
84const SAMPLE_OFFSET_Y: i32 = 1;
85const SAMPLE_OFFSET_Z: i32 = -5;
86
87const LAVA_LEVEL: i32 = -54;
88/// Sentinel for "no fluid" — well below any real Y coordinate.
89const WAY_BELOW_MIN_Y: i32 = -32512;
90const FLOWING_UPDATE_SIMILARITY: f64 = 1.0 - ((12 * 12 - 10 * 10) as f64) / 25.0;
91
92/// Chunk offsets (in chunks, ×16 for blocks) used when sampling
93/// preliminary surface levels around an aquifer cell center.
94const 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/// Fluid status at an aquifer cell center.
111///
112/// Matches vanilla's `Aquifer.FluidStatus` — stores the actual fluid block state
113/// rather than a boolean flag, so the aquifer is agnostic to which fluids exist.
114#[derive(Clone, Copy, PartialEq, Eq)]
115struct FluidStatus {
116    /// Y level of the fluid surface (exclusive upper bound).
117    fluid_level: i32,
118    /// Block state placed below `fluid_level`.
119    fluid_type: BlockStateId,
120}
121
122impl FluidStatus {
123    /// What block is at `block_y`? Returns the fluid type if below the surface,
124    /// or `None` for air above the surface.
125    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
134/// Result of the aquifer substance check.
135pub enum AquiferResult {
136    /// Solid block (density > 0 or barrier makes it solid).
137    Solid,
138    /// Air (no block placed).
139    Air,
140    /// Fluid block to place.
141    Fluid(BlockStateId),
142}
143
144/// Column-scan state for the 12 aquifer-neighborhood cells, stored `SoA` so the
145/// per-Y distance computation can be SIMD-batched as 3× `i32x4`.
146///
147/// `compute_substance` is called many times with the same `(world_x, world_z)`
148/// and decreasing `world_y` (innermost loop in `noise_chunk::fill`). Within a
149/// stable `y_anchor` window (`Y_SPACING` blocks tall) the 12 cells of the
150/// neighborhood don't change, and `dx*dx + dz*dz` only depends on x/z — only
151/// `dy = loc_y - world_y` varies per call.
152///
153/// `y_anchor = i32::MIN` marks the cache as invalid (forces a refill).
154struct AquiferColumnCache {
155    world_x: i32,
156    world_z: i32,
157    y_anchor: i32,
158    /// Per-cell unpacked Y of the aquifer-cell center (constant while cached).
159    /// Padded to 16 entries so the 12 valid cells fit cleanly into 3× i32x4
160    /// SIMD batches; trailing slots stay at default `0`.
161    cell_loc_y: [i32; 16],
162    /// Per-cell `(dx + fx)² + (dz + fz)²` (Y-independent component of distance).
163    cell_xz_dist_sq: [i32; 16],
164    /// Per-cell index into `location_cache` / `status_cache`.
165    cell_idx: [u32; 12],
166    /// The shared status when all 12 cells have the same one.
167    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
184/// Noise-based aquifer for a single chunk.
185///
186/// Constructed once per chunk, used throughout the fill loop.
187pub struct Aquifer<N: DimensionNoises> {
188    /// Packed (x, y, z) locations of aquifer cell centers.
189    /// `i64::MAX` = not yet computed.
190    location_cache: Vec<i64>,
191    /// Lazily computed fluid statuses per grid cell.
192    status_cache: Vec<Option<FluidStatus>>,
193    /// Positional random for grid cell jitter.
194    splitter: RandomSplitter,
195    /// Column cache owned by the aquifer for density function evaluation.
196    cache: N::ColumnCache,
197    /// Grid bounds.
198    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 aquifer sampling above this Y (optimization).
204    skip_sampling_above_y: i32,
205    /// Sea level for this dimension.
206    sea_level: i32,
207    /// Precomputed `min(LAVA_LEVEL, sea_level)`. Hoisted out of `global_fluid`
208    /// (which is on the per-block hot path) so we don't recompute it millions
209    /// of times per chunk.
210    lava_floor: i32,
211    /// Block state IDs.
212    water_id: BlockStateId,
213    lava_id: BlockStateId,
214    /// The dimension's default fluid (water for overworld, lava for nether).
215    default_fluid_id: BlockStateId,
216    /// Vanilla's `shouldScheduleFluidUpdate` flag from the most recent substance lookup.
217    should_schedule_fluid_update: bool,
218    /// 12-cell neighborhood snapshot for the current Y-column.
219    /// Placed at the end so dimensions with disabled aquifers (nether/end)
220    /// keep the hot fluid-id fields earlier in the struct's cache lines.
221    col_cache: AquiferColumnCache,
222    /// Generator-wide memo of `preliminary_surface_level`, replacing vanilla's
223    /// per-aquifer `Long2IntMap`: `compute_fluid` samples 13 columns per cell
224    /// and the constructor scans ~121, mostly shared with neighboring chunks.
225    preliminary_surface: Arc<PreliminarySurfaceStore>,
226}
227
228// Grid coordinate conversions
229#[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
254// BlockPos packing (matches vanilla's BlockPos.asLong / getX / getY / getZ)
255const PACKED_X_MASK: i64 = 0x3FF_FFFF; // 26 bits
256const PACKED_Y_MASK: i64 = 0xFFF; // 12 bits
257const PACKED_Z_MASK: i64 = 0x3FF_FFFF; // 26 bits
258const 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/// Similarity between two squared distances. Positive when the two nearest
284/// aquifer cells are close together (near a boundary).
285#[inline]
286fn similarity(dist_sq1: i32, dist_sq2: i32) -> f64 {
287    1.0 - f64::from(dist_sq2 - dist_sq1) / 25.0
288}
289
290/// Deep dark region check matching `OverworldBiomeBuilder.isDeepDarkRegion`.
291fn 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
304/// Global fluid picker matching vanilla's `NoiseBasedChunkGenerator.createFluidPicker`.
305///
306/// Below `min(-54, sea_level)` → lava at Y=-54. Otherwise → the dimension's
307/// default fluid at sea level (water for overworld, lava for nether).
308const 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    /// Create an aquifer for a full 16×16 chunk.
330    ///
331    /// `chunk_min_x/z` are the block coordinates of the chunk's NW corner.
332    /// `min_block_y` and `y_block_size` define the vertical range.
333    /// `splitter` is the seed's positional splitter.
334    /// `cache` should be a pre-initialized column cache for this chunk
335    /// (avoids a redundant `init_grid` call).
336    #[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    /// Create an aquifer with custom XZ extent (in blocks).
366    /// Vanilla's iterateNoiseColumn uses width=cellWidth (4) for single-column queries.
367    #[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        // When aquifers are disabled (nether/end), compute_substance uses only
394        // the global fluid picker — skip grid allocation and surface sampling.
395        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        // Compute skip_sampling_above_y from max preliminary surface level.
438        // The scan primes the store for the columns `compute_fluid` reuses.
439        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        // Sample at 4-block intervals (quart-block resolution) across the chunk area
485        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    /// shared status if every cached neighbor agrees
509    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    /// Whether `fluid_at` is water directly above the global lava sea.
522    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    /// refill in x/y/z order to keep nearest-cell ties unchanged
537    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    /// Compute what block to place at this position given the interpolated density.
582    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        // Solid block — let the caller decide (stone or ore)
591        if density > 0.0 {
592            self.should_schedule_fluid_update = false;
593            return AquiferResult::Solid;
594        }
595
596        // Disabled aquifers (nether/end): use global fluid picker directly,
597        // matching vanilla's `Aquifer.createDisabled`.
598        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        // Above the skip threshold: use global fluid directly
622        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 global fluid is lava here, return lava
631        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        // Find 4 nearest aquifer cell centers from the 2×3×2 neighborhood.
637        // Within a Y-column scan, the 12 cells and their `xz_dist_sq` values
638        // are constant for a given `y_anchor`; only `dy` varies. The column
639        // cache amortizes location lookup, splitter calls, and i64 unpacking.
640        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        // equal statuses mean zero pressure and no fluid update
650        // water over global lava still needs the full path
651        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    /// nearest cells and barriers; the column cache must be current
666    #[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        // SIMD-batch the per-cell distance computation: `loc_y - world_y` then
679        // `xz_dist_sq + dy²`, processing 4 cells per `i32x4` op. The 12 valid
680        // cells fit in 3 batches; the 4-slot tail of `cell_loc_y` /
681        // `cell_xz_dist_sq` is harmless padding (we ignore the trailing slot).
682        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            // Insert into sorted top-4
700            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        // `similarity(d1, d2) = 1 - (d2 - d1) / 25`, so `sim12 <= 0.0` is exactly
731        // `d2 - d1 >= 25` in i32. Defer the f64 conversion + divide until after
732        // the early-return check. Fluid-update scheduling still matches vanilla:
733        // `sim12 >= FLOWING_UPDATE_SIMILARITY` is exactly `d2 - d1 <= 44`.
734        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        // Water adjacent to global lava below → return water
750        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        // Compute barrier pressure between closest pairs
758        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        // Return the closest fluid
826        match fluid_at {
827            Some(id) => AquiferResult::Fluid(id),
828            None => AquiferResult::Air,
829        }
830    }
831
832    /// first y where every non-positive density gives air without sampling
833    /// safe to skip: only the fluid-update flag changes, and each call resets it
834    #[must_use]
835    pub fn unsampled_air_min_y(&self) -> i32 {
836        // The global fluid picker places fluid only below its level: lava below
837        // `LAVA_LEVEL` (under `lava_floor <= sea_level`), otherwise the default
838        // fluid below `sea_level`.
839        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    /// Returns whether the most recent substance lookup needs postprocessing for placed fluids.
847    #[must_use]
848    pub const fn should_schedule_fluid_update(&self) -> bool {
849        self.should_schedule_fluid_update
850    }
851
852    /// Returns the quart-quantized preliminary surface level, reusing this aquifer's
853    /// density-column cache and the generator-wide result store.
854    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    /// Get or compute the fluid status for the aquifer cell at the given cache index.
860    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    /// Compute the fluid status for an aquifer cell centered at (x, y, z).
875    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; // sectionToBlockCoord
890            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        // fluid_level_spread is evaluated at grid coordinates (not block coordinates)
1003        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    /// Calculate barrier pressure between two aquifer cells.
1040    ///
1041    /// Matches vanilla's check: if lava meets water at this Y, return max pressure.
1042    #[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        // Lava–water interface → max pressure
1064        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/// Quantize: snap value down to the nearest multiple of `quantum`.
1111#[inline]
1112fn quantize(value: f64, quantum: i32) -> i32 {
1113    let q = f64::from(quantum);
1114    (value / q).floor() as i32 * quantum
1115}
1116
1117/// Evaluate preliminary surface level at quart-quantized coordinates.
1118///
1119/// Vanilla's `NoiseChunk.preliminarySurfaceLevel()` quantizes X/Z to quart
1120/// positions before lookup, matching `FlatCache`'s 4-block grid.
1121pub fn preliminary_surface_level<N: DimensionNoises>(
1122    noises: &N,
1123    cache: &mut N::ColumnCache,
1124    x: i32,
1125    z: i32,
1126) -> i32 {
1127    // Quantize to quart positions: (x >> 2) << 2
1128    let qx = (x >> 2) << 2;
1129    let qz = (z >> 2) << 2;
1130    cache.ensure(qx, qz, noises);
1131    // Vanilla uses Mth.floor(), not truncation
1132    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    /// Wherever the uniform-status shortcut answers, the full nearest-cell and
1158    /// barrier path must give the same block and fluid-update flag.
1159    #[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}