1use std::sync::Arc;
9
10use steel_macros::block_behavior;
11use steel_registry::blocks::BlockRef;
12use steel_registry::blocks::block_state_ext::BlockStateExt;
13use steel_registry::blocks::properties::{BlockStateProperties, Direction};
14use steel_registry::sound_event::SoundEventRef;
15use steel_registry::vanilla_game_events;
16use steel_utils::types::UpdateFlags;
17use steel_utils::{BlockPos, BlockStateId};
18
19use super::face_attached_horizontal_directional_block::FaceAttachedHorizontalDirectionalBlock;
20use crate::behavior::InventoryAccess;
21use crate::behavior::block::BlockBehavior;
22use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
23use crate::entity::{Entity, InsideBlockEffectCollector, SharedEntity};
24use crate::player::Player;
25use crate::world::{
26 LevelReader, ScheduledTickAccess, SignalQueryContext, World, game_event::GameEventContext,
27};
28
29#[block_behavior]
34pub struct ButtonBlock {
35 face_attached: FaceAttachedHorizontalDirectionalBlock,
36 #[json_arg(value)]
37 ticks_to_stay_pressed: i32,
38 #[json_arg(value, json = "type_can_button_be_activated_by_arrows")]
39 arrow_sensitive: bool,
40 #[json_arg(sound_events, json = "type_button_click_on")]
41 sound_click_on: SoundEventRef,
42 #[json_arg(sound_events, json = "type_button_click_off")]
43 sound_click_off: SoundEventRef,
44}
45
46impl ButtonBlock {
47 #[must_use]
51 pub const fn new(
52 block: BlockRef,
53 ticks_to_stay_pressed: i32,
54 arrow_sensitive: bool,
55 sound_click_on: SoundEventRef,
56 sound_click_off: SoundEventRef,
57 ) -> Self {
58 Self {
59 face_attached: FaceAttachedHorizontalDirectionalBlock::new(block),
60 ticks_to_stay_pressed,
61 arrow_sensitive,
62 sound_click_on,
63 sound_click_off,
64 }
65 }
66
67 fn update_button_neighbors(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
71 world.update_neighbors_at(pos, self.face_attached.block);
72 let support_dir =
73 FaceAttachedHorizontalDirectionalBlock::connected_direction(state).opposite();
74 let support_pos = support_dir.relative(pos);
75 world.update_neighbors_at(support_pos, self.face_attached.block);
76 }
77
78 fn press(
81 &self,
82 state: BlockStateId,
83 world: &Arc<World>,
84 pos: BlockPos,
85 player: Option<&Player>,
86 ) {
87 let powered_state = state.set_value(&BlockStateProperties::POWERED, true);
88 world.set_block(pos, powered_state, UpdateFlags::UPDATE_ALL);
89 self.update_button_neighbors(powered_state, world, pos);
90 world.schedule_block_tick_default(
91 pos,
92 self.face_attached.block,
93 self.ticks_to_stay_pressed,
94 );
95 world.play_block_sound(self.sound_click_on, pos, 1.0, 1.0, player.map(Player::id));
96 world.game_event(
97 &vanilla_game_events::BLOCK_ACTIVATE,
98 pos,
99 &GameEventContext::new(player.map(|player| player as &dyn Entity), None),
100 );
101 }
102
103 fn first_arrow(
104 &self,
105 state: BlockStateId,
106 world: &World,
107 pos: BlockPos,
108 ) -> Option<SharedEntity> {
109 if !self.arrow_sensitive {
110 return None;
111 }
112 let bounds = state.get_outline_shape_at(pos).bounds()?.at_block(pos);
113 world
114 .get_entities_in_aabb_matching(&bounds, Entity::is_abstract_arrow)
115 .into_iter()
116 .next()
117 }
118
119 fn check_pressed(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
120 let first_arrow = self.first_arrow(state, world, pos);
121 let should_be_pressed = first_arrow.is_some();
122 let was_pressed = state.get_value(&BlockStateProperties::POWERED);
123 if should_be_pressed != was_pressed {
124 world.set_block(
125 pos,
126 state.set_value(&BlockStateProperties::POWERED, should_be_pressed),
127 UpdateFlags::UPDATE_ALL,
128 );
129 self.update_button_neighbors(state, world, pos);
130 world.play_block_sound(
131 if should_be_pressed {
132 self.sound_click_on
133 } else {
134 self.sound_click_off
135 },
136 pos,
137 1.0,
138 1.0,
139 None,
140 );
141 world.game_event(
142 if should_be_pressed {
143 &vanilla_game_events::BLOCK_ACTIVATE
144 } else {
145 &vanilla_game_events::BLOCK_DEACTIVATE
146 },
147 pos,
148 &GameEventContext::new(first_arrow.as_deref(), None),
149 );
150 }
151
152 if should_be_pressed {
153 world.schedule_block_tick_default(
154 pos,
155 self.face_attached.block,
156 self.ticks_to_stay_pressed,
157 );
158 }
159 }
160}
161
162impl BlockBehavior for ButtonBlock {
163 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
165 FaceAttachedHorizontalDirectionalBlock::can_survive(state, world, pos)
166 }
167
168 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
169 self.face_attached.state_for_placement(context)
170 }
171
172 fn update_shape(
173 &self,
174 state: BlockStateId,
175 world: &dyn ScheduledTickAccess,
176 pos: BlockPos,
177 direction: Direction,
178 _neighbor_pos: BlockPos,
179 _neighbor_state: BlockStateId,
180 ) -> BlockStateId {
181 FaceAttachedHorizontalDirectionalBlock::update_shape(state, world, pos, direction)
182 }
183
184 fn use_without_item(
185 &self,
186 state: BlockStateId,
187 world: &Arc<World>,
188 pos: BlockPos,
189 player: &Player,
190 _hit_result: &BlockHitResult,
191 _inv: &mut InventoryAccess,
192 ) -> InteractionResult {
193 let powered: bool = state.get_value(&BlockStateProperties::POWERED);
194 if powered {
195 return InteractionResult::Consume;
196 }
197 self.press(state, world, pos, Some(player));
198 InteractionResult::Success
199 }
200
201 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
202 let powered: bool = state.get_value(&BlockStateProperties::POWERED);
203 if !powered {
204 return;
205 }
206 self.check_pressed(state, world, pos);
207 }
208
209 fn entity_inside(
210 &self,
211 state: BlockStateId,
212 world: &Arc<World>,
213 pos: BlockPos,
214 _entity: &dyn Entity,
215 _effect_collector: &mut InsideBlockEffectCollector,
216 _is_precise: bool,
217 ) {
218 if self.arrow_sensitive && !state.get_value(&BlockStateProperties::POWERED) {
219 self.check_pressed(state, world, pos);
220 }
221 }
222
223 fn affect_neighbors_after_removal(
224 &self,
225 state: BlockStateId,
226 world: &Arc<World>,
227 pos: BlockPos,
228 moved_by_piston: bool,
229 ) {
230 if moved_by_piston {
231 return;
232 }
233 let powered: bool = state.get_value(&BlockStateProperties::POWERED);
234 if !powered {
235 return;
236 }
237 self.update_button_neighbors(state, world, pos);
238 }
239
240 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
241 true
242 }
243
244 fn get_own_signal(
245 &self,
246 state: BlockStateId,
247 _world: &dyn LevelReader,
248 _pos: BlockPos,
249 _context: SignalQueryContext,
250 ) -> i32 {
251 if state.get_value(&BlockStateProperties::POWERED) {
252 15
253 } else {
254 0
255 }
256 }
257
258 fn get_direct_signal(
259 &self,
260 state: BlockStateId,
261 _world: &dyn LevelReader,
262 _pos: BlockPos,
263 direction: Direction,
264 _context: SignalQueryContext,
265 ) -> i32 {
266 if state.get_value(&BlockStateProperties::POWERED)
267 && FaceAttachedHorizontalDirectionalBlock::connected_direction(state) == direction
268 {
269 15
270 } else {
271 0
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use std::sync::Arc;
279
280 use glam::DVec3;
281 use steel_registry::init_vanilla_registry;
282 use steel_registry::{vanilla_blocks, vanilla_entities};
283 use steel_utils::ChunkPos;
284
285 use super::*;
286 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
287 use crate::entity::entities::RawEntity;
288 use crate::entity::{InsideBlockEffectCollector, SharedEntity};
289 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
290
291 #[test]
292 fn wooden_button_stays_pressed_while_arrow_intersects_its_shape() {
293 init_vanilla_registry();
294 init_behaviors();
295 let world = fresh_test_world("wooden_button_arrow");
296 let pos = BlockPos::new(8, 64, 8);
297 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
298 let state = vanilla_blocks::OAK_BUTTON.default_state();
299 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
300
301 let bounds = state
302 .get_outline_shape_at(pos)
303 .bounds()
304 .expect("button outline should be non-empty")
305 .at_block(pos);
306 let arrow_pos = DVec3::new(
307 f64::midpoint(bounds.min_x(), bounds.max_x()),
308 bounds.min_y(),
309 f64::midpoint(bounds.min_z(), bounds.max_z()),
310 );
311 let arrow: SharedEntity = Arc::new(RawEntity::new(
312 7_001,
313 arrow_pos,
314 Arc::downgrade(&world),
315 &vanilla_entities::ARROW,
316 ));
317 world
318 .try_add_entity(Arc::clone(&arrow))
319 .expect("test arrow should enter loaded chunk");
320
321 let mut effects = InsideBlockEffectCollector::new();
322 BLOCK_BEHAVIORS
323 .get_behavior(&vanilla_blocks::OAK_BUTTON)
324 .entity_inside(state, &world, pos, arrow.as_ref(), &mut effects, true);
325
326 assert!(
327 world
328 .get_block_state(pos)
329 .get_value(&BlockStateProperties::POWERED)
330 );
331 assert!(world.has_scheduled_block_tick(pos, &vanilla_blocks::OAK_BUTTON));
332 }
333}