Skip to main content

steel_worldgen/structure/
generation.rs

1use crate::biomes::ChunkBiomeSampler;
2use crate::density::traits::{ColumnCache, NoiseSettings};
3use crate::noise::AquiferResult;
4use crate::noise::LazyAquifer;
5use crate::noise::PreliminarySurfaceStore;
6use crate::structure::StructurePiece;
7use crate::utils::column_base_height;
8use crate::utils::column_interpolated_density;
9use crate::utils::find_solid_block_below_air;
10use crate::utils::iterate_noise_column_with_aquifer;
11use crate::{density::DimensionNoises, noise::Aquifer};
12use rustc_hash::FxHashMap;
13use std::cell::RefCell;
14use std::sync::Arc;
15use steel_registry::biome::BiomeRef;
16use steel_registry::template_pool::{TemplateData, TemplatePoolData};
17use steel_utils::Identifier;
18use steel_utils::random::RandomSplitter;
19
20/// Block classification in the base-noise column (no surface rules).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ColumnBlock {
23    /// Empty.
24    Air,
25    /// Aquifer-placed fluid (lava/water).
26    Fluid,
27    /// Default solid block (stone, netherrack, end stone).
28    Solid,
29}
30
31/// Per-chunk context shared by every structure's `findGenerationPoint`.
32///
33/// Holds mutable per-chunk state (biome sampler, height cache, aquifer) so structures
34/// don't each allocate their own. Wraps `VanillaGenerator`'s helpers.
35pub struct GenerationContext<'ctx, 'src, N: DimensionNoises>
36where
37    'src: 'ctx,
38{
39    /// World seed.
40    seed: i64,
41    /// Chunk being populated.
42    chunk_x: i32,
43    /// Chunk being populated.
44    chunk_z: i32,
45    /// `chunk_x * 16`.
46    chunk_min_x: i32,
47    /// `chunk_z * 16`.
48    chunk_min_z: i32,
49    /// `chunk_min_x + 8`.
50    center_block_x: i32,
51    /// `chunk_min_z + 8`.
52    center_block_z: i32,
53    /// Sea level for this dimension.
54    sea_level: i32,
55    /// Shared memoisation slot for the chunk-center surface Y.
56    surface_y_cache: &'ctx mut Option<i32>,
57    /// Whether `height_cache`'s 5×5 quart grid has been populated. Shared across
58    /// per-structure contexts in the same chunk.
59    height_cache_grid_ready: &'ctx mut bool,
60
61    /// Dimension noise router.
62    noises: &'src N,
63    /// Positional splitter for per-chunk RNG.
64    splitter: &'src RandomSplitter,
65    /// Generator-wide preliminary surface level memo, shared by probe aquifers.
66    preliminary_surface: &'src Arc<PreliminarySurfaceStore>,
67    /// Template pool registry for jigsaw assembly.
68    template_pools: &'src FxHashMap<Identifier, TemplatePoolData>,
69    /// Template data registry for jigsaw assembly.
70    templates: &'src FxHashMap<Identifier, TemplateData>,
71
72    /// Biome sampler scoped to this chunk.
73    biome_sampler: &'ctx mut ChunkBiomeSampler<'src>,
74    /// Column cache for height/density queries (grid-initialized on demand).
75    height_cache: &'ctx mut N::ColumnCache,
76    /// Aquifer built on first query; skipped on chunks where no structure needs it.
77    aquifer: &'ctx mut LazyAquifer<'src, N>,
78    /// Cache for terrain height checks.
79    terrain_height_cache: RefCell<FxHashMap<(i32, i32, bool), i32>>,
80    /// Cache for terrain opacity checks.
81    terrain_opaque_cache: RefCell<FxHashMap<(i32, i32, i32, bool), bool>>,
82    /// Probes for off-chunk height/opaque checks.
83    terrain_probes: RefCell<FxHashMap<(i32, i32), TerrainProbe<N>>>,
84}
85
86/// An off-chunk height and opacity probe.
87pub struct TerrainProbe<N: DimensionNoises> {
88    cache: N::ColumnCache,
89    aquifer: Aquifer<N>,
90}
91
92impl<N: DimensionNoises> TerrainProbe<N> {
93    fn new(
94        chunk_min_x: i32,
95        chunk_min_z: i32,
96        splitter: &RandomSplitter,
97        noises: &N,
98        preliminary_surface: &Arc<PreliminarySurfaceStore>,
99    ) -> Self {
100        let mut cache = N::ColumnCache::default();
101        cache.init_grid(chunk_min_x, chunk_min_z, noises);
102        let aquifer = Aquifer::<N>::new(
103            chunk_min_x,
104            chunk_min_z,
105            N::Settings::MIN_Y,
106            N::Settings::HEIGHT,
107            splitter,
108            noises,
109            preliminary_surface,
110            cache.clone(),
111        );
112        Self { cache, aquifer }
113    }
114}
115
116/// Result of a successful `Structure::find_generation_point`.
117pub struct GenerationStub {
118    /// World-space position the start anchors at.
119    pub position: (i32, i32, i32),
120    /// Pieces already sized and positioned in world space.
121    pub pieces: Vec<StructurePiece>,
122}
123
124/// Terrain, biome, and template queries exposed to structure algorithms.
125///
126/// Vanilla calls these through `ChunkGenerator`/`WorldGenLevel`; keeping the
127/// interface here lets structure algorithms stay independent of a concrete
128/// chunk generator while preserving their vanilla query order.
129pub trait StructureGenerationContext {
130    /// World seed.
131    fn seed(&self) -> i64;
132    /// Chunk X being populated.
133    fn chunk_x(&self) -> i32;
134    /// Chunk Z being populated.
135    fn chunk_z(&self) -> i32;
136    /// Minimum block X of the chunk.
137    fn chunk_min_x(&self) -> i32;
138    /// Minimum block Z of the chunk.
139    fn chunk_min_z(&self) -> i32;
140    /// Center block X of the chunk.
141    fn center_block_x(&self) -> i32;
142    /// Center block Z of the chunk.
143    fn center_block_z(&self) -> i32;
144    /// Sea level for this generator/dimension.
145    fn sea_level(&self) -> i32;
146    /// Minimum build Y.
147    fn min_y(&self) -> i32;
148    /// Total build height.
149    fn height(&self) -> i32;
150    /// One-past-maximum build Y.
151    fn max_y(&self) -> i32 {
152        self.min_y() + self.height()
153    }
154    /// Template pool registry for jigsaw assembly.
155    fn template_pools(&self) -> &FxHashMap<Identifier, TemplatePoolData>;
156    /// Structure templates (piece definitions + sizes).
157    fn templates(&self) -> &FxHashMap<Identifier, TemplateData>;
158    /// Base height at a column.
159    fn base_height(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32;
160    /// Full-column base height scan.
161    fn base_height_full(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32;
162    /// Biome at a block position.
163    fn biome_at(&mut self, block_x: i32, block_y: i32, block_z: i32) -> BiomeRef;
164    /// Classify a block in the generator's base terrain.
165    fn column_state(&mut self, x: i32, y: i32, z: i32) -> ColumnBlock;
166    /// Highest solid base-terrain block directly below air in `[min_solid_y, start_y)`.
167    fn solid_block_below_air(
168        &mut self,
169        x: i32,
170        z: i32,
171        start_y: i32,
172        min_solid_y: i32,
173    ) -> Option<i32> {
174        if start_y <= min_solid_y {
175            return None;
176        }
177
178        let mut above = self.column_state(x, start_y, z);
179        for y in (min_solid_y..start_y).rev() {
180            let current = self.column_state(x, y, z);
181            if above == ColumnBlock::Air && current == ColumnBlock::Solid {
182                return Some(y);
183            }
184            above = current;
185        }
186        None
187    }
188    /// Chunk-center surface Y, memoised by the concrete context.
189    fn surface_y(&mut self) -> i32;
190    /// Surface height for off-chunk terrain queries used by piece placement.
191    fn terrain_surface_height(&self, x: i32, z: i32, ocean_floor: bool) -> i32;
192    /// Opaque terrain test for off-chunk terrain queries used by piece placement.
193    fn terrain_is_opaque(&self, x: i32, y: i32, z: i32, ocean_floor: bool) -> bool;
194}
195
196impl<'ctx, 'src, N: DimensionNoises> GenerationContext<'ctx, 'src, N>
197where
198    'src: 'ctx,
199{
200    /// Creates a per-chunk structure generation context.
201    #[must_use]
202    #[expect(
203        clippy::too_many_arguments,
204        reason = "borrows all mutable per-chunk generation state without owning it"
205    )]
206    pub fn new(
207        seed: i64,
208        chunk_x: i32,
209        chunk_z: i32,
210        sea_level: i32,
211        noises: &'src N,
212        splitter: &'src RandomSplitter,
213        preliminary_surface: &'src Arc<PreliminarySurfaceStore>,
214        template_pools: &'src FxHashMap<Identifier, TemplatePoolData>,
215        templates: &'src FxHashMap<Identifier, TemplateData>,
216        biome_sampler: &'ctx mut ChunkBiomeSampler<'src>,
217        height_cache: &'ctx mut N::ColumnCache,
218        aquifer: &'ctx mut LazyAquifer<'src, N>,
219        surface_y_cache: &'ctx mut Option<i32>,
220        height_cache_grid_ready: &'ctx mut bool,
221    ) -> Self {
222        let chunk_min_x = chunk_x * 16;
223        let chunk_min_z = chunk_z * 16;
224        Self {
225            seed,
226            chunk_x,
227            chunk_z,
228            chunk_min_x,
229            chunk_min_z,
230            center_block_x: chunk_min_x + 8,
231            center_block_z: chunk_min_z + 8,
232            sea_level,
233            surface_y_cache,
234            height_cache_grid_ready,
235            noises,
236            splitter,
237            preliminary_surface,
238            template_pools,
239            templates,
240            biome_sampler,
241            height_cache,
242            aquifer,
243            terrain_height_cache: RefCell::default(),
244            terrain_opaque_cache: RefCell::default(),
245            terrain_probes: RefCell::default(),
246        }
247    }
248
249    /// `getBaseHeight(WORLD_SURFACE_WG)` — aquifer-aware, scans from
250    /// `preliminary_surface_level + 16`.
251    ///
252    /// `ocean_floor=false` → opaque is Solid+Fluid; `true` → opaque is Solid only.
253    ///
254    /// In dimensions with a constant `preliminary_surface_level` (End), use
255    /// [`base_height_full`](Self::base_height_full) instead.
256    pub fn base_height(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 {
257        self.ensure_height_cache_grid();
258        let aq = self.aquifer.ensure(self.height_cache);
259        column_base_height::<N>(self.height_cache, self.noises, aq, x, z, ocean_floor)
260    }
261
262    /// Full-column scan from chunk top. Matches vanilla's `iterateNoiseColumn`.
263    pub fn base_height_full(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 {
264        self.ensure_height_cache_grid();
265        let aq = self.aquifer.ensure(self.height_cache);
266        iterate_noise_column_with_aquifer::<N>(
267            self.height_cache,
268            self.noises,
269            aq,
270            x,
271            z,
272            ocean_floor,
273        )
274    }
275
276    /// Biome at a block position (quantized to quart).
277    pub fn biome_at(&mut self, block_x: i32, block_y: i32, block_z: i32) -> BiomeRef {
278        self.biome_sampler
279            .sample(block_x >> 2, block_y >> 2, block_z >> 2)
280    }
281
282    /// Classify a single block in the base-noise column.
283    pub fn column_state(&mut self, x: i32, y: i32, z: i32) -> ColumnBlock {
284        self.ensure_height_cache_grid();
285        let cw = N::Settings::CELL_WIDTH;
286        let ch = N::Settings::CELL_HEIGHT;
287        let density =
288            column_interpolated_density::<N>(self.height_cache, self.noises, x, y, z, cw, ch);
289        let aq = self.aquifer.ensure(self.height_cache);
290        match aq.compute_substance(self.noises, x, y, z, f64::from(density)) {
291            AquiferResult::Solid => ColumnBlock::Solid,
292            AquiferResult::Fluid(_) => ColumnBlock::Fluid,
293            AquiferResult::Air => ColumnBlock::Air,
294        }
295    }
296
297    /// Highest solid base-terrain block directly below air in `[min_solid_y, start_y)`.
298    pub fn solid_block_below_air(
299        &mut self,
300        x: i32,
301        z: i32,
302        start_y: i32,
303        min_solid_y: i32,
304    ) -> Option<i32> {
305        self.ensure_height_cache_grid();
306        let aq = self.aquifer.ensure(self.height_cache);
307        find_solid_block_below_air::<N>(
308            self.height_cache,
309            self.noises,
310            aq,
311            x,
312            z,
313            start_y,
314            min_solid_y,
315        )
316    }
317
318    /// Surface Y at chunk center, memoised across per-structure contexts.
319    pub fn surface_y(&mut self) -> i32 {
320        if let Some(y) = *self.surface_y_cache {
321            return y;
322        }
323        let y = self.base_height(self.center_block_x, self.center_block_z, false) - 1;
324        *self.surface_y_cache = Some(y);
325        y
326    }
327
328    fn ensure_height_cache_grid(&mut self) {
329        if *self.height_cache_grid_ready {
330            return;
331        }
332        self.height_cache
333            .init_grid(self.chunk_min_x, self.chunk_min_z, self.noises);
334        *self.height_cache_grid_ready = true;
335    }
336}
337
338impl<N: DimensionNoises> StructureGenerationContext for GenerationContext<'_, '_, N> {
339    fn seed(&self) -> i64 {
340        self.seed
341    }
342
343    fn chunk_x(&self) -> i32 {
344        self.chunk_x
345    }
346
347    fn chunk_z(&self) -> i32 {
348        self.chunk_z
349    }
350
351    fn chunk_min_x(&self) -> i32 {
352        self.chunk_min_x
353    }
354
355    fn chunk_min_z(&self) -> i32 {
356        self.chunk_min_z
357    }
358
359    fn center_block_x(&self) -> i32 {
360        self.center_block_x
361    }
362
363    fn center_block_z(&self) -> i32 {
364        self.center_block_z
365    }
366
367    fn sea_level(&self) -> i32 {
368        self.sea_level
369    }
370
371    fn min_y(&self) -> i32 {
372        N::Settings::MIN_Y
373    }
374
375    fn height(&self) -> i32 {
376        N::Settings::HEIGHT
377    }
378
379    fn template_pools(&self) -> &FxHashMap<Identifier, TemplatePoolData> {
380        self.template_pools
381    }
382
383    fn templates(&self) -> &FxHashMap<Identifier, TemplateData> {
384        self.templates
385    }
386
387    fn base_height(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 {
388        GenerationContext::base_height(self, x, z, ocean_floor)
389    }
390
391    fn base_height_full(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 {
392        GenerationContext::base_height_full(self, x, z, ocean_floor)
393    }
394
395    fn biome_at(&mut self, block_x: i32, block_y: i32, block_z: i32) -> BiomeRef {
396        GenerationContext::biome_at(self, block_x, block_y, block_z)
397    }
398
399    fn column_state(&mut self, x: i32, y: i32, z: i32) -> ColumnBlock {
400        GenerationContext::column_state(self, x, y, z)
401    }
402
403    fn solid_block_below_air(
404        &mut self,
405        x: i32,
406        z: i32,
407        start_y: i32,
408        min_solid_y: i32,
409    ) -> Option<i32> {
410        GenerationContext::solid_block_below_air(self, x, z, start_y, min_solid_y)
411    }
412
413    fn surface_y(&mut self) -> i32 {
414        GenerationContext::surface_y(self)
415    }
416
417    fn terrain_surface_height(&self, x: i32, z: i32, ocean_floor: bool) -> i32 {
418        if let Some(height) = self
419            .terrain_height_cache
420            .borrow()
421            .get(&(x, z, ocean_floor))
422            .copied()
423        {
424            return height;
425        }
426
427        let cell_w = N::Settings::CELL_WIDTH;
428        let cell_x = x.div_euclid(cell_w) * cell_w;
429        let cell_z = z.div_euclid(cell_w) * cell_w;
430        let aq_chunk_x = (cell_x >> 4) * 16;
431        let aq_chunk_z = (cell_z >> 4) * 16;
432        let height = {
433            let mut probes = self.terrain_probes.borrow_mut();
434            let probe = probes.entry((aq_chunk_x, aq_chunk_z)).or_insert_with(|| {
435                TerrainProbe::<N>::new(
436                    aq_chunk_x,
437                    aq_chunk_z,
438                    self.splitter,
439                    self.noises,
440                    self.preliminary_surface,
441                )
442            });
443            iterate_noise_column_with_aquifer::<N>(
444                &mut probe.cache,
445                self.noises,
446                &mut probe.aquifer,
447                x,
448                z,
449                ocean_floor,
450            )
451        };
452        self.terrain_height_cache
453            .borrow_mut()
454            .insert((x, z, ocean_floor), height);
455        height
456    }
457
458    fn terrain_is_opaque(&self, x: i32, y: i32, z: i32, ocean_floor: bool) -> bool {
459        if let Some(opaque) = self
460            .terrain_opaque_cache
461            .borrow()
462            .get(&(x, y, z, ocean_floor))
463            .copied()
464        {
465            return opaque;
466        }
467
468        let cell_w = N::Settings::CELL_WIDTH;
469        let cell_h = N::Settings::CELL_HEIGHT;
470        let cell_x = x.div_euclid(cell_w) * cell_w;
471        let cell_z = z.div_euclid(cell_w) * cell_w;
472        let aq_chunk_x = (cell_x >> 4) * 16;
473        let aq_chunk_z = (cell_z >> 4) * 16;
474        let opaque = {
475            let mut probes = self.terrain_probes.borrow_mut();
476            let probe = probes.entry((aq_chunk_x, aq_chunk_z)).or_insert_with(|| {
477                TerrainProbe::<N>::new(
478                    aq_chunk_x,
479                    aq_chunk_z,
480                    self.splitter,
481                    self.noises,
482                    self.preliminary_surface,
483                )
484            });
485            let density = column_interpolated_density::<N>(
486                &mut probe.cache,
487                self.noises,
488                x,
489                y,
490                z,
491                cell_w,
492                cell_h,
493            );
494            match probe
495                .aquifer
496                .compute_substance(self.noises, x, y, z, f64::from(density))
497            {
498                AquiferResult::Solid => true,
499                AquiferResult::Fluid(_) => !ocean_floor,
500                AquiferResult::Air => false,
501            }
502        };
503        self.terrain_opaque_cache
504            .borrow_mut()
505            .insert((x, y, z, ocean_floor), opaque);
506        opaque
507    }
508}