Skip to main content

steel_core/behavior/blocks/redstone/
observer_block.rs

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