Skip to main content

steel_core/worldgen/structure/piece_placer/
scattered_feature.rs

1use std::sync::Weak;
2
3use glam::IVec3;
4use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_registry::blocks::properties::BlockStateProperties;
6use steel_registry::{Registry, vanilla_block_entity_types, vanilla_blocks};
7use steel_utils::random::Random;
8use steel_utils::random::worldgen_random::WorldgenRandom;
9use steel_utils::{BlockPos, BlockStateId, BoundingBox, Direction, types::UpdateFlags};
10
11use crate::chunk::heightmap::HeightmapType;
12use crate::entity::SharedEntity;
13use crate::world::World;
14use crate::worldgen::region::WorldGenRegion;
15use crate::worldgen::template::StructureTemplate;
16
17use super::StructurePiecePlacer;
18
19pub(super) struct ScatteredFeaturePlacer<'a, 'world> {
20    region: &'a mut WorldGenRegion<'world>,
21    registry: &'a Registry,
22    bounding_box: &'a mut BoundingBox,
23    orientation: Option<Direction>,
24    clip: BoundingBox,
25}
26
27impl<'a, 'world> ScatteredFeaturePlacer<'a, 'world> {
28    pub(super) const fn new(
29        region: &'a mut WorldGenRegion<'world>,
30        registry: &'a Registry,
31        bounding_box: &'a mut BoundingBox,
32        orientation: Option<Direction>,
33        clip: BoundingBox,
34    ) -> Self {
35        Self {
36            region,
37            registry,
38            bounding_box,
39            orientation,
40            clip,
41        }
42    }
43
44    pub(super) fn update_average_ground_height(
45        &mut self,
46        height_position: &mut Option<i32>,
47        offset: i32,
48    ) -> bool {
49        if height_position.is_some() {
50            return true;
51        }
52
53        let mut total = 0;
54        let mut count = 0;
55        for z in self.bounding_box.min_z()..=self.bounding_box.max_z() {
56            for x in self.bounding_box.min_x()..=self.bounding_box.max_x() {
57                if self.clip.contains_blockpos(BlockPos::new(x, 64, z)) {
58                    total += self
59                        .region
60                        .height_at(HeightmapType::MotionBlockingNoLeaves, x, z);
61                    count += 1;
62                }
63            }
64        }
65
66        if count == 0 {
67            return false;
68        }
69
70        let adjusted = total / count;
71        *height_position = Some(adjusted);
72        let dy = adjusted - self.bounding_box.min_y() + offset;
73        *self.bounding_box = self.bounding_box.translate(IVec3::new(0, dy, 0));
74        true
75    }
76
77    #[expect(
78        clippy::too_many_arguments,
79        reason = "mirrors vanilla StructurePiece.generateBox parameters"
80    )]
81    pub(super) fn generate_box(
82        &mut self,
83        x0: i32,
84        y0: i32,
85        z0: i32,
86        x1: i32,
87        y1: i32,
88        z1: i32,
89        edge: BlockStateId,
90        fill: BlockStateId,
91        skip_air: bool,
92    ) {
93        for y in y0..=y1 {
94            for x in x0..=x1 {
95                for z in z0..=z1 {
96                    if skip_air && self.get_block(x, y, z).is_air() {
97                        continue;
98                    }
99                    let state = if y != y0 && y != y1 && x != x0 && x != x1 && z != z0 && z != z1 {
100                        fill
101                    } else {
102                        edge
103                    };
104                    self.place_block(state, x, y, z);
105                }
106            }
107        }
108    }
109
110    #[expect(
111        clippy::too_many_arguments,
112        reason = "mirrors vanilla StructurePiece.generateBox selector overload"
113    )]
114    pub(super) fn generate_box_with_selector(
115        &mut self,
116        x0: i32,
117        y0: i32,
118        z0: i32,
119        x1: i32,
120        y1: i32,
121        z1: i32,
122        skip_air: bool,
123        random: &mut WorldgenRandom,
124        mut selector: impl FnMut(&mut WorldgenRandom, i32, i32, i32, bool) -> BlockStateId,
125    ) {
126        for y in y0..=y1 {
127            for x in x0..=x1 {
128                for z in z0..=z1 {
129                    if skip_air && self.get_block(x, y, z).is_air() {
130                        continue;
131                    }
132                    let is_edge = y == y0 || y == y1 || x == x0 || x == x1 || z == z0 || z == z1;
133                    let state = selector(random, x, y, z, is_edge);
134                    self.place_block(state, x, y, z);
135                }
136            }
137        }
138    }
139
140    pub(super) fn generate_air_box(
141        &mut self,
142        x0: i32,
143        y0: i32,
144        z0: i32,
145        x1: i32,
146        y1: i32,
147        z1: i32,
148    ) {
149        let air = vanilla_blocks::AIR.default_state();
150        for y in y0..=y1 {
151            for x in x0..=x1 {
152                for z in z0..=z1 {
153                    self.place_block(air, x, y, z);
154                }
155            }
156        }
157    }
158
159    pub(super) fn fill_column_down(&mut self, state: BlockStateId, x: i32, start_y: i32, z: i32) {
160        let mut pos = self.world_pos(x, start_y, z);
161        if !self.clip.contains_blockpos(pos) {
162            return;
163        }
164
165        while Self::is_replaceable_by_structures(self.region.block_state(pos))
166            && pos.y() > self.region.min_y() + 1
167        {
168            let _ = self
169                .region
170                .set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS);
171            pos = pos.below();
172        }
173    }
174
175    pub(super) fn place_block(&mut self, state: BlockStateId, x: i32, y: i32, z: i32) {
176        let pos = self.world_pos(x, y, z);
177        if !self.clip.contains_blockpos(pos) {
178            return;
179        }
180
181        let state = self.transform_state(state);
182        let _ = self
183            .region
184            .set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS);
185        if StructurePiecePlacer::needs_structure_shape_postprocessing(state) {
186            self.region.mark_pos_for_postprocessing(pos);
187        }
188        let fluid_state = state.get_fluid_state();
189        if !fluid_state.is_empty() {
190            let _ = self
191                .region
192                .schedule_fluid_tick_default(pos, fluid_state.fluid_id, 0);
193        }
194    }
195
196    pub(super) fn create_chest(
197        &mut self,
198        random: &mut WorldgenRandom,
199        x: i32,
200        y: i32,
201        z: i32,
202        loot_table: &'static str,
203    ) -> bool {
204        let pos = self.world_pos(x, y, z);
205        StructurePiecePlacer::create_loot_chest(self.region, self.clip, random, pos, loot_table)
206    }
207
208    pub(super) fn create_dispenser(
209        &mut self,
210        random: &mut WorldgenRandom,
211        x: i32,
212        y: i32,
213        z: i32,
214        facing: Direction,
215        loot_table: &'static str,
216    ) -> bool {
217        let pos = self.world_pos(x, y, z);
218        if !self.clip.contains_blockpos(pos)
219            || self.region.block_state(pos).get_block() == &vanilla_blocks::DISPENSER
220        {
221            return false;
222        }
223
224        let state = self.transform_state(
225            vanilla_blocks::DISPENSER
226                .default_state()
227                .set_value(&BlockStateProperties::FACING, facing),
228        );
229        if !self
230            .region
231            .set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS)
232        {
233            return false;
234        }
235        if StructurePiecePlacer::needs_structure_shape_postprocessing(state) {
236            self.region.mark_pos_for_postprocessing(pos);
237        }
238
239        StructurePiecePlacer::set_loot_table_block_entity(
240            self.region,
241            pos,
242            &vanilla_block_entity_types::DISPENSER,
243            state,
244            loot_table,
245            random.next_i64(),
246        )
247    }
248
249    pub(super) fn create_spawner(
250        &mut self,
251        x: i32,
252        y: i32,
253        z: i32,
254        entity_id: &'static str,
255    ) -> bool {
256        let pos = self.world_pos(x, y, z);
257        if !self.clip.contains_blockpos(pos) {
258            return false;
259        }
260
261        let spawner = vanilla_blocks::SPAWNER.default_state();
262        if !self
263            .region
264            .set_block_state(pos, spawner, UpdateFlags::UPDATE_CLIENTS)
265        {
266            return false;
267        }
268        StructurePiecePlacer::set_spawner_entity(self.region, pos, spawner, entity_id)
269    }
270
271    pub(super) const fn world_pos(&self, x: i32, y: i32, z: i32) -> BlockPos {
272        let world_y = if self.orientation.is_some() {
273            y + self.bounding_box.min_y()
274        } else {
275            y
276        };
277        let (world_x, world_z) = match self.orientation {
278            None | Some(Direction::Up | Direction::Down) => (x, z),
279            Some(Direction::North) => {
280                (self.bounding_box.min_x() + x, self.bounding_box.max_z() - z)
281            }
282            Some(Direction::South) => {
283                (self.bounding_box.min_x() + x, self.bounding_box.min_z() + z)
284            }
285            Some(Direction::West) => (self.bounding_box.max_x() - z, self.bounding_box.min_z() + x),
286            Some(Direction::East) => (self.bounding_box.min_x() + z, self.bounding_box.min_z() + x),
287        };
288        BlockPos::new(world_x, world_y, world_z)
289    }
290
291    pub(super) const fn clip(&self) -> BoundingBox {
292        self.clip
293    }
294
295    pub(super) const fn sea_level(&self) -> i32 {
296        self.region.sea_level()
297    }
298
299    pub(super) fn block_at(&self, x: i32, y: i32, z: i32) -> BlockStateId {
300        self.get_block(x, y, z)
301    }
302
303    pub(super) fn chunk_intersects(&self, x0: i32, z0: i32, x1: i32, z1: i32) -> bool {
304        let pos0 = self.world_pos(x0, 0, z0);
305        let pos1 = self.world_pos(x1, 0, z1);
306        self.clip.intersects_xz(
307            pos0.x().min(pos1.x()),
308            pos0.z().min(pos1.z()),
309            pos0.x().max(pos1.x()),
310            pos0.z().max(pos1.z()),
311        )
312    }
313
314    pub(super) fn weak_world(&self) -> Weak<World> {
315        self.region.weak_world()
316    }
317
318    pub(super) fn add_fresh_entity(&mut self, entity: SharedEntity) -> bool {
319        self.region.add_fresh_entity(entity)
320    }
321
322    fn get_block(&self, x: i32, y: i32, z: i32) -> BlockStateId {
323        let pos = self.world_pos(x, y, z);
324        if self.clip.contains_blockpos(pos) {
325            self.region.block_state(pos)
326        } else {
327            vanilla_blocks::AIR.default_state()
328        }
329    }
330
331    fn transform_state(&self, state: BlockStateId) -> BlockStateId {
332        let (mirror, rotation) = StructurePiecePlacer::orientation_transform(self.orientation);
333        StructureTemplate::transform_state(self.registry, state, mirror, rotation)
334    }
335
336    fn is_replaceable_by_structures(state: BlockStateId) -> bool {
337        state.is_air()
338            || state.has_fluid()
339            || state.get_block() == &vanilla_blocks::GLOW_LICHEN
340            || state.get_block() == &vanilla_blocks::SEAGRASS
341            || state.get_block() == &vanilla_blocks::TALL_SEAGRASS
342    }
343}