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