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