Skip to main content

steel_core/worldgen/generator/
generation_chunk.rs

1use std::marker::PhantomData;
2
3use steel_utils::{BlockPos, BlockStateId, ChunkPos, DowncastType, types::UpdateFlags};
4
5use crate::chunk::Chunk;
6use crate::chunk::chunk_holder::ChunkHolder;
7use crate::chunk::heightmap::{Heightmap, HeightmapType};
8use crate::chunk::status::ChunkStatus;
9
10/// Marker for vanilla's atomic Terrain generation operation.
11pub enum TerrainPhase {}
12
13/// Stage-scoped access to the center chunk being generated.
14///
15/// The scheduler constructs this capability only after the chunk's input status is
16/// published. It deliberately does not expose the underlying [`Chunk`] or its raw
17/// section storage, so status-sensitive writes stay tied to the operation that owns
18/// them.
19///
20/// ```compile_fail
21/// use steel_core::chunk::Chunk;
22/// use steel_core::worldgen::generator::{GenerationChunk, TerrainPhase};
23///
24/// fn forge_generation_access(chunk: &Chunk) -> GenerationChunk<'_, TerrainPhase> {
25///     GenerationChunk::from_chunk(chunk)
26/// }
27/// ```
28#[repr(transparent)]
29pub struct GenerationChunk<'a, Phase> {
30    chunk: &'a Chunk,
31    phase: PhantomData<fn() -> Phase>,
32}
33
34impl<Phase> Copy for GenerationChunk<'_, Phase> {}
35
36impl<Phase> Clone for GenerationChunk<'_, Phase> {
37    fn clone(&self) -> Self {
38        *self
39    }
40}
41
42impl<'a, Phase> GenerationChunk<'a, Phase> {
43    const fn from_chunk(chunk: &'a Chunk) -> Self {
44        Self {
45            chunk,
46            phase: PhantomData,
47        }
48    }
49
50    #[cfg(test)]
51    pub(crate) const fn for_test(chunk: &'a Chunk) -> Self {
52        Self::from_chunk(chunk)
53    }
54
55    fn acquire_input(holder: &'a ChunkHolder, input_status: ChunkStatus) -> Self {
56        assert_eq!(
57            holder.published_status(),
58            Some(input_status),
59            "generation capability requires the exact published input status"
60        );
61        let Some(chunk) = holder.try_chunk(input_status) else {
62            panic!("chunk data missing after {input_status:?} publication");
63        };
64        Self::from_chunk(chunk)
65    }
66
67    /// Returns the chunk position.
68    #[must_use]
69    pub const fn pos(self) -> ChunkPos {
70        self.chunk.pos()
71    }
72
73    /// Returns the dimension's minimum block Y.
74    #[must_use]
75    pub const fn min_y(self) -> i32 {
76        self.chunk.min_y()
77    }
78
79    /// Returns the dimension's build height.
80    #[must_use]
81    pub const fn height(self) -> i32 {
82        self.chunk.height()
83    }
84
85    /// Returns the number of vertical chunk sections.
86    #[must_use]
87    #[inline]
88    pub fn section_count(self) -> usize {
89        self.chunk.sections().sections.len()
90    }
91
92    /// Reads one block using coordinates relative to the dimension's minimum Y.
93    #[must_use]
94    #[inline]
95    pub fn get_relative_block(
96        self,
97        relative_x: usize,
98        relative_y: usize,
99        relative_z: usize,
100    ) -> Option<BlockStateId> {
101        self.chunk
102            .get_relative_block(relative_x, relative_y, relative_z)
103    }
104
105    /// Reads one block at a world position.
106    #[must_use]
107    #[inline]
108    pub fn get_block_state(self, pos: BlockPos) -> BlockStateId {
109        self.chunk.get_block_state(pos)
110    }
111}
112
113impl GenerationChunk<'_, TerrainPhase> {
114    pub(crate) fn acquire(holder: &ChunkHolder) -> GenerationChunk<'_, TerrainPhase> {
115        GenerationChunk::acquire_input(holder, ChunkStatus::Biomes)
116    }
117
118    /// Writes a terrain block batch using the published Biomes semantics.
119    #[inline]
120    pub fn write_block_batch(self, blocks: &[(usize, usize, usize, BlockStateId)]) {
121        self.chunk
122            .write_block_batch_for_generation(ChunkStatus::Biomes, blocks);
123    }
124
125    /// write a noise column heightmaps are installed separately
126    #[inline]
127    pub fn write_noise_column(self, x: usize, z: usize, blocks: &[(usize, BlockStateId)]) {
128        self.chunk
129            .write_column_blocks_for_generation(ChunkStatus::Biomes, x, z, blocks);
130    }
131
132    /// Replaces the two heightmaps initialized directly by noise filling.
133    ///
134    /// # Panics
135    ///
136    /// Panics if either heightmap has a type other than its corresponding noise-stage type.
137    pub fn replace_noise_heightmaps(self, ocean_floor: Heightmap, world_surface: Heightmap) {
138        assert_eq!(ocean_floor.heightmap_type(), HeightmapType::OceanFloorWg);
139        assert_eq!(
140            world_surface.heightmap_type(),
141            HeightmapType::WorldSurfaceWg
142        );
143        let mut heightmaps = self.chunk.heightmaps.write();
144        heightmaps.replace(ocean_floor);
145        heightmaps.replace(world_surface);
146    }
147
148    /// Installs generator-owned state for reuse by the remaining terrain work.
149    pub fn install_post_noise_state<T>(self, state: T)
150    where
151        T: DowncastType + Send + Sync,
152    {
153        self.chunk.install_transient_generation_state(state);
154    }
155
156    /// Marks a position for Vanilla generation postprocessing.
157    #[inline]
158    pub fn mark_pos_for_postprocessing(self, pos: BlockPos) {
159        self.chunk.mark_pos_for_postprocessing(pos);
160    }
161
162    /// Writes one block using the published Biomes semantics.
163    #[inline]
164    pub fn set_relative_block(
165        self,
166        relative_x: usize,
167        relative_y: usize,
168        relative_z: usize,
169        state: BlockStateId,
170    ) {
171        self.chunk.set_relative_block_for_generation(
172            ChunkStatus::Biomes,
173            relative_x,
174            relative_y,
175            relative_z,
176            state,
177        );
178    }
179    /// Ensures the world-surface worldgen heightmap exists.
180    pub fn prime_world_surface_heightmap(self) {
181        self.chunk
182            .prime_heightmaps(&[HeightmapType::WorldSurfaceWg]);
183    }
184
185    /// Borrows generator-owned state retained after noise filling.
186    pub fn with_post_noise_state_mut<T, R>(self, f: impl FnOnce(&mut T) -> R) -> Option<R>
187    where
188        T: DowncastType + Send + Sync,
189    {
190        self.chunk.with_transient_generation_state_mut(f)
191    }
192
193    /// Copies every biome palette entry in section order.
194    #[must_use]
195    pub fn read_all_biomes(self) -> Box<[u16]> {
196        self.chunk.sections().read_all_biomes()
197    }
198
199    /// Reads one complete block column into `output`.
200    #[inline]
201    pub fn read_column_into(self, local_x: usize, local_z: usize, output: &mut Vec<BlockStateId>) {
202        self.chunk
203            .sections()
204            .read_column_into(local_x, local_z, output);
205    }
206
207    /// Reads the world-surface worldgen height, lazily priming it if needed.
208    #[must_use]
209    #[inline]
210    pub fn world_surface_height_at(self, local_x: usize, local_z: usize) -> i32 {
211        self.chunk
212            .generation_height_at(HeightmapType::WorldSurfaceWg, local_x, local_z)
213    }
214
215    /// Writes one terrain column and applies its Biomes-input heightmap effects.
216    #[inline]
217    pub fn write_column(self, local_x: usize, local_z: usize, blocks: &[(usize, BlockStateId)]) {
218        self.chunk.write_column_blocks_for_generation(
219            ChunkStatus::Biomes,
220            local_x,
221            local_z,
222            blocks,
223        );
224        self.chunk.update_heightmaps_after_direct_column_writes(
225            ChunkStatus::Biomes,
226            local_x,
227            local_z,
228            blocks,
229        );
230        self.chunk.mark_dirty();
231    }
232
233    /// Drops retained generator state after terrain generation.
234    pub fn clear_post_noise_state(self) {
235        self.chunk.clear_transient_generation_state();
236    }
237
238    /// Consumes generator-owned state retained after noise filling.
239    pub fn consume_post_noise_state<T, R>(self, f: impl FnOnce(Option<&mut T>) -> R) -> R
240    where
241        T: DowncastType + Send + Sync,
242    {
243        self.chunk.consume_transient_generation_state(f)
244    }
245
246    /// Sets one carved block using the published Biomes semantics.
247    #[inline]
248    pub fn set_block_state(self, pos: BlockPos, state: BlockStateId) {
249        let _ = self.chunk.set_block_state_for_generation(
250            ChunkStatus::Biomes,
251            pos,
252            state,
253            UpdateFlags::empty(),
254        );
255    }
256
257    /// Runs `f` with the world-surface worldgen heightmap if priming provides it.
258    pub fn with_world_surface_heightmap<R>(self, f: impl FnOnce(&Heightmap) -> R) -> Option<R> {
259        let heightmaps = self.chunk.generation_heightmaps();
260        if let Some(heightmap) = heightmaps.get(HeightmapType::WorldSurfaceWg) {
261            return Some(f(heightmap));
262        }
263        drop(heightmaps);
264
265        self.prime_world_surface_heightmap();
266        let heightmaps = self.chunk.generation_heightmaps();
267        heightmaps.get(HeightmapType::WorldSurfaceWg).map(f)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use std::sync::{Arc, Weak};
274
275    use steel_registry::init_vanilla_registry;
276    use steel_utils::ChunkPos;
277
278    use super::{GenerationChunk, TerrainPhase};
279    use crate::chunk::Chunk;
280    use crate::chunk::chunk_holder::ChunkHolder;
281    use crate::chunk::chunk_ticket_manager::ChunkTicketLevel;
282    use crate::chunk::section::{ChunkSection, Sections};
283    use crate::chunk::status::ChunkStatus;
284
285    fn holder_at(status: ChunkStatus) -> Arc<ChunkHolder> {
286        init_vanilla_registry();
287        let pos = ChunkPos::new(0, 0);
288        let holder = Arc::new(ChunkHolder::new(
289            pos,
290            ChunkTicketLevel::STRONGEST,
291            None,
292            0,
293            16,
294        ));
295        let chunk = Chunk::new(
296            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
297            pos,
298            0,
299            16,
300            Weak::new(),
301        );
302        holder.insert_chunk(chunk, status);
303        holder
304    }
305
306    #[test]
307    #[should_panic(expected = "generation capability requires the exact published input status")]
308    fn capability_rejects_the_wrong_published_input() {
309        let holder = holder_at(ChunkStatus::StructureReferences);
310        let _ = GenerationChunk::<TerrainPhase>::acquire(&holder);
311    }
312
313    #[test]
314    #[should_panic(expected = "generation capability requires the exact published input status")]
315    fn capability_rejects_an_advanced_published_input() {
316        let holder = holder_at(ChunkStatus::Terrain);
317        let _ = GenerationChunk::<TerrainPhase>::acquire(&holder);
318    }
319}
320
321#[cfg(feature = "benchmark-support")]
322/// Direct generator calls used by Criterion benchmarks.
323pub mod benchmark_support {
324    use glam::IVec3;
325    use steel_worldgen::noise::Beardifier;
326
327    use super::{GenerationChunk, TerrainPhase};
328    use crate::chunk::Chunk;
329    use crate::worldgen::generator::ChunkGenerator;
330
331    /// Calls Noise directly for a Criterion benchmark.
332    pub fn fill_from_noise<G>(generator: &G, chunk: &Chunk, beardifier: Option<&Beardifier>)
333    where
334        G: ChunkGenerator + ?Sized,
335    {
336        generator.fill_from_noise(
337            GenerationChunk::<TerrainPhase>::from_chunk(chunk),
338            beardifier,
339        );
340    }
341
342    /// Calls Surface directly for a Criterion benchmark.
343    pub fn build_surface<G>(generator: &G, chunk: &Chunk, neighbor_biomes: &dyn Fn(IVec3) -> u16)
344    where
345        G: ChunkGenerator + ?Sized,
346    {
347        generator.build_surface(
348            GenerationChunk::<TerrainPhase>::from_chunk(chunk),
349            neighbor_biomes,
350        );
351    }
352
353    /// Calls Carvers directly for a Criterion benchmark.
354    pub fn apply_carvers<G>(generator: &G, chunk: &Chunk)
355    where
356        G: ChunkGenerator + ?Sized,
357    {
358        generator.apply_carvers(GenerationChunk::<TerrainPhase>::from_chunk(chunk));
359    }
360}