Skip to main content

steel_core/worldgen/generator/
surface_biomes.rs

1//! fuzzed biome lookup for the surface stage
2
3use glam::IVec3;
4use rustc_hash::FxHashMap;
5use steel_registry::REGISTRY;
6use steel_registry::biome::TemperatureModifier;
7use steel_worldgen::density::{DimensionNoises, NoiseSettings};
8use steel_worldgen::surface::SurfaceBiomeProvider;
9use steel_worldgen::surface_partial::PartialSurfaceOutcome;
10
11use super::vanilla::{get_fiddle, lcg_next};
12
13/// partition of biome ids into classes
14pub(super) struct SurfaceBiomeClasses {
15    class_of: Box<[u16]>,
16    below_preliminary: Box<[PartialSurfaceOutcome]>,
17    min_y: i32,
18    height: i32,
19}
20
21impl SurfaceBiomeClasses {
22    /// group by rule membership and temperature none if unused or too many sets
23    pub(super) fn new<N: DimensionNoises>() -> Option<Self> {
24        let sets = N::surface_rule_biome_sets();
25        if !N::surface_rule_uses_biome() || sets.len() > u64::BITS as usize {
26            return None;
27        }
28
29        let mut classes = FxHashMap::<(u64, u32, bool), u16>::default();
30        let mut class_masks = Vec::new();
31        let class_of = REGISTRY
32            .biomes
33            .iter()
34            .map(|(id, biome)| {
35                let mask = sets
36                    .iter()
37                    .enumerate()
38                    .filter(|(_, set)| set.contains(&(id as u16)))
39                    .fold(0u64, |mask, (bit, _)| mask | 1 << bit);
40
41                let key = (
42                    mask,
43                    biome.temperature.to_bits(),
44                    matches!(biome.temperature_modifier, TemperatureModifier::Frozen),
45                );
46
47                let next = classes.len() as u16;
48                *classes.entry(key).or_insert_with(|| {
49                    class_masks.push(mask);
50                    next
51                })
52            })
53            .collect();
54
55        let min_y = N::Settings::MIN_Y;
56        let height = N::Settings::HEIGHT;
57        let below_preliminary = if N::surface_rule_uses_preliminary_surface() {
58            let rule = N::surface_rule_below_preliminary_surface();
59            class_masks
60                .iter()
61                .flat_map(|&mask| (min_y..min_y + height).map(move |y| rule.resolve(y, mask)))
62                .collect()
63        } else {
64            Box::default()
65        };
66
67        Some(Self {
68            class_of,
69            below_preliminary,
70            min_y,
71            height,
72        })
73    }
74
75    fn class(&self, biome_id: u16) -> Option<u16> {
76        self.class_of.get(usize::from(biome_id)).copied()
77    }
78
79    fn below_preliminary_outcome(&self, class: u16, block_y: i32) -> PartialSurfaceOutcome {
80        let index = usize::from(class) * self.height as usize + (block_y - self.min_y) as usize;
81        self.below_preliminary
82            .get(index)
83            .copied()
84            .unwrap_or(PartialSurfaceOutcome::Evaluate)
85    }
86}
87
88/// Quart biome reads for one chunk and the one-quart ring around it.
89pub(super) struct SurfaceQuartBiomes<'a> {
90    biome_data: &'a [u16],
91    chunk_quart_x: i32,
92    chunk_quart_z: i32,
93    min_qy: i32,
94    total_quarts_y: i32,
95    neighbor_biomes: &'a dyn Fn(IVec3) -> u16,
96    ring: Box<[u16]>,
97}
98
99const QUARTS_PER_SECTION_SIDE: i32 = 4;
100const QUARTS_PER_SECTION_PLANE: i32 = QUARTS_PER_SECTION_SIDE * QUARTS_PER_SECTION_SIDE;
101const QUARTS_PER_SECTION: i32 = QUARTS_PER_SECTION_PLANE * QUARTS_PER_SECTION_SIDE;
102const RING_SIDE: i32 = QUARTS_PER_SECTION_SIDE + 2;
103const UNREAD: u16 = u16::MAX;
104
105impl<'a> SurfaceQuartBiomes<'a> {
106    pub(super) fn new(
107        biome_data: &'a [u16],
108        section_count: usize,
109        min_y: i32,
110        chunk_quart_x: i32,
111        chunk_quart_z: i32,
112        neighbor_biomes: &'a dyn Fn(IVec3) -> u16,
113    ) -> Self {
114        let total_quarts_y = (section_count * QUARTS_PER_SECTION_SIDE as usize) as i32;
115
116        Self {
117            biome_data,
118            chunk_quart_x,
119            chunk_quart_z,
120            min_qy: min_y >> 2,
121            total_quarts_y,
122            neighbor_biomes,
123            ring: vec![UNREAD; (RING_SIDE * RING_SIDE * total_quarts_y) as usize].into(),
124        }
125    }
126
127    #[expect(
128        clippy::similar_names,
129        reason = "quart coordinate names mirror the x/y/z axes"
130    )]
131    fn biome_at(&mut self, quart: IVec3) -> u16 {
132        let qy = (quart.y - self.min_qy).clamp(0, self.total_quarts_y - 1);
133        let local_qx = quart.x - self.chunk_quart_x;
134        let local_qz = quart.z - self.chunk_quart_z;
135
136        if (0..QUARTS_PER_SECTION_SIDE).contains(&local_qx)
137            && (0..QUARTS_PER_SECTION_SIDE).contains(&local_qz)
138        {
139            let (section_idx, local_qy) =
140                (qy / QUARTS_PER_SECTION_SIDE, qy % QUARTS_PER_SECTION_SIDE);
141
142            return self.biome_data[(section_idx * QUARTS_PER_SECTION
143                + local_qy * QUARTS_PER_SECTION_PLANE
144                + local_qz * QUARTS_PER_SECTION_SIDE
145                + local_qx) as usize];
146        }
147
148        let (rx, rz) = (local_qx + 1, local_qz + 1);
149        if !(0..RING_SIDE).contains(&rx) || !(0..RING_SIDE).contains(&rz) {
150            return (self.neighbor_biomes)(quart);
151        }
152
153        let slot = &mut self.ring[((qy * RING_SIDE + rz) * RING_SIDE + rx) as usize];
154        if *slot == UNREAD {
155            *slot = (self.neighbor_biomes)(quart);
156        }
157
158        *slot
159    }
160}
161
162#[derive(Clone, Copy, Default)]
163struct BiomeCandidate {
164    y_fiddle: f64,
165    x_distance_sq: f64,
166    z_distance_sq: f64,
167}
168
169impl BiomeCandidate {
170    fn squared_distance(self, dy: f64) -> f64 {
171        self.z_distance_sq + (dy + self.y_fiddle) * (dy + self.y_fiddle) + self.x_distance_sq
172    }
173}
174
175/// cache fuzzed biome lookups for one column
176pub(super) struct FuzzedBiomeColumn<'a, 'b> {
177    quarts: &'a mut SurfaceQuartBiomes<'b>,
178    classes: Option<&'a SurfaceBiomeClasses>,
179    biome_zoom_seed: i64,
180    parent_x: i32,
181    parent_z: i32,
182    fract_x: f64,
183    fract_z: f64,
184    cached_parent_y: i32,
185    candidates: [BiomeCandidate; 8],
186    rval_after_cx: [i64; 2],
187    class_parent_y: i32,
188    uniform_biome: Option<u16>,
189}
190
191impl<'a, 'b> FuzzedBiomeColumn<'a, 'b> {
192    pub(super) fn new(
193        quarts: &'a mut SurfaceQuartBiomes<'b>,
194        classes: Option<&'a SurfaceBiomeClasses>,
195        biome_zoom_seed: i64,
196        block_x: i32,
197        block_z: i32,
198    ) -> Self {
199        let abs_x = block_x - 2;
200        let abs_z = block_z - 2;
201        let parent_x = abs_x >> 2;
202        let parent_z = abs_z >> 2;
203        Self {
204            quarts,
205            classes,
206            biome_zoom_seed,
207            parent_x,
208            parent_z,
209            fract_x: f64::from(abs_x & 3) / 4.0,
210            fract_z: f64::from(abs_z & 3) / 4.0,
211            cached_parent_y: i32::MIN,
212            candidates: [BiomeCandidate::default(); 8],
213            rval_after_cx: [
214                lcg_next(biome_zoom_seed, i64::from(parent_x)),
215                lcg_next(biome_zoom_seed, i64::from(parent_x + 1)),
216            ],
217            class_parent_y: i32::MIN,
218            uniform_biome: None,
219        }
220    }
221
222    /// fill the low or high y slots for this candidate group
223    #[inline]
224    fn compute_cy_group(&mut self, cy: i32, high: bool) {
225        let base_idx = if high { 2 } else { 0 };
226        for cx_idx in 0..2usize {
227            let cx = self.parent_x + cx_idx as i32;
228            let dx = if cx_idx == 0 {
229                self.fract_x
230            } else {
231                self.fract_x - 1.0
232            };
233
234            let rval_cy = lcg_next(self.rval_after_cx[cx_idx], i64::from(cy));
235            for cz_off in 0..2usize {
236                let cz = self.parent_z + cz_off as i32;
237                let dz = if cz_off == 0 {
238                    self.fract_z
239                } else {
240                    self.fract_z - 1.0
241                };
242
243                let mut rval = lcg_next(rval_cy, i64::from(cz));
244                rval = lcg_next(rval, i64::from(cx));
245                rval = lcg_next(rval, i64::from(cy));
246                rval = lcg_next(rval, i64::from(cz));
247                let fx = get_fiddle(rval);
248                rval = lcg_next(rval, self.biome_zoom_seed);
249                let fy = get_fiddle(rval);
250                rval = lcg_next(rval, self.biome_zoom_seed);
251                let fz = get_fiddle(rval);
252
253                self.candidates[cx_idx * 4 + base_idx + cz_off] = BiomeCandidate {
254                    y_fiddle: fy,
255                    x_distance_sq: (dx + fx) * (dx + fx),
256                    z_distance_sq: (dz + fz) * (dz + fz),
257                };
258            }
259        }
260    }
261
262    /// refresh candidates for this parent y
263    fn recompute_candidates(&mut self, parent_y: i32) {
264        if self.cached_parent_y != i32::MIN && parent_y == self.cached_parent_y - 1 {
265            // moving down: old low y becomes new high y
266            self.candidates[2] = self.candidates[0];
267            self.candidates[3] = self.candidates[1];
268            self.candidates[6] = self.candidates[4];
269            self.candidates[7] = self.candidates[5];
270            self.compute_cy_group(parent_y, false);
271        } else {
272            self.compute_cy_group(parent_y, false);
273            self.compute_cy_group(parent_y + 1, true);
274        }
275
276        self.cached_parent_y = parent_y;
277    }
278
279    /// exact fuzzed biome at this y
280    #[inline]
281    pub(super) fn get(&mut self, block_y: i32) -> u16 {
282        let abs_y = block_y - 2;
283        let parent_y = abs_y >> 2;
284        let fract_y = f64::from(abs_y & 3) / 4.0;
285
286        if parent_y != self.cached_parent_y {
287            self.recompute_candidates(parent_y);
288        }
289
290        let mut min_i = 0usize;
291        let mut min_dist = f64::INFINITY;
292        for (i, candidate) in self.candidates.iter().enumerate() {
293            let dy = if (i & 2) == 0 { fract_y } else { fract_y - 1.0 };
294            let dist = candidate.squared_distance(dy);
295            if min_dist > dist {
296                min_i = i;
297                min_dist = dist;
298            }
299        }
300
301        self.quarts.biome_at(IVec3::new(
302            self.parent_x + i32::from(min_i & 4 != 0),
303            parent_y + i32::from(min_i & 2 != 0),
304            self.parent_z + i32::from(min_i & 1 != 0),
305        ))
306    }
307
308    /// A biome the surface rule cannot distinguish from [`Self::get`]'s.
309    pub(super) fn get_for_surface_rule(&mut self, block_y: i32) -> u16 {
310        match self.uniform_group_biome(block_y) {
311            Some(biome) => biome,
312            None => self.get(block_y),
313        }
314    }
315
316    /// resolve below the preliminary surface using y and a shared biome class
317    pub(super) fn below_preliminary_outcome(&mut self, block_y: i32) -> PartialSurfaceOutcome {
318        let Some(classes) = self.classes else {
319            return PartialSurfaceOutcome::Evaluate;
320        };
321        self.uniform_group_biome(block_y)
322            .and_then(|biome| classes.class(biome))
323            .map_or(PartialSurfaceOutcome::Evaluate, |class| {
324                classes.below_preliminary_outcome(class, block_y)
325            })
326    }
327
328    /// The group's shared-class candidate biome, if its 8 candidates agree.
329    fn uniform_group_biome(&mut self, block_y: i32) -> Option<u16> {
330        let classes = self.classes?;
331        let parent_y = (block_y - 2) >> 2;
332        if parent_y != self.class_parent_y {
333            self.class_parent_y = parent_y;
334            self.uniform_biome = self.uniform_candidate_biome(classes, parent_y);
335        }
336
337        self.uniform_biome
338    }
339
340    fn uniform_candidate_biome(
341        &mut self,
342        classes: &SurfaceBiomeClasses,
343        parent_y: i32,
344    ) -> Option<u16> {
345        let first = self
346            .quarts
347            .biome_at(IVec3::new(self.parent_x, parent_y, self.parent_z));
348        let class = classes.class(first)?;
349
350        for i in 1..self.candidates.len() {
351            let biome = self.quarts.biome_at(IVec3::new(
352                self.parent_x + i32::from(i & 4 != 0),
353                parent_y + i32::from(i & 2 != 0),
354                self.parent_z + i32::from(i & 1 != 0),
355            ));
356            if classes.class(biome) != Some(class) {
357                return None;
358            }
359        }
360
361        Some(first)
362    }
363}
364
365impl SurfaceBiomeProvider for FuzzedBiomeColumn<'_, '_> {
366    #[inline]
367    fn biome_id(&mut self, block_y: i32) -> u16 {
368        self.get_for_surface_rule(block_y)
369    }
370}