Skip to main content

steel_core/behavior/blocks/redstone/tripwire/
block.rs

1//! Vanilla tripwire sensing, line updates, and shears disarming.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::properties::{BoolProperty, EnumProperty};
7use steel_registry::blocks::{
8    BlockRef, block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
9    properties::Direction, shapes::VoxelShape,
10};
11use steel_registry::{vanilla_game_events, vanilla_items};
12use steel_utils::{
13    BlockPos, BlockStateId,
14    axis::Axis,
15    types::{InteractionHand, UpdateFlags},
16};
17
18use super::TripWireHookBlock;
19use crate::behavior::{BlockBehavior, BlockPlaceContext};
20use crate::entity::{Entity, InsideBlockEffectCollector};
21use crate::player::Player;
22use crate::world::{LevelReader, ScheduledTickAccess, World, game_event::GameEventContext};
23
24const WIRE_DISTANCE_MAX: i32 = 42;
25const RECHECK_PERIOD: i32 = 10;
26
27/// Vanilla `TripWireBlock` behavior.
28#[block_behavior]
29pub struct TripWireBlock {
30    block: BlockRef,
31    #[json_arg(vanilla_blocks)]
32    hook: BlockRef,
33}
34
35const DISARMED: &BoolProperty = &BlockStateProperties::DISARMED;
36const EAST: &BoolProperty = &BlockStateProperties::EAST;
37const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
38const NORTH: &BoolProperty = &BlockStateProperties::NORTH;
39const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
40const SOUTH: &BoolProperty = &BlockStateProperties::SOUTH;
41const WEST: &BoolProperty = &BlockStateProperties::WEST;
42
43impl TripWireBlock {
44    /// Creates tripwire behavior.
45    #[must_use]
46    pub const fn new(block: BlockRef, hook: BlockRef) -> Self {
47        Self { block, hook }
48    }
49
50    fn should_connect_to(&self, state: BlockStateId, direction: Direction) -> bool {
51        if state.get_block() == self.hook {
52            state.get_value(HORIZONTAL_FACING) == direction.opposite()
53        } else {
54            state.get_block() == self.block
55        }
56    }
57
58    fn set_connection(state: BlockStateId, direction: Direction, connected: bool) -> BlockStateId {
59        match direction {
60            Direction::North => state.set_value(NORTH, connected),
61            Direction::East => state.set_value(EAST, connected),
62            Direction::South => state.set_value(SOUTH, connected),
63            Direction::West => state.set_value(WEST, connected),
64            Direction::Up | Direction::Down => state,
65        }
66    }
67
68    fn update_source(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
69        for direction in [Direction::South, Direction::West] {
70            for distance in 1..WIRE_DISTANCE_MAX {
71                let test_pos = pos.relative_n(direction, distance);
72                let test_state = world.get_block_state(test_pos);
73                if test_state.get_block() == self.hook {
74                    if test_state.get_value(HORIZONTAL_FACING) == direction.opposite() {
75                        TripWireHookBlock::calculate_state(
76                            world,
77                            test_pos,
78                            test_state,
79                            false,
80                            true,
81                            distance,
82                            Some(state),
83                        );
84                    }
85                    break;
86                }
87                if test_state.get_block() != self.block {
88                    break;
89                }
90            }
91        }
92    }
93
94    fn check_pressed_for_entity(&self, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
95        self.set_pressed(world, pos, !entity.is_ignoring_block_triggers());
96    }
97
98    fn check_pressed(&self, world: &Arc<World>, pos: BlockPos) {
99        let state = world.get_block_state(pos);
100        let Some(local_bounds) = state.get_outline_shape_at(pos).bounds() else {
101            self.set_pressed(world, pos, false);
102            return;
103        };
104        let bounds = local_bounds.at_block(pos);
105        let should_be_pressed = !world
106            .get_entities_in_aabb_matching(&bounds, |entity| !entity.is_ignoring_block_triggers())
107            .is_empty();
108        self.set_pressed(world, pos, should_be_pressed);
109    }
110
111    fn set_pressed(&self, world: &Arc<World>, pos: BlockPos, should_be_pressed: bool) {
112        let mut state = world.get_block_state(pos);
113        let was_pressed = state.get_value(POWERED);
114        if should_be_pressed != was_pressed {
115            state = state.set_value(POWERED, should_be_pressed);
116            world.set_block(pos, state, UpdateFlags::UPDATE_ALL);
117            self.update_source(world, pos, state);
118        }
119
120        if should_be_pressed {
121            world.schedule_block_tick_default(pos, self.block, RECHECK_PERIOD);
122        } else if was_pressed {
123            world.schedule_block_tick_default(pos, self.block, 0);
124        }
125    }
126}
127
128impl BlockBehavior for TripWireBlock {
129    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
130        let pos = context.place_pos();
131        let mut state = self.block.default_state();
132        for direction in [
133            Direction::North,
134            Direction::East,
135            Direction::South,
136            Direction::West,
137        ] {
138            state = Self::set_connection(
139                state,
140                direction,
141                self.should_connect_to(
142                    context.world.get_block_state(pos.relative(direction)),
143                    direction,
144                ),
145            );
146        }
147        Some(state)
148    }
149
150    fn update_shape(
151        &self,
152        state: BlockStateId,
153        _world: &dyn ScheduledTickAccess,
154        _pos: BlockPos,
155        direction: Direction,
156        _neighbor_pos: BlockPos,
157        neighbor_state: BlockStateId,
158    ) -> BlockStateId {
159        if direction.axis() == Axis::Y {
160            state
161        } else {
162            Self::set_connection(
163                state,
164                direction,
165                self.should_connect_to(neighbor_state, direction),
166            )
167        }
168    }
169
170    fn on_place(
171        &self,
172        state: BlockStateId,
173        world: &Arc<World>,
174        pos: BlockPos,
175        old_state: BlockStateId,
176        _moved_by_piston: bool,
177    ) {
178        if old_state.get_block() != self.block {
179            self.update_source(world, pos, state);
180        }
181    }
182
183    fn affect_neighbors_after_removal(
184        &self,
185        state: BlockStateId,
186        world: &Arc<World>,
187        pos: BlockPos,
188        moved_by_piston: bool,
189    ) {
190        if !moved_by_piston {
191            self.update_source(world, pos, state.set_value(POWERED, true));
192        }
193    }
194
195    fn player_will_destroy(
196        &self,
197        state: BlockStateId,
198        world: &Arc<World>,
199        pos: BlockPos,
200        player: &Player,
201    ) -> BlockStateId {
202        let held_shears = {
203            let inventory = player.inventory.lock();
204            let main_hand = inventory.get_item_in_hand(InteractionHand::MainHand);
205            !main_hand.is_empty() && main_hand.is(&vanilla_items::SHEARS)
206        };
207        if held_shears {
208            world.set_block(
209                pos,
210                state.set_value(DISARMED, true),
211                UpdateFlags::UPDATE_NONE,
212            );
213            world.game_event(
214                &vanilla_game_events::SHEAR,
215                pos,
216                &GameEventContext::new(Some(player), None),
217            );
218        }
219        state
220    }
221
222    fn get_entity_inside_collision_shape(
223        &self,
224        state: BlockStateId,
225        _world: &dyn LevelReader,
226        _pos: BlockPos,
227        _entity: &dyn Entity,
228    ) -> VoxelShape {
229        state.get_static_outline_shape()
230    }
231
232    fn entity_inside(
233        &self,
234        state: BlockStateId,
235        world: &Arc<World>,
236        pos: BlockPos,
237        entity: &dyn Entity,
238        _effect_collector: &mut InsideBlockEffectCollector,
239        _is_precise: bool,
240    ) {
241        if !state.get_value(POWERED) && !world.has_scheduled_block_tick(pos, self.block) {
242            self.check_pressed_for_entity(world, pos, entity);
243        }
244    }
245
246    fn tick(&self, _state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
247        if world.get_block_state(pos).get_value(POWERED) {
248            self.check_pressed(world, pos);
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::sync::Arc;
256
257    use glam::DVec3;
258    use steel_registry::init_vanilla_registry;
259    use steel_registry::{vanilla_blocks, vanilla_entities};
260    use steel_utils::ChunkPos;
261
262    use super::*;
263    use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
264    use crate::entity::SharedEntity;
265    use crate::test_support::{TestEntity, fresh_test_world, insert_ready_full_chunk};
266
267    #[test]
268    fn entity_inside_powers_wire_and_attached_hooks() {
269        init_vanilla_registry();
270        init_behaviors();
271        let world = fresh_test_world("tripwire_entity_inside");
272        let left = BlockPos::new(5, 64, 8);
273        let wire_pos = left.east();
274        let right = wire_pos.east();
275        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(left));
276        assert!(world.set_block(
277            left.west(),
278            vanilla_blocks::STONE.default_state(),
279            UpdateFlags::UPDATE_NONE,
280        ));
281        assert!(world.set_block(
282            right.east(),
283            vanilla_blocks::STONE.default_state(),
284            UpdateFlags::UPDATE_NONE,
285        ));
286        let left_state = vanilla_blocks::TRIPWIRE_HOOK
287            .default_state()
288            .set_value(HORIZONTAL_FACING, Direction::East);
289        let right_state = vanilla_blocks::TRIPWIRE_HOOK
290            .default_state()
291            .set_value(HORIZONTAL_FACING, Direction::West);
292        assert!(world.set_block(left, left_state, UpdateFlags::UPDATE_NONE));
293        assert!(world.set_block(right, right_state, UpdateFlags::UPDATE_NONE));
294        assert!(world.set_block(
295            wire_pos,
296            vanilla_blocks::TRIPWIRE.default_state(),
297            UpdateFlags::UPDATE_NONE,
298        ));
299        TripWireHookBlock::calculate_state(&world, left, left_state, false, false, -1, None);
300
301        let entity: SharedEntity = TestEntity::shared(
302            7_002,
303            DVec3::new(6.5, 64.0, 8.5),
304            Arc::downgrade(&world),
305            &vanilla_entities::PIG,
306        );
307        let mut effects = InsideBlockEffectCollector::new();
308        BLOCK_BEHAVIORS
309            .get_behavior(&vanilla_blocks::TRIPWIRE)
310            .entity_inside(
311                world.get_block_state(wire_pos),
312                &world,
313                wire_pos,
314                entity.as_ref(),
315                &mut effects,
316                true,
317            );
318
319        assert!(world.get_block_state(wire_pos).get_value(POWERED));
320        assert!(world.get_block_state(left).get_value(POWERED));
321        assert!(world.get_block_state(right).get_value(POWERED));
322        assert!(world.has_scheduled_block_tick(wire_pos, &vanilla_blocks::TRIPWIRE));
323    }
324}