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