steel_core/worldgen/feature/
runner.rs1use glam::IVec3;
2use smallvec::SmallVec;
3use steel_registry::biome::BiomeRef;
4use steel_registry::structure::StructureRef;
5use steel_utils::{BoundingBox, ChunkPos};
6
7use super::prelude::*;
8use super::sorter::{FeatureSorter, FeatureStepData};
9use crate::worldgen::structure::piece_placer::StructurePiecePlacer;
10#[cfg(test)]
11use steel_worldgen::structure::StructureReferenceMap;
12use steel_worldgen::structure::StructureStart;
13
14#[derive(Debug)]
16pub(crate) struct FeatureDecorationRunner {
17 pub(super) sorter: FeatureSorter,
18 source_biome_lookup: Box<[bool]>,
19}
20
21impl FeatureDecorationRunner {
22 pub(super) const VANILLA_DIRECTION_VALUES: [Direction; 6] = [
23 Direction::Down,
24 Direction::Up,
25 Direction::North,
26 Direction::South,
27 Direction::West,
28 Direction::East,
29 ];
30
31 pub(super) const VANILLA_HORIZONTAL_DIRECTIONS: [Direction; 4] = [
32 Direction::North,
33 Direction::East,
34 Direction::South,
35 Direction::West,
36 ];
37
38 pub(super) fn random_horizontal_direction(random: &mut WorldgenRandom) -> Direction {
39 Self::VANILLA_HORIZONTAL_DIRECTIONS[random.next_i32_bounded(4) as usize]
40 }
41
42 pub(super) fn shuffled_directions<const N: usize>(
43 random: &mut WorldgenRandom,
44 mut directions: [Direction; N],
45 ) -> [Direction; N] {
46 for i in (1..N).rev() {
47 let Ok(bound) = i32::try_from(i + 1) else {
48 panic!("direction shuffle length {N} exceeds i32 range");
49 };
50 let j = random.next_i32_bounded(bound) as usize;
51 directions.swap(i, j);
52 }
53 directions
54 }
55
56 pub(super) const fn manhattan_distance(left: BlockPos, right: BlockPos) -> i32 {
57 Self::abs_diff(left.x(), right.x())
58 + Self::abs_diff(left.y(), right.y())
59 + Self::abs_diff(left.z(), right.z())
60 }
61
62 pub(super) fn for_each_vanilla_within_manhattan(
63 origin: BlockPos,
64 reach_x: i32,
65 reach_y: i32,
66 reach_z: i32,
67 mut visitor: impl FnMut(BlockPos) -> bool,
68 ) {
69 let max_depth = reach_x + reach_y + reach_z;
70 for current_depth in 0..=max_depth {
71 let max_x = reach_x.min(current_depth);
72 for x in -max_x..=max_x {
73 let max_y = reach_y.min(current_depth - x.abs());
74 for y in -max_y..=max_y {
75 let z = current_depth - x.abs() - y.abs();
76 if z > reach_z {
77 continue;
78 }
79
80 if !visitor(origin.offset(x, y, z)) {
81 return;
82 }
83
84 if z != 0 && !visitor(origin.offset(x, y, -z)) {
85 return;
86 }
87 }
88 }
89 }
90 }
91
92 pub(super) fn for_each_vanilla_between_closed(
93 min: BlockPos,
94 max: BlockPos,
95 mut visitor: impl FnMut(BlockPos),
96 ) {
97 let width = max.x() - min.x() + 1;
98 let height = max.y() - min.y() + 1;
99 let depth = max.z() - min.z() + 1;
100 debug_assert!(width > 0 && height > 0 && depth > 0);
101
102 let end = i64::from(width) * i64::from(height) * i64::from(depth);
103 for index in 0..end {
104 let x = (index % i64::from(width)) as i32;
105 let slice = index / i64::from(width);
106 let y = (slice % i64::from(height)) as i32;
107 let z = (slice / i64::from(height)) as i32;
108 visitor(BlockPos::new(min.x() + x, min.y() + y, min.z() + z));
109 }
110 }
111
112 const fn abs_diff(left: i32, right: i32) -> i32 {
113 if left >= right {
114 left - right
115 } else {
116 right - left
117 }
118 }
119
120 #[must_use]
121 pub(crate) fn new(possible_biomes: &[BiomeRef], registry: &Registry) -> Self {
122 let mut source_biome_ids = FxHashSet::default();
123 let mut unique_biomes = Vec::new();
124 let mut max_biome_id = 0;
125
126 for &biome in possible_biomes {
127 let Some(biome_id) = biome.try_id() else {
128 panic!("possible biome {} is not registered", biome.key);
129 };
130 max_biome_id = max_biome_id.max(biome_id);
131
132 if source_biome_ids.insert(biome_id) {
133 unique_biomes.push(biome);
134 }
135 }
136
137 let mut source_biome_lookup = vec![false; max_biome_id + 1].into_boxed_slice();
138 for biome_id in source_biome_ids {
139 source_biome_lookup[biome_id] = true;
140 }
141
142 Self {
143 sorter: FeatureSorter::build(&unique_biomes, registry),
144 source_biome_lookup,
145 }
146 }
147
148 pub(crate) fn decorate(
149 &self,
150 region: &mut WorldGenRegion<'_>,
151 registry: &Registry,
152 seed: i64,
153 biome_zoom_seed: i64,
154 ) {
155 let center = region.center();
156 let origin = BlockPos::new(center.0.x * 16, region.min_y(), center.0.y * 16);
157 let possible_biomes = self.collect_possible_biome_ids(region);
158
159 let mut random = WorldgenRandom::from_seed(0);
160 let decoration_seed = random.set_decoration_seed(seed, origin.x(), origin.z());
161 let step_count = DECORATION_STEP_COUNT.max(self.sorter.step_count());
162
163 for step in 0..step_count {
164 Self::place_structures_for_step(
165 region,
166 registry,
167 decoration_seed,
168 &mut random,
169 step,
170 biome_zoom_seed,
171 );
172
173 let Some(step_features) = self.sorter.step(step) else {
174 continue;
175 };
176 Self::place_features_for_step(
177 region,
178 registry,
179 decoration_seed,
180 &mut random,
181 origin,
182 step,
183 step_features,
184 &possible_biomes,
185 biome_zoom_seed,
186 );
187 }
188 }
189
190 pub(super) fn collect_possible_biome_ids(&self, region: &WorldGenRegion<'_>) -> Vec<usize> {
191 let center = region.center();
192 let mut seen = vec![false; self.source_biome_lookup.len()];
193 let mut biomes = Vec::new();
194
195 for chunk_z in center.0.y - 1..=center.0.y + 1 {
196 for chunk_x in center.0.x - 1..=center.0.x + 1 {
197 let chunk = region.chunk(chunk_x, chunk_z, ChunkStatus::Biomes);
198 chunk.sections().for_each_biome_id(|biome_id| {
199 let biome_id = usize::from(biome_id);
200 if self
201 .source_biome_lookup
202 .get(biome_id)
203 .copied()
204 .unwrap_or(false)
205 && !seen[biome_id]
206 {
207 seen[biome_id] = true;
208 biomes.push(biome_id);
209 }
210 });
211 }
212 }
213
214 biomes.sort_unstable();
215 biomes
216 }
217
218 pub(super) fn structures_for_decoration_step(
219 registry: &Registry,
220 step: usize,
221 ) -> Vec<StructureRef> {
222 registry
223 .structures
224 .iter()
225 .map(|(_, structure)| structure)
226 .filter(|structure| structure.step.decoration_ordinal() == step)
227 .collect()
228 }
229
230 pub(super) const fn center_chunk_writable_box(region: &WorldGenRegion<'_>) -> BoundingBox {
231 Self::chunk_writable_box(region.center(), region.min_y(), region.max_y_exclusive())
232 }
233
234 pub(super) const fn chunk_writable_box(
235 center: ChunkPos,
236 min_y: i32,
237 max_y_exclusive: i32,
238 ) -> BoundingBox {
239 let min_x = center.0.x * 16;
240 let min_z = center.0.y * 16;
241 BoundingBox::new(
242 IVec3::new(min_x, min_y + 1, min_z),
243 IVec3::new(min_x + 15, max_y_exclusive - 1, min_z + 15),
244 )
245 }
246
247 #[cfg(test)]
248 pub(super) fn resolve_structure_starts_from_references(
249 references: &StructureReferenceMap,
250 structure_id: &Identifier,
251 mut start_lookup: impl FnMut(steel_utils::ChunkPos, &Identifier) -> Option<StructureStart>,
252 ) -> Vec<StructureStart> {
253 let Some(source_positions) = references.get(structure_id) else {
254 return Vec::new();
255 };
256
257 let mut starts = Vec::new();
258 for &source_pos in source_positions {
259 let Some(start) = start_lookup(source_pos, structure_id) else {
260 continue;
261 };
262 if start.chunk_pos == source_pos && !start.pieces.is_empty() {
263 starts.push(start);
264 }
265 }
266 starts
267 }
268
269 fn structure_source_positions_in_region(
270 region: &WorldGenRegion<'_>,
271 structure_id: &Identifier,
272 ) -> Vec<steel_utils::ChunkPos> {
273 let center = region.center();
274 let center_chunk = region.chunk(center.0.x, center.0.y, ChunkStatus::StructureStarts);
275 let references = center_chunk.structure_references();
276 let source_positions = references
277 .get(structure_id)
278 .map(|positions| positions.iter().copied().collect::<Vec<_>>())
279 .unwrap_or_default();
280 drop(references);
281 source_positions
282 }
283
284 pub(super) fn place_structures_for_step(
285 region: &mut WorldGenRegion<'_>,
286 registry: &Registry,
287 decoration_seed: i64,
288 random: &mut WorldgenRandom,
289 step: usize,
290 biome_zoom_seed: i64,
291 ) {
292 let writable_box = Self::center_chunk_writable_box(region);
293
294 for (structure_index, structure) in Self::structures_for_decoration_step(registry, step)
295 .into_iter()
296 .enumerate()
297 {
298 Self::set_structure_seed(random, decoration_seed, structure_index, step);
299
300 let source_positions =
301 Self::structure_source_positions_in_region(region, &structure.key);
302 for source_pos in source_positions {
303 let Some(source_chunk) =
304 region.try_chunk(source_pos.0.x, source_pos.0.y, ChunkStatus::StructureStarts)
305 else {
306 continue;
307 };
308 let mut source_starts = source_chunk.structure_starts_mut();
309 let Some(start) = source_starts.get_mut(&structure.key) else {
310 continue;
311 };
312 if start.chunk_pos != source_pos || start.pieces.is_empty() {
313 continue;
314 }
315 let Some(reference_pos) = start.placement_reference_pos() else {
316 continue;
317 };
318 for piece in &mut start.pieces {
319 if piece.bounding_box.intersects(writable_box) {
320 StructurePiecePlacer::place_piece(
321 region,
322 registry,
323 piece,
324 reference_pos,
325 writable_box,
326 random,
327 biome_zoom_seed,
328 );
329 }
330 }
331 StructurePiecePlacer::after_place_structure(
332 region,
333 structure,
334 &mut start.pieces,
335 writable_box,
336 );
337 start.bounding_box =
338 StructureStart::compute_bounding_box(&start.pieces, start.bb_inflate);
339 }
340 }
341 }
342
343 pub(super) fn set_structure_seed(
344 random: &mut WorldgenRandom,
345 decoration_seed: i64,
346 structure_index: usize,
347 step: usize,
348 ) {
349 let Ok(structure_index_i32) = i32::try_from(structure_index) else {
350 panic!("structure index {structure_index} exceeds i32 range");
351 };
352 let Ok(step_i32) = i32::try_from(step) else {
353 panic!("decoration step {step} exceeds i32 range");
354 };
355 random.set_feature_seed(decoration_seed, structure_index_i32, step_i32);
356 }
357
358 #[expect(
359 clippy::too_many_arguments,
360 reason = "mirrors vanilla's decoration loop state without hiding generation inputs"
361 )]
362 pub(super) fn place_features_for_step(
363 region: &mut WorldGenRegion<'_>,
364 registry: &Registry,
365 decoration_seed: i64,
366 random: &mut WorldgenRandom,
367 origin: BlockPos,
368 step: usize,
369 step_features: &FeatureStepData,
370 possible_biomes: &[usize],
371 biome_zoom_seed: i64,
372 ) {
373 let mut feature_indices = SmallVec::<[usize; 64]>::new();
374
375 for &biome_id in possible_biomes {
376 if let Some(indices) = step_features.feature_indices_for_biome(biome_id) {
377 feature_indices.extend_from_slice(indices);
378 }
379 }
380
381 feature_indices.sort_unstable();
382 feature_indices.dedup();
383
384 for feature_index in feature_indices {
385 let Ok(feature_index_i32) = i32::try_from(feature_index) else {
386 panic!("decoration feature index {feature_index} exceeds i32 range");
387 };
388 let Ok(step_i32) = i32::try_from(step) else {
389 panic!("decoration step {step} exceeds i32 range");
390 };
391 let Some(feature) = step_features.feature(feature_index) else {
392 panic!("decoration step {step} references missing feature index {feature_index}");
393 };
394 random.set_feature_seed(decoration_seed, feature_index_i32, step_i32);
395 Self::place_placed_feature_entry(
396 region,
397 registry,
398 random,
399 origin,
400 feature,
401 biome_zoom_seed,
402 );
403 }
404 }
405}