steel_core/behavior/blocks/redstone/
observer_block.rs1use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{
9 BlockStateProperties, BoolProperty, Direction, EnumProperty,
10};
11use steel_utils::types::UpdateFlags;
12use steel_utils::{BlockPos, BlockStateId};
13
14use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
15use crate::behavior::{BlockBehavior, BlockPlaceContext};
16use crate::world::{LevelReader, ScheduledTickAccess, SignalQueryContext, World};
17
18const PULSE_DELAY: i32 = 2;
19const PLACEMENT_RESET_FLAGS: UpdateFlags =
20 UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
21
22#[block_behavior]
24pub struct ObserverBlock {
25 block: BlockRef,
26}
27
28const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
29const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
30
31impl ObserverBlock {
32 #[must_use]
34 pub const fn new(block: BlockRef) -> Self {
35 Self { block }
36 }
37
38 fn start_signal(&self, ticks: &dyn ScheduledTickAccess, pos: BlockPos) {
39 if !ticks.has_scheduled_block_tick(pos, self.block) {
40 ticks.schedule_block_tick_default(pos, self.block, PULSE_DELAY);
41 }
42 }
43
44 fn update_neighbors_in_front(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
45 let direction = state.get_value(FACING);
46 let output_pos = pos.relative(direction.opposite());
47 world.neighbor_changed(output_pos, self.block);
48 world.update_neighbors_at_except_from_facing(output_pos, self.block, direction);
49 }
50
51 fn own_signal(state: BlockStateId) -> i32 {
52 if state.get_value(POWERED) {
53 MAX_REDSTONE_SIGNAL
54 } else {
55 MIN_REDSTONE_SIGNAL
56 }
57 }
58}
59
60impl BlockBehavior for ObserverBlock {
61 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
62 Some(
63 self.block
64 .default_state()
65 .set_value(FACING, context.get_nearest_looking_direction()),
66 )
67 }
68
69 fn update_shape(
70 &self,
71 state: BlockStateId,
72 world: &dyn ScheduledTickAccess,
73 pos: BlockPos,
74 direction: Direction,
75 _neighbor_pos: BlockPos,
76 _neighbor_state: BlockStateId,
77 ) -> BlockStateId {
78 if state.get_value(FACING) == direction && !state.get_value(POWERED) {
79 self.start_signal(world, pos);
80 }
81 state
82 }
83
84 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
85 if state.get_value(POWERED) {
86 world.set_block(
87 pos,
88 state.set_value(POWERED, false),
89 UpdateFlags::UPDATE_CLIENTS,
90 );
91 } else {
92 world.set_block(
93 pos,
94 state.set_value(POWERED, true),
95 UpdateFlags::UPDATE_CLIENTS,
96 );
97 world.schedule_block_tick_default(pos, self.block, PULSE_DELAY);
98 }
99 self.update_neighbors_in_front(world, pos, state);
100 }
101
102 fn on_place(
103 &self,
104 state: BlockStateId,
105 world: &Arc<World>,
106 pos: BlockPos,
107 old_state: BlockStateId,
108 _moved_by_piston: bool,
109 ) {
110 if state.get_block() == old_state.get_block()
111 || !state.get_value(POWERED)
112 || world.has_scheduled_block_tick(pos, self.block)
113 {
114 return;
115 }
116
117 let reset_state = state.set_value(POWERED, false);
118 world.set_block(pos, reset_state, PLACEMENT_RESET_FLAGS);
119 self.update_neighbors_in_front(world, pos, reset_state);
120 }
121
122 fn affect_neighbors_after_removal(
123 &self,
124 state: BlockStateId,
125 world: &Arc<World>,
126 pos: BlockPos,
127 _moved_by_piston: bool,
128 ) {
129 if state.get_value(POWERED) && world.has_scheduled_block_tick(pos, self.block) {
130 self.update_neighbors_in_front(world, pos, state.set_value(POWERED, false));
131 }
132 }
133
134 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
135 true
136 }
137
138 fn get_own_signal(
139 &self,
140 state: BlockStateId,
141 _world: &dyn LevelReader,
142 _pos: BlockPos,
143 _context: SignalQueryContext,
144 ) -> i32 {
145 Self::own_signal(state)
146 }
147
148 fn get_signal(
149 &self,
150 state: BlockStateId,
151 _world: &dyn LevelReader,
152 _pos: BlockPos,
153 direction: Direction,
154 _context: SignalQueryContext,
155 ) -> i32 {
156 if state.get_value(FACING) == direction {
157 Self::own_signal(state)
158 } else {
159 0
160 }
161 }
162
163 fn get_direct_signal(
164 &self,
165 state: BlockStateId,
166 world: &dyn LevelReader,
167 pos: BlockPos,
168 direction: Direction,
169 context: SignalQueryContext,
170 ) -> i32 {
171 self.get_signal(state, world, pos, direction, context)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use steel_registry::init_vanilla_registry;
178 use steel_registry::vanilla_blocks;
179
180 use super::*;
181 use crate::test_support::TestLevel;
182
183 fn observer_state(facing: Direction, powered: bool) -> BlockStateId {
184 vanilla_blocks::OBSERVER
185 .default_state()
186 .set_value(FACING, facing)
187 .set_value(POWERED, powered)
188 }
189
190 #[test]
191 fn observed_face_update_schedules_one_two_tick_pulse() {
192 init_vanilla_registry();
193 let observer = ObserverBlock::new(&vanilla_blocks::OBSERVER);
194 let pos = BlockPos::new(0, 64, 0);
195 let state = observer_state(Direction::East, false);
196 let level = TestLevel::default();
197
198 observer.update_shape(
199 state,
200 &level,
201 pos,
202 Direction::East,
203 pos.east(),
204 vanilla_blocks::STONE.default_state(),
205 );
206 let scheduled = level.scheduled_block_ticks.borrow();
207 assert_eq!(scheduled.len(), 1);
208 assert_eq!(scheduled[0].pos, pos);
209 assert_eq!(scheduled[0].block, &vanilla_blocks::OBSERVER);
210 assert_eq!(scheduled[0].delay, PULSE_DELAY);
211 }
212
213 #[test]
214 fn observer_output_is_powered_and_directional() {
215 init_vanilla_registry();
216 let observer = ObserverBlock::new(&vanilla_blocks::OBSERVER);
217 let state = observer_state(Direction::Down, true);
218 let level = TestLevel::default();
219 let pos = BlockPos::new(0, 64, 0);
220
221 assert_eq!(
222 observer.get_signal(
223 state,
224 &level,
225 pos,
226 Direction::Down,
227 SignalQueryContext::DEFAULT,
228 ),
229 15
230 );
231 assert_eq!(
232 observer.get_signal(
233 state,
234 &level,
235 pos,
236 Direction::Up,
237 SignalQueryContext::DEFAULT,
238 ),
239 0
240 );
241 }
242}