steel_core/behavior/blocks/redstone/pressure_plate/
base.rs1use 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
21pub(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 }
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}