Skip to main content

steel_core/worldgen/feature/
runner.rs

1use 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/// Runs the structure-piece and placed-feature decoration pass for a generator.
15#[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_or_default(|positions| positions.iter().copied().collect::<Vec<_>>());
279        drop(references);
280        source_positions
281    }
282
283    pub(super) fn place_structures_for_step(
284        region: &mut WorldGenRegion<'_>,
285        registry: &Registry,
286        decoration_seed: i64,
287        random: &mut WorldgenRandom,
288        step: usize,
289        biome_zoom_seed: i64,
290    ) {
291        let writable_box = Self::center_chunk_writable_box(region);
292
293        for (structure_index, structure) in Self::structures_for_decoration_step(registry, step)
294            .into_iter()
295            .enumerate()
296        {
297            Self::set_structure_seed(random, decoration_seed, structure_index, step);
298
299            let source_positions =
300                Self::structure_source_positions_in_region(region, &structure.key);
301            for source_pos in source_positions {
302                let Some(source_chunk) =
303                    region.try_chunk(source_pos.0.x, source_pos.0.y, ChunkStatus::StructureStarts)
304                else {
305                    continue;
306                };
307                let mut source_starts = source_chunk.structure_starts_mut();
308                let Some(start) = source_starts.get_mut(&structure.key) else {
309                    continue;
310                };
311                if start.chunk_pos != source_pos || start.pieces.is_empty() {
312                    continue;
313                }
314                let Some(reference_pos) = start.placement_reference_pos() else {
315                    continue;
316                };
317                for piece in &mut start.pieces {
318                    if piece.bounding_box.intersects(writable_box) {
319                        StructurePiecePlacer::place_piece(
320                            region,
321                            registry,
322                            piece,
323                            reference_pos,
324                            writable_box,
325                            random,
326                            biome_zoom_seed,
327                        );
328                    }
329                }
330                StructurePiecePlacer::after_place_structure(
331                    region,
332                    structure,
333                    &mut start.pieces,
334                    writable_box,
335                );
336                start.bounding_box =
337                    StructureStart::compute_bounding_box(&start.pieces, start.bb_inflate);
338            }
339        }
340    }
341
342    pub(super) fn set_structure_seed(
343        random: &mut WorldgenRandom,
344        decoration_seed: i64,
345        structure_index: usize,
346        step: usize,
347    ) {
348        let Ok(structure_index_i32) = i32::try_from(structure_index) else {
349            panic!("structure index {structure_index} exceeds i32 range");
350        };
351        let Ok(step_i32) = i32::try_from(step) else {
352            panic!("decoration step {step} exceeds i32 range");
353        };
354        random.set_feature_seed(decoration_seed, structure_index_i32, step_i32);
355    }
356
357    #[expect(
358        clippy::too_many_arguments,
359        reason = "mirrors vanilla's decoration loop state without hiding generation inputs"
360    )]
361    pub(super) fn place_features_for_step(
362        region: &mut WorldGenRegion<'_>,
363        registry: &Registry,
364        decoration_seed: i64,
365        random: &mut WorldgenRandom,
366        origin: BlockPos,
367        step: usize,
368        step_features: &FeatureStepData,
369        possible_biomes: &[usize],
370        biome_zoom_seed: i64,
371    ) {
372        let mut feature_indices = SmallVec::<[usize; 64]>::new();
373
374        for &biome_id in possible_biomes {
375            if let Some(indices) = step_features.feature_indices_for_biome(biome_id) {
376                feature_indices.extend_from_slice(indices);
377            }
378        }
379
380        feature_indices.sort_unstable();
381        feature_indices.dedup();
382
383        for feature_index in feature_indices {
384            let Ok(feature_index_i32) = i32::try_from(feature_index) else {
385                panic!("decoration feature index {feature_index} exceeds i32 range");
386            };
387            let Ok(step_i32) = i32::try_from(step) else {
388                panic!("decoration step {step} exceeds i32 range");
389            };
390            let Some(feature) = step_features.feature(feature_index) else {
391                panic!("decoration step {step} references missing feature index {feature_index}");
392            };
393            random.set_feature_seed(decoration_seed, feature_index_i32, step_i32);
394            Self::place_placed_feature_entry(
395                region,
396                registry,
397                random,
398                origin,
399                feature,
400                biome_zoom_seed,
401            );
402        }
403    }
404}