steel_core/behavior/blocks/building/
brushable_block.rs1use 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#[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 #[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}