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