Skip to main content

steel_core/behavior/blocks/building/
scaffolding_block.rs

1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::blocks::{
5    BlockRef,
6    block_state_ext::BlockStateExt,
7    properties::{BlockStateProperties, BoolProperty, Direction, IntProperty},
8    shapes::VoxelShape,
9};
10use steel_registry::{REGISTRY, vanilla_blocks};
11use steel_utils::types::UpdateFlags;
12use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId};
13
14use crate::behavior::{
15    BlockBehavior, BlockCollisionContext, BlockPlaceContext,
16    block::schedule_water_tick_if_waterlogged,
17};
18use crate::entity::entities::FallingBlockEntity;
19use crate::world::{LevelReader, ScheduledTickAccess, World};
20
21const SHAPE_STABLE_BOXES: &[BlockLocalAabb] = &[
22    BlockLocalAabb::new(0.0, 0.875, 0.0, 1.0, 1.0, 1.0),
23    BlockLocalAabb::new(0.0, 0.0, 0.0, 0.125, 1.0, 0.125),
24    BlockLocalAabb::new(0.875, 0.0, 0.0, 1.0, 1.0, 0.125),
25    BlockLocalAabb::new(0.0, 0.0, 0.875, 0.125, 1.0, 1.0),
26    BlockLocalAabb::new(0.875, 0.0, 0.875, 1.0, 1.0, 1.0),
27];
28const SHAPE_UNSTABLE_BOTTOM_BOXES: &[BlockLocalAabb] =
29    &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.125, 1.0)];
30const SHAPE_BELOW_BLOCK_BOXES: &[BlockLocalAabb] =
31    &[BlockLocalAabb::new(0.0, -1.0, 0.0, 1.0, 0.0, 1.0)];
32
33const SHAPE_STABLE: VoxelShape = VoxelShape::from_boxes(SHAPE_STABLE_BOXES);
34const SHAPE_UNSTABLE_BOTTOM: VoxelShape = VoxelShape::from_boxes(SHAPE_UNSTABLE_BOTTOM_BOXES);
35const SHAPE_BELOW_BLOCK: VoxelShape = VoxelShape::from_boxes(SHAPE_BELOW_BLOCK_BOXES);
36
37const TICK_DELAY: i32 = 1;
38
39/// Vanilla scaffolding placement, stability, falling, and collision behavior.
40#[block_behavior]
41pub struct ScaffoldingBlock {
42    block: BlockRef,
43}
44
45const BOTTOM: &BoolProperty = &BlockStateProperties::BOTTOM;
46const STABILITY_DISTANCE: &IntProperty = &BlockStateProperties::STABILITY_DISTANCE;
47const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
48
49impl ScaffoldingBlock {
50    pub(crate) const STABILITY_MAX_DISTANCE: u8 = 7;
51
52    /// Creates a scaffolding block behavior.
53    #[must_use]
54    pub const fn new(block: BlockRef) -> Self {
55        Self { block }
56    }
57
58    fn is_bottom(&self, world: &dyn LevelReader, pos: BlockPos, distance: u8) -> bool {
59        distance > 0 && world.get_block_state(pos.below()).get_block() != self.block
60    }
61
62    /// Vanilla `ScaffoldingBlock.getDistance`.
63    pub(crate) fn get_distance(world: &dyn LevelReader, pos: BlockPos) -> u8 {
64        let below_pos = pos.below();
65        let below_state = world.get_block_state(below_pos);
66        let mut distance = Self::STABILITY_MAX_DISTANCE;
67        if below_state.get_block() == &vanilla_blocks::SCAFFOLDING {
68            distance = below_state.get_value(STABILITY_DISTANCE);
69        } else if world.is_face_sturdy(below_state, below_pos, Direction::Up) {
70            return 0;
71        }
72
73        for direction in Direction::HORIZONTAL {
74            let neighbor_state = world.get_block_state(pos.relative(direction));
75            if neighbor_state.get_block() == &vanilla_blocks::SCAFFOLDING {
76                distance = distance.min(neighbor_state.get_value(STABILITY_DISTANCE) + 1);
77                if distance == 1 {
78                    break;
79                }
80            }
81        }
82        distance
83    }
84}
85
86impl BlockBehavior for ScaffoldingBlock {
87    fn update_shape(
88        &self,
89        state: BlockStateId,
90        world: &dyn ScheduledTickAccess,
91        pos: BlockPos,
92        _direction: Direction,
93        _neighbor_pos: BlockPos,
94        _neighbor_state: BlockStateId,
95    ) -> BlockStateId {
96        schedule_water_tick_if_waterlogged(state, world, pos);
97        world.schedule_block_tick_default(pos, self.block, TICK_DELAY);
98        state
99    }
100
101    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
102        let pos = context.place_pos();
103        let distance = Self::get_distance(context.world.as_ref(), pos);
104        Some(
105            self.block
106                .default_state()
107                .set_value(WATERLOGGED, context.is_water_source())
108                .set_value(STABILITY_DISTANCE, distance)
109                .set_value(
110                    BOTTOM,
111                    self.is_bottom(context.world.as_ref(), pos, distance),
112                ),
113        )
114    }
115
116    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
117        Self::get_distance(world, pos) < Self::STABILITY_MAX_DISTANCE
118    }
119
120    fn can_be_replaced(&self, _state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
121        context.with_item(|item| item.item() == REGISTRY.items.by_block(self.block))
122    }
123
124    fn on_place(
125        &self,
126        _state: BlockStateId,
127        world: &Arc<World>,
128        pos: BlockPos,
129        _old_state: BlockStateId,
130        _moved_by_piston: bool,
131    ) {
132        let _ = world.schedule_block_tick_default(pos, self.block, TICK_DELAY);
133    }
134
135    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
136        let distance = Self::get_distance(world.as_ref(), pos);
137        let new_state = state
138            .set_value(STABILITY_DISTANCE, distance)
139            .set_value(BOTTOM, self.is_bottom(world.as_ref(), pos, distance));
140
141        if distance == Self::STABILITY_MAX_DISTANCE {
142            if state.get_value(STABILITY_DISTANCE) == Self::STABILITY_MAX_DISTANCE {
143                let _ = FallingBlockEntity::fall(world, pos, new_state);
144            } else {
145                let _ = world.destroy_block(pos, true);
146            }
147        } else if state != new_state {
148            let _ = world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
149        }
150    }
151
152    fn get_collision_shape(
153        &self,
154        state: BlockStateId,
155        _world: &dyn LevelReader,
156        pos: BlockPos,
157        context: BlockCollisionContext,
158    ) -> VoxelShape {
159        if context.is_placement() {
160            return VoxelShape::EMPTY;
161        }
162
163        if context.is_above(VoxelShape::FULL_BLOCK, pos, true) && !context.is_descending() {
164            return SHAPE_STABLE;
165        }
166
167        let distance = state.get_value(STABILITY_DISTANCE);
168        let bottom = state.get_value(BOTTOM);
169        if distance != 0 && bottom && context.is_above(SHAPE_BELOW_BLOCK, pos, true) {
170            SHAPE_UNSTABLE_BOTTOM
171        } else {
172            VoxelShape::EMPTY
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_entities, vanilla_fluids};
181    use steel_utils::{ChunkPos, WorldAabb};
182
183    use crate::behavior::init_behaviors;
184    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
185
186    fn scaffolding_state(distance: u8, bottom: bool) -> BlockStateId {
187        vanilla_blocks::SCAFFOLDING
188            .default_state()
189            .set_value(STABILITY_DISTANCE, distance)
190            .set_value(BOTTOM, bottom)
191    }
192
193    fn collision_shape(state: BlockStateId, context: BlockCollisionContext) -> VoxelShape {
194        let behavior = ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING);
195        let level = TestLevel::default().with_min_y(0);
196        behavior.get_collision_shape(state, &level, BlockPos::new(0, 64, 0), context)
197    }
198
199    #[test]
200    fn placement_context_has_no_scaffolding_collision() {
201        init_vanilla_registry();
202
203        let shape = collision_shape(
204            scaffolding_state(0, false),
205            BlockCollisionContext::with_position(65.0, false),
206        );
207
208        assert_eq!(shape, VoxelShape::EMPTY);
209    }
210
211    #[test]
212    fn entity_above_scaffolding_collides_with_stable_shape() {
213        init_vanilla_registry();
214
215        let shape = collision_shape(
216            scaffolding_state(0, false),
217            BlockCollisionContext::entity(65.0, false),
218        );
219
220        assert_eq!(shape, SHAPE_STABLE);
221    }
222
223    #[test]
224    fn descending_entity_only_collides_with_unstable_bottom_shape() {
225        init_vanilla_registry();
226
227        let shape = collision_shape(
228            scaffolding_state(1, true),
229            BlockCollisionContext::entity(64.5, true),
230        );
231
232        assert_eq!(shape, SHAPE_UNSTABLE_BOTTOM);
233    }
234
235    #[test]
236    fn non_bottom_descending_scaffolding_has_empty_collision() {
237        init_vanilla_registry();
238
239        let shape = collision_shape(
240            scaffolding_state(1, false),
241            BlockCollisionContext::entity(64.5, true),
242        );
243
244        assert_eq!(shape, VoxelShape::EMPTY);
245    }
246
247    #[test]
248    fn shape_update_schedules_stability_and_water_ticks() {
249        init_vanilla_registry();
250        let behavior = ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING);
251        let state = vanilla_blocks::SCAFFOLDING
252            .default_state()
253            .set_value(WATERLOGGED, true);
254        let pos = BlockPos::new(0, 64, 0);
255        let level = TestLevel::default();
256
257        assert_eq!(
258            behavior.update_shape(
259                state,
260                &level,
261                pos,
262                Direction::North,
263                pos.north(),
264                vanilla_blocks::AIR.default_state(),
265            ),
266            state
267        );
268        assert_eq!(
269            level
270                .scheduled_block_ticks
271                .borrow()
272                .iter()
273                .map(|tick| (tick.pos, tick.block, tick.delay))
274                .collect::<Vec<_>>(),
275            vec![(pos, &vanilla_blocks::SCAFFOLDING, 1)]
276        );
277        assert_eq!(
278            level
279                .scheduled_fluid_ticks
280                .borrow()
281                .iter()
282                .map(|tick| (tick.pos, tick.fluid, tick.delay))
283                .collect::<Vec<_>>(),
284            vec![(pos, &vanilla_fluids::WATER, 5)]
285        );
286    }
287
288    #[test]
289    fn distance_uses_sturdy_vertical_and_nearest_horizontal_support() {
290        init_vanilla_registry();
291        let pos = BlockPos::new(0, 64, 0);
292        let level = TestLevel::default().with_min_y(0);
293
294        assert_eq!(
295            ScaffoldingBlock::get_distance(&level, pos),
296            ScaffoldingBlock::STABILITY_MAX_DISTANCE
297        );
298
299        level.set_test_block(pos.below(), vanilla_blocks::STONE.default_state());
300        assert_eq!(ScaffoldingBlock::get_distance(&level, pos), 0);
301
302        level.set_test_block(pos.below(), scaffolding_state(4, false));
303        assert_eq!(ScaffoldingBlock::get_distance(&level, pos), 4);
304
305        level.set_test_block(pos.below(), vanilla_blocks::AIR.default_state());
306        level.set_test_block(pos.east(), scaffolding_state(4, true));
307        level.set_test_block(pos.west(), scaffolding_state(2, true));
308        assert_eq!(ScaffoldingBlock::get_distance(&level, pos), 3);
309    }
310
311    #[test]
312    fn newly_unsupported_scaffolding_breaks_and_drops_an_item() {
313        init_vanilla_registry();
314        init_behaviors();
315        let world = fresh_test_world("scaffolding_destroy_tick");
316        let pos = BlockPos::new(8, 64, 8);
317        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
318        let state = scaffolding_state(ScaffoldingBlock::STABILITY_MAX_DISTANCE - 1, true);
319        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
320
321        ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING).tick(state, &world, pos);
322
323        assert!(world.get_block_state(pos).is_air());
324        let entities =
325            world.get_entities_in_aabb(&WorldAabb::new(7.0, 63.0, 7.0, 10.0, 67.0, 10.0));
326        assert!(
327            entities
328                .iter()
329                .any(|entity| entity.entity_type() == &vanilla_entities::ITEM)
330        );
331    }
332
333    #[test]
334    fn max_distance_waterlogged_scaffolding_falls_and_leaves_water() {
335        init_vanilla_registry();
336        init_behaviors();
337        let world = fresh_test_world("scaffolding_fall_tick");
338        let pos = BlockPos::new(8, 64, 8);
339        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
340        let state = scaffolding_state(ScaffoldingBlock::STABILITY_MAX_DISTANCE, true)
341            .set_value(WATERLOGGED, true);
342        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
343
344        ScaffoldingBlock::new(&vanilla_blocks::SCAFFOLDING).tick(state, &world, pos);
345
346        assert_eq!(
347            world.get_block_state(pos),
348            vanilla_blocks::WATER.default_state()
349        );
350        let entities =
351            world.get_entities_in_aabb(&WorldAabb::new(7.0, 63.0, 7.0, 10.0, 67.0, 10.0));
352        assert!(
353            entities
354                .iter()
355                .any(|entity| entity.entity_type() == &vanilla_entities::FALLING_BLOCK)
356        );
357    }
358}