Skip to main content

steel_core/behavior/blocks/redstone/pressure_plate/
base.rs

1//! Shared vanilla pressure-plate behavior.
2
3use std::sync::Arc;
4
5use steel_registry::blocks::BlockRef;
6use steel_registry::blocks::properties::Direction;
7use steel_registry::blocks::shapes::SupportType;
8use steel_registry::sound_event::SoundEventRef;
9use steel_registry::{vanilla_blocks, vanilla_game_events};
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId, WorldAabb};
12
13use crate::behavior::BlockPlaceContext;
14use crate::entity::Entity;
15use crate::world::game_event::GameEventContext;
16use crate::world::{LevelAccessor, LevelReader, World};
17
18const TOUCH_INSET: f64 = 1.0 / 16.0;
19const TOUCH_HEIGHT: f64 = 4.0 / 16.0;
20
21/// Common server-side behavior inherited from vanilla's `BasePressurePlateBlock`.
22pub(super) struct BasePressurePlateBlock {
23    pub(super) block: BlockRef,
24}
25
26impl BasePressurePlateBlock {
27    #[must_use]
28    pub(super) const fn new(block: BlockRef) -> Self {
29        Self { block }
30    }
31
32    pub(super) fn can_survive(level: &dyn LevelReader, pos: BlockPos) -> bool {
33        let below_pos = pos.below();
34        let below_state = level.get_block_state(below_pos);
35        level.is_face_sturdy_for(below_state, below_pos, Direction::Up, SupportType::Rigid)
36            || level.is_face_sturdy_for(below_state, below_pos, Direction::Up, SupportType::Center)
37    }
38
39    pub(super) fn state_for_placement(
40        &self,
41        context: &BlockPlaceContext<'_>,
42    ) -> Option<BlockStateId> {
43        let state = self.block.default_state();
44        Self::can_survive(context.world.as_ref(), context.place_pos()).then_some(state)
45    }
46
47    pub(super) fn update_shape(
48        state: BlockStateId,
49        level: &dyn LevelReader,
50        pos: BlockPos,
51        direction: Direction,
52    ) -> BlockStateId {
53        if direction == Direction::Down && !Self::can_survive(level, pos) {
54            vanilla_blocks::AIR.default_state()
55        } else {
56            state
57        }
58    }
59
60    fn update_neighbors(&self, world: &Arc<World>, pos: BlockPos) {
61        world.update_neighbors_at(pos, self.block);
62        world.update_neighbors_at(pos.below(), self.block);
63    }
64
65    pub(super) fn affect_neighbors_after_removal(
66        &self,
67        world: &Arc<World>,
68        pos: BlockPos,
69        moved_by_piston: bool,
70        signal: i32,
71    ) {
72        if !moved_by_piston && signal > 0 {
73            self.update_neighbors(world, pos);
74        }
75    }
76
77    pub(super) fn entity_count(
78        world: &World,
79        pos: BlockPos,
80        mut class_filter: impl FnMut(&dyn Entity) -> bool,
81    ) -> usize {
82        let min_x = f64::from(pos.x()) + TOUCH_INSET;
83        let min_y = f64::from(pos.y());
84        let min_z = f64::from(pos.z()) + TOUCH_INSET;
85        let bounds = WorldAabb::new(
86            min_x,
87            min_y,
88            min_z,
89            f64::from(pos.x() + 1) - TOUCH_INSET,
90            min_y + TOUCH_HEIGHT,
91            f64::from(pos.z() + 1) - TOUCH_INSET,
92        );
93        world
94            .get_entities_in_aabb_matching(&bounds, |entity| {
95                !entity.is_spectator()
96                    && !entity.is_ignoring_block_triggers()
97                    && class_filter(entity)
98            })
99            .len()
100    }
101
102    fn emit_transition_effects(
103        level: &dyn LevelAccessor,
104        source_entity: Option<&dyn Entity>,
105        pos: BlockPos,
106        is_pressed: bool,
107        sound_click_on: SoundEventRef,
108        sound_click_off: SoundEventRef,
109    ) {
110        level.play_block_sound(
111            if is_pressed {
112                sound_click_on
113            } else {
114                sound_click_off
115            },
116            pos,
117            1.0,
118            1.0,
119            None,
120        );
121        level.game_event(
122            if is_pressed {
123                &vanilla_game_events::BLOCK_ACTIVATE
124            } else {
125                &vanilla_game_events::BLOCK_DEACTIVATE
126            },
127            pos,
128            &GameEventContext::new(source_entity, None),
129        );
130    }
131
132    #[expect(
133        clippy::too_many_arguments,
134        reason = "arguments mirror vanilla checkPressed state and variant hooks"
135    )]
136    pub(super) fn check_pressed(
137        &self,
138        source_entity: Option<&dyn Entity>,
139        world: &Arc<World>,
140        pos: BlockPos,
141        old_signal: i32,
142        signal: i32,
143        new_state: BlockStateId,
144        pressed_time: i32,
145        sound_click_on: SoundEventRef,
146        sound_click_off: SoundEventRef,
147    ) {
148        let was_pressed = old_signal > 0;
149        let is_pressed = signal > 0;
150        if old_signal != signal {
151            world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
152            self.update_neighbors(world, pos);
153            // Vanilla's `setBlocksDirty` is client rendering bookkeeping and
154            // is a server-side no-op.
155        }
156
157        if is_pressed != was_pressed {
158            Self::emit_transition_effects(
159                world,
160                source_entity,
161                pos,
162                is_pressed,
163                sound_click_on,
164                sound_click_off,
165            );
166        }
167
168        if is_pressed {
169            world.schedule_block_tick_default(pos, self.block, pressed_time);
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use std::sync::Weak;
177
178    use glam::DVec3;
179    use steel_registry::{
180        init_vanilla_registry, sound_events, vanilla_entities, vanilla_game_events,
181    };
182
183    use super::*;
184    use crate::entity::entities::RawEntity;
185    use crate::test_support::TestLevel;
186
187    #[test]
188    fn redstone_transition_side_effects_match_vanilla_for_pressure_plates() {
189        init_vanilla_registry();
190        let level = TestLevel::default();
191        let source = RawEntity::new(42, DVec3::ZERO, Weak::new(), &vanilla_entities::ARMOR_STAND);
192        let pos = BlockPos::new(3, 64, -2);
193
194        BasePressurePlateBlock::emit_transition_effects(
195            &level,
196            Some(&source),
197            pos,
198            true,
199            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_ON,
200            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_OFF,
201        );
202        BasePressurePlateBlock::emit_transition_effects(
203            &level,
204            None,
205            pos,
206            false,
207            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_ON,
208            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_OFF,
209        );
210
211        let sounds = level.block_sounds.borrow();
212        assert_eq!(sounds.len(), 2);
213        assert_eq!(
214            sounds[0].sound,
215            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_ON
216        );
217        assert_eq!(sounds[0].exclude, None);
218        assert_eq!(
219            sounds[1].sound,
220            &sound_events::BLOCK_STONE_PRESSURE_PLATE_CLICK_OFF
221        );
222        assert_eq!(sounds[1].exclude, None);
223
224        let events = level.game_events.borrow();
225        assert_eq!(events.len(), 2);
226        assert_eq!(events[0].event, &vanilla_game_events::BLOCK_ACTIVATE);
227        assert_eq!(events[0].source_entity_id, Some(42));
228        assert_eq!(events[1].event, &vanilla_game_events::BLOCK_DEACTIVATE);
229        assert_eq!(events[1].source_entity_id, None);
230    }
231}