steel_core/worldgen/generator/
surface_biomes.rs1use 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
13pub(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 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
88pub(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
175pub(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 #[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 fn recompute_candidates(&mut self, parent_y: i32) {
264 if self.cached_parent_y != i32::MIN && parent_y == self.cached_parent_y - 1 {
265 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 #[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 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 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 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}