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::{
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/// Vanilla `ObserverBlock` two-game-tick pulse behavior.
23#[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    /// Creates observer behavior for `block`.
33    #[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}