1use rand::{Rng, RngExt};
2use steel_macros::block_behavior;
3use steel_registry::blocks::block_state_ext::BlockStateExt;
4use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty, Direction};
5use steel_registry::blocks::shapes::{self, SupportType, is_block_local_face_sturdy};
6use steel_registry::fluid::FluidStateExt;
7use steel_registry::{REGISTRY, vanilla_blocks};
8use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
9
10use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
11use crate::behavior::blocks::multiface_face_property;
12use crate::behavior::context::BlockPlaceContext;
13use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext};
14use crate::fluid::get_fluid_state_from_block;
15use crate::world::{LevelAccessor, LevelReader, ScheduledTickAccess};
16
17use super::BlockRef;
18
19#[block_behavior]
21pub struct MultifaceBlock {
22 block: BlockRef,
23}
24
25pub(super) struct MultifaceSpreader {
26 block: BlockRef,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub(crate) struct MultifaceSpreadPos {
31 pub(crate) pos: BlockPos,
32 pub(crate) face: Direction,
33}
34
35#[derive(Clone, Copy)]
36pub(crate) enum MultifaceSpreadType {
37 SamePosition,
38 SamePlane,
39 WrapAround,
40}
41
42pub(crate) fn multiface_spread_pos(
43 pos: BlockPos,
44 spread_direction: Direction,
45 starting_face: Direction,
46 spread_type: MultifaceSpreadType,
47) -> MultifaceSpreadPos {
48 match spread_type {
49 MultifaceSpreadType::SamePosition => MultifaceSpreadPos {
50 pos,
51 face: spread_direction,
52 },
53 MultifaceSpreadType::SamePlane => MultifaceSpreadPos {
54 pos: pos.relative(spread_direction),
55 face: starting_face,
56 },
57 MultifaceSpreadType::WrapAround => MultifaceSpreadPos {
58 pos: pos.relative(spread_direction).relative(starting_face),
59 face: spread_direction.opposite(),
60 },
61 }
62}
63
64const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
65
66impl MultifaceBlock {
67 #[must_use]
69 pub const fn new(block: BlockRef) -> Self {
70 Self { block }
71 }
72
73 pub fn can_attach_to(
78 world: &dyn LevelReader,
79 pos: BlockPos,
80 direction_to_neighbor: Direction,
81 ) -> bool {
82 let neighbor_pos = pos.relative(direction_to_neighbor);
83 let block_state = world.get_block_state(neighbor_pos);
84 Self::can_attach_to_state(world, direction_to_neighbor, neighbor_pos, block_state)
85 }
86
87 pub(super) fn can_attach_to_state(
90 world: &dyn LevelReader,
91 direction_to_neighbor: Direction,
92 neighbor_pos: BlockPos,
93 neighbor_state: BlockStateId,
94 ) -> bool {
95 let support_direction = direction_to_neighbor.opposite();
96 if neighbor_state.get_block().config.dynamic_shape {
97 let behavior = BLOCK_BEHAVIORS.get_behavior(neighbor_state.get_block());
98 return is_block_local_face_sturdy(
99 &behavior.get_block_support_boxes(neighbor_state, world, neighbor_pos),
100 support_direction,
101 SupportType::Full,
102 ) || is_block_local_face_sturdy(
103 &behavior.get_collision_boxes(
104 neighbor_state,
105 world,
106 neighbor_pos,
107 BlockCollisionContext::empty(),
108 ),
109 support_direction,
110 SupportType::Full,
111 );
112 }
113
114 shapes::is_offset_face_full(
115 neighbor_state.get_support_shape_at(neighbor_pos),
116 support_direction,
117 ) || shapes::is_offset_face_full(
118 neighbor_state.get_collision_shape_at(neighbor_pos),
119 support_direction,
120 )
121 }
122
123 const fn is_face_supported(_face_direction: Direction) -> bool {
124 true
125 }
126
127 fn get_state_for_placement_with_dir(
129 block: BlockRef,
130 old_state: BlockStateId,
131 world: &dyn LevelReader,
132 placement_pos: BlockPos,
133 placement_direction: Direction,
134 ) -> Option<BlockStateId> {
135 if !Self::is_valid_state_for_placement(
136 block,
137 world,
138 old_state,
139 placement_pos,
140 placement_direction,
141 ) {
142 return None;
143 }
144
145 let mut new_state = if old_state.get_block() == block {
146 old_state
147 } else {
148 let fluid_state = get_fluid_state_from_block(old_state);
149 if fluid_state.is_water() && fluid_state.is_source() {
150 block.default_state().set_value(WATERLOGGED, true)
151 } else {
152 block.default_state()
153 }
154 };
155 new_state = new_state.set_value(multiface_face_property(placement_direction), true);
156 Some(new_state)
157 }
158
159 fn is_valid_state_for_placement(
160 block: BlockRef,
161 world: &dyn LevelReader,
162 old_state: BlockStateId,
163 placement_pos: BlockPos,
164 placement_direction: Direction,
165 ) -> bool {
166 if Self::is_face_supported(placement_direction)
167 && (old_state.get_block() != block || !Self::has_face(old_state, placement_direction))
168 {
169 let neighbor_pos = placement_pos.relative(placement_direction);
170 return Self::can_attach_to_state(
171 world,
172 placement_direction,
173 neighbor_pos,
174 world.get_block_state(neighbor_pos),
175 );
176 }
177 false
178 }
179
180 pub(super) fn has_face(state: BlockStateId, direction: Direction) -> bool {
181 state
182 .try_get_value(multiface_face_property(direction))
183 .unwrap_or(false)
184 }
185
186 fn has_any_face(state: BlockStateId) -> bool {
187 Direction::ALL
188 .iter()
189 .any(|direction| Self::has_face(state, *direction))
190 }
191
192 fn has_any_vacant_face(state: BlockStateId) -> bool {
193 Direction::ALL
194 .iter()
195 .any(|direction| !Self::has_face(state, *direction))
196 }
197
198 fn remove_face(state: BlockStateId, property: &BoolProperty) -> BlockStateId {
199 let new_state = state.set_value(property, false);
200 if Self::has_any_face(new_state) {
201 return new_state;
202 }
203 vanilla_blocks::AIR.default_state()
204 }
205}
206
207impl MultifaceSpreader {
208 const SPREAD_TYPES: [MultifaceSpreadType; 3] = [
209 MultifaceSpreadType::SamePosition,
210 MultifaceSpreadType::SamePlane,
211 MultifaceSpreadType::WrapAround,
212 ];
213
214 pub(super) const fn new(block: BlockRef) -> Self {
215 Self { block }
216 }
217
218 pub(super) fn can_spread_in_any_direction(
219 &self,
220 state: BlockStateId,
221 world: &dyn LevelReader,
222 pos: BlockPos,
223 starting_face: Direction,
224 ) -> bool {
225 Direction::ALL.iter().any(|spread_direction| {
226 self.get_spread_from_face_toward_direction(
227 state,
228 world,
229 pos,
230 starting_face,
231 *spread_direction,
232 )
233 .is_some()
234 })
235 }
236
237 pub(super) fn spread_from_random_face_toward_random_direction<W: LevelAccessor>(
238 &self,
239 state: BlockStateId,
240 world: &W,
241 pos: BlockPos,
242 rng: &mut dyn Rng,
243 ) -> Option<MultifaceSpreadPos> {
244 let mut faces = Direction::ALL;
245 Self::shuffle_directions(&mut faces, rng);
246
247 for starting_face in faces {
248 if !MultifaceBlock::has_face(state, starting_face) {
249 continue;
250 }
251
252 let mut spread_directions = Direction::ALL;
253 Self::shuffle_directions(&mut spread_directions, rng);
254 for spread_direction in spread_directions {
255 if let Some(spread_pos) = self.spread_from_face_toward_direction(
256 state,
257 world,
258 pos,
259 starting_face,
260 spread_direction,
261 ) {
262 return Some(spread_pos);
263 }
264 }
265 }
266
267 None
268 }
269
270 pub(super) fn spread_from_face_toward_direction<W: LevelAccessor>(
271 &self,
272 state: BlockStateId,
273 world: &W,
274 pos: BlockPos,
275 starting_face: Direction,
276 spread_direction: Direction,
277 ) -> Option<MultifaceSpreadPos> {
278 let spread_pos = self.get_spread_from_face_toward_direction(
279 state,
280 world,
281 pos,
282 starting_face,
283 spread_direction,
284 )?;
285 let old_state = world.get_block_state(spread_pos.pos);
286 let spread_state = MultifaceBlock::get_state_for_placement_with_dir(
287 self.block,
288 old_state,
289 world,
290 spread_pos.pos,
291 spread_pos.face,
292 )?;
293
294 world
295 .set_block_state(spread_pos.pos, spread_state, UpdateFlags::UPDATE_CLIENTS)
296 .then_some(spread_pos)
297 }
298
299 fn get_spread_from_face_toward_direction(
300 &self,
301 state: BlockStateId,
302 world: &dyn LevelReader,
303 pos: BlockPos,
304 starting_face: Direction,
305 spread_direction: Direction,
306 ) -> Option<MultifaceSpreadPos> {
307 if spread_direction.axis() == starting_face.axis()
308 || !MultifaceBlock::has_face(state, starting_face)
309 || MultifaceBlock::has_face(state, spread_direction)
310 {
311 return None;
312 }
313
314 Self::SPREAD_TYPES.iter().find_map(|spread_type| {
315 let spread_pos =
316 multiface_spread_pos(pos, spread_direction, starting_face, *spread_type);
317 self.can_spread_into(world, spread_pos)
318 .then_some(spread_pos)
319 })
320 }
321
322 fn can_spread_into(&self, world: &dyn LevelReader, spread_pos: MultifaceSpreadPos) -> bool {
323 let old_state = world.get_block_state(spread_pos.pos);
324 let fluid_state = get_fluid_state_from_block(old_state);
325 let replaceable = old_state.is_air()
326 || old_state.get_block() == self.block
327 || (fluid_state.is_water() && fluid_state.is_source());
328
329 replaceable
330 && MultifaceBlock::is_valid_state_for_placement(
331 self.block,
332 world,
333 old_state,
334 spread_pos.pos,
335 spread_pos.face,
336 )
337 }
338
339 fn shuffle_directions(directions: &mut [Direction; 6], rng: &mut dyn Rng) {
340 for i in (1..directions.len()).rev() {
341 let j = rng.random_range(0..=i);
342 directions.swap(i, j);
343 }
344 }
345}
346
347impl BlockBehavior for MultifaceBlock {
348 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
349 let mut has_at_least_one_face = false;
350 for direction in Direction::ALL {
351 if Self::has_face(state, direction) {
352 if !MultifaceBlock::can_attach_to(world, pos, direction) {
353 return false;
354 }
355 has_at_least_one_face = true;
356 }
357 }
358 has_at_least_one_face
359 }
360
361 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
362 let level = context.world;
363 let old_state = level.get_block_state(context.place_pos());
364
365 context
366 .get_nearest_looking_directions()
367 .iter()
368 .find_map(|direction| {
369 MultifaceBlock::get_state_for_placement_with_dir(
370 self.block,
371 old_state,
372 level,
373 context.place_pos(),
374 *direction,
375 )
376 })
377 }
378
379 fn update_shape(
380 &self,
381 state: BlockStateId,
382 world: &dyn ScheduledTickAccess,
383 pos: BlockPos,
384 direction: Direction,
385 neighbor_pos: BlockPos,
386 neighbor_state: BlockStateId,
387 ) -> BlockStateId {
388 schedule_water_tick_if_waterlogged(state, world, pos);
389
390 if !MultifaceBlock::has_any_face(state) {
391 return vanilla_blocks::AIR.default_state();
392 }
393
394 if Self::has_face(state, direction)
395 && !Self::can_attach_to_state(world, direction, neighbor_pos, neighbor_state)
396 {
397 return Self::remove_face(state, multiface_face_property(direction));
398 }
399 state
400 }
401
402 fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
403 !context.with_item(|item| item.item() == REGISTRY.items.by_block(state.get_block()))
404 || Self::has_any_vacant_face(state)
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use steel_registry::init_vanilla_registry;
411
412 use super::*;
413 use crate::behavior::init_behaviors;
414 use crate::test_support::TestLevel;
415
416 const NORTH: &BoolProperty = &BlockStateProperties::NORTH;
417 #[test]
418 fn update_shape_uses_supplied_neighbor_state_and_schedules_water_first() {
419 init_vanilla_registry();
420 init_behaviors();
421
422 let behavior = MultifaceBlock::new(&vanilla_blocks::GLOW_LICHEN);
423 let pos = BlockPos::new(0, 64, 0);
424 let state = vanilla_blocks::GLOW_LICHEN
425 .default_state()
426 .set_value(NORTH, true)
427 .set_value(WATERLOGGED, true);
428 let level =
429 TestLevel::default().with_block(pos.north(), vanilla_blocks::STONE.default_state());
430
431 let updated = behavior.update_shape(
432 state,
433 &level,
434 pos,
435 Direction::North,
436 pos.north(),
437 vanilla_blocks::AIR.default_state(),
438 );
439
440 assert!(updated.is_air());
441 assert!(level.scheduled_water_tick());
442 }
443}