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(|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}