Skip to main content

steel_core/behavior/blocks/building/
brushable_block.rs

1//! Brushable block behavior for suspicious sand and suspicious gravel.
2
3use std::sync::{Arc, Weak};
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{BlockStateProperties, Direction, IntProperty};
9use steel_registry::sound_event::SoundEventRef;
10use steel_registry::{vanilla_block_entity_types, vanilla_game_events};
11use steel_utils::Downcast as _;
12use steel_utils::{BlockPos, BlockStateId};
13
14use crate::behavior::blocks::FallingBlock;
15use crate::behavior::{
16    BlockBehavior, BlockEntityCreation, BlockPlaceContext, BrushableData, Fallable,
17};
18use crate::block_entity::BLOCK_ENTITIES;
19use crate::block_entity::entities::BrushableBlockEntity;
20use crate::entity::entities::FallingBlockEntity;
21use crate::entity::{Entity as _, EntityEventSource as _};
22use crate::world::game_event::GameEventContext;
23use crate::world::{ScheduledTickAccess, World};
24
25/// Vanilla archaeology block behavior for suspicious sand and suspicious gravel.
26#[block_behavior]
27pub struct BrushableBlock {
28    block: BlockRef,
29    #[json_arg(vanilla_blocks, json = "turns_into")]
30    turns_into: BlockRef,
31    #[json_arg(sound_events, json = "brush_sound")]
32    brush_sound: SoundEventRef,
33    #[json_arg(sound_events, json = "brush_completed_sound")]
34    brush_completed_sound: SoundEventRef,
35}
36
37const DUSTED: &IntProperty = &BlockStateProperties::DUSTED;
38
39impl BrushableBlock {
40    /// Creates a brushable block behavior from extracted vanilla block arguments.
41    #[must_use]
42    pub const fn new(
43        block: BlockRef,
44        turns_into: BlockRef,
45        brush_sound: SoundEventRef,
46        brush_completed_sound: SoundEventRef,
47    ) -> Self {
48        Self {
49            block,
50            turns_into,
51            brush_sound,
52            brush_completed_sound,
53        }
54    }
55}
56
57impl BlockBehavior for BrushableBlock {
58    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
59        Some(self.block.default_state().set_value(DUSTED, 0))
60    }
61
62    fn on_place(
63        &self,
64        _state: BlockStateId,
65        world: &Arc<World>,
66        pos: BlockPos,
67        _old_state: BlockStateId,
68        _moved_by_piston: bool,
69    ) {
70        world.schedule_block_tick_default(pos, self.block, 2);
71    }
72
73    fn update_shape(
74        &self,
75        state: BlockStateId,
76        world: &dyn ScheduledTickAccess,
77        pos: BlockPos,
78        _direction: Direction,
79        _neighbor_pos: BlockPos,
80        _neighbor_state: BlockStateId,
81    ) -> BlockStateId {
82        let _ = world.schedule_block_tick_default(pos, self.block, 2);
83        state
84    }
85
86    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
87        if let Some(block_entity) = world.get_block_entity(pos)
88            && let Some(brushable) = block_entity.downcast_ref::<BrushableBlockEntity>()
89        {
90            let mutation = brushable.check_reset(world);
91            mutation.apply(world, pos);
92        }
93
94        if let Some(entity) = FallingBlock::tick(state, world, pos) {
95            entity.disable_drop();
96        }
97    }
98
99    fn new_block_entity(
100        &self,
101        level: Weak<World>,
102        pos: BlockPos,
103        state: BlockStateId,
104    ) -> BlockEntityCreation {
105        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
106            &vanilla_block_entity_types::BRUSHABLE_BLOCK,
107            level,
108            pos,
109            state,
110        ))
111    }
112
113    fn should_keep_block_entity(&self, old_state: BlockStateId, new_state: BlockStateId) -> bool {
114        old_state.get_block() == new_state.get_block()
115    }
116
117    fn brushable_data(&self, _state: BlockStateId) -> Option<BrushableData> {
118        Some(BrushableData {
119            turns_into: self.turns_into,
120            brush_sound: self.brush_sound,
121            brush_completed_sound: self.brush_completed_sound,
122        })
123    }
124
125    fn as_fallable(&self) -> Option<&dyn Fallable> {
126        Some(self)
127    }
128}
129
130impl Fallable for BrushableBlock {
131    fn on_broken_after_fall(
132        &self,
133        world: &Arc<World>,
134        _pos: BlockPos,
135        entity: &FallingBlockEntity,
136    ) {
137        let center = entity.bounding_box().center();
138        world.destroy_block_effect(
139            BlockPos::from(center),
140            u32::from(entity.block_state().0),
141            None,
142        );
143        world.game_event_at(
144            &vanilla_game_events::BLOCK_DESTROY,
145            center,
146            &GameEventContext::new(
147                Some(entity.as_entity_event_source()),
148                Some(entity.block_state()),
149            ),
150        );
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use steel_registry::blocks::BlockRef;
157    use steel_registry::{init_vanilla_registry, vanilla_blocks};
158    use steel_utils::types::UpdateFlags;
159    use steel_utils::{BlockPos, ChunkPos, WorldAabb};
160
161    use super::*;
162    use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
163    use crate::block_entity::init_block_entities;
164    use crate::entity::SharedEntity;
165    use crate::entity::entities::ItemEntity;
166    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
167
168    fn brushable_test_world(key: &'static str) -> Arc<World> {
169        init_vanilla_registry();
170        init_behaviors();
171        init_block_entities();
172        let world = fresh_test_world(key);
173        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
174        world
175    }
176
177    fn spawn_from_scheduled_tick(
178        world: &Arc<World>,
179        block: BlockRef,
180        pos: BlockPos,
181    ) -> SharedEntity {
182        let state = block.default_state();
183        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_ALL));
184        BLOCK_BEHAVIORS.get_behavior(block).tick(state, world, pos);
185
186        let query = WorldAabb::new(
187            f64::from(pos.x()),
188            f64::from(pos.y()),
189            f64::from(pos.z()),
190            f64::from(pos.x() + 1),
191            f64::from(pos.y() + 1),
192            f64::from(pos.z() + 1),
193        );
194        let Some(entity) = world
195            .get_entities_in_aabb(&query)
196            .into_iter()
197            .find(|entity| {
198                entity
199                    .as_ref()
200                    .downcast_ref::<FallingBlockEntity>()
201                    .is_some()
202            })
203        else {
204            panic!("brushable block tick should spawn a falling block entity");
205        };
206        entity
207    }
208
209    fn tick_until_removed(entity: &SharedEntity) {
210        for _ in 0..240 {
211            if entity.is_removed() {
212                return;
213            }
214            entity.set_old_position_to_current();
215            entity.advance_tick_count();
216            entity.tick();
217        }
218        panic!("falling brushable block did not settle within the test limit");
219    }
220
221    #[test]
222    fn suspicious_sand_and_gravel_fall_then_break_without_drops() {
223        let world = brushable_test_world("brushable_blocks_fall");
224
225        for (x, block) in [
226            (4, &vanilla_blocks::SUSPICIOUS_SAND),
227            (8, &vanilla_blocks::SUSPICIOUS_GRAVEL),
228        ] {
229            let ground = BlockPos::new(x, 64, 4);
230            assert!(world.set_block(
231                ground,
232                vanilla_blocks::STONE.default_state(),
233                UpdateFlags::UPDATE_ALL,
234            ));
235            let entity = spawn_from_scheduled_tick(&world, block, BlockPos::new(x, 72, 4));
236
237            let Some(falling) = entity.as_ref().downcast_ref::<FallingBlockEntity>() else {
238                panic!("spawned entity should be a falling block");
239            };
240            assert_eq!(falling.block_state(), block.default_state());
241
242            tick_until_removed(&entity);
243            assert!(world.get_block_state(ground.above()).is_air());
244
245            let query = WorldAabb::new(
246                f64::from(ground.x() - 2),
247                f64::from(ground.y()),
248                f64::from(ground.z() - 2),
249                f64::from(ground.x() + 3),
250                f64::from(ground.y() + 4),
251                f64::from(ground.z() + 3),
252            );
253            assert!(
254                world
255                    .get_entities_in_aabb(&query)
256                    .iter()
257                    .all(|entity| { entity.as_ref().downcast_ref::<ItemEntity>().is_none() })
258            );
259        }
260    }
261}