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}