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    /// Vanilla `MultifaceBlock.canAttachTo(level, pos, direction)`.
74    ///
75    /// Returns whether a face of the multiface block at `pos` can attach to the
76    /// neighbour in `direction_to_neighbor`. The neighbour position is derived
77    /// here, so callers that already have it should use
78    /// [`can_attach_to_state`](Self::can_attach_to_state).
79    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    /// Vanilla `MultifaceBlock.canAttachTo(level, directionTowardsNeighbor, neighborPos, neighborState)`.
90    ///
91    /// Returns whether `neighbor_state` has a full face pointing back at the
92    /// attaching block, checking the support shape before the collision shape.
93    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    /// Vanilla `MultifaceBlock.get_state_for_placement()`
132    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    /// Vanilla `MultifaceBlock.canSurvive`.
353    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    /// Vanilla `MultifaceBlock.getStateForPlacement`
367    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    /// Vanilla `MultifaceBlock.updateShape`
386    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    /// Vanilla `MultifaceBlock.canBeReplaced`
410    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}