Skip to main content

steel_core/behavior/blocks/vegetation/
multiface_block.rs

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/// Behavior for vanilla multiface blocks.
20#[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    /// Creates a new multiface block behavior.
68    #[must_use]
69    pub const fn new(block: BlockRef) -> Self {
70        Self { block }
71    }
72
73    /// Returns whether a face of the multiface block at `pos` can attach to the
74    /// neighbour in `direction_to_neighbor`. The neighbour position is derived
75    /// here, so callers that already have it should use
76    /// [`can_attach_to_state`](Self::can_attach_to_state).
77    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    /// Returns whether `neighbor_state` has a full face pointing back at the
88    /// attaching block, checking the support shape before the collision shape.
89    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    /// Vanilla `MultifaceBlock.get_state_for_placement()`
128    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}