Skip to main content

steel_core/behavior/blocks/redstone/diode/
repeater.rs

1//! Vanilla redstone repeater 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, IntProperty,
10};
11use steel_registry::{REGISTRY, vanilla_blocks};
12use steel_utils::types::UpdateFlags;
13use steel_utils::{BlockPos, BlockStateId};
14
15use super::base::DiodeBlock;
16use crate::behavior::blocks::redstone::MAX_REDSTONE_SIGNAL;
17use crate::behavior::{
18    BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
19    PlacementSource,
20};
21use crate::player::Player;
22use crate::world::{LevelReader, ScheduledTickAccess, SignalQueryContext, World};
23
24/// Vanilla `RepeaterBlock`, including side locking and scheduled pulse behavior.
25#[block_behavior]
26pub struct RepeaterBlock {
27    diode: DiodeBlock,
28}
29
30const DELAY: &IntProperty = &BlockStateProperties::DELAY;
31const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
32const LOCKED: &BoolProperty = &BlockStateProperties::LOCKED;
33
34impl RepeaterBlock {
35    /// Creates a repeater behavior.
36    #[must_use]
37    pub const fn new(block: BlockRef) -> Self {
38        Self {
39            diode: DiodeBlock::new(block),
40        }
41    }
42
43    fn delay(state: BlockStateId) -> i32 {
44        i32::from(state.get_value(DELAY)) * 2
45    }
46
47    fn is_locked_at(level: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> bool {
48        DiodeBlock::get_alternate_signal(level, pos, state, true) > 0
49    }
50
51    fn should_turn_on(level: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> bool {
52        DiodeBlock::get_input_signal(level, pos, state) > 0
53    }
54
55    fn check_tick_on_neighbor(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
56        self.diode.check_tick_on_neighbor(
57            world,
58            pos,
59            state,
60            Self::is_locked_at(world.as_ref(), pos, state),
61            Self::should_turn_on(world.as_ref(), pos, state),
62            Self::delay(state),
63        );
64    }
65}
66
67impl BlockBehavior for RepeaterBlock {
68    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
69        DiodeBlock::can_survive(world, pos)
70    }
71
72    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
73        let state = self.diode.state_for_placement(context);
74        Some(state.set_value(
75            LOCKED,
76            Self::is_locked_at(context.world.as_ref(), context.place_pos(), state),
77        ))
78    }
79
80    fn update_shape(
81        &self,
82        state: BlockStateId,
83        world: &dyn ScheduledTickAccess,
84        pos: BlockPos,
85        direction: Direction,
86        neighbor_pos: BlockPos,
87        neighbor_state: BlockStateId,
88    ) -> BlockStateId {
89        if direction == Direction::Down
90            && !DiodeBlock::can_survive_on(world, neighbor_pos, neighbor_state)
91        {
92            return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
93        }
94
95        let facing = state.get_value(HORIZONTAL_FACING);
96        if direction.get_axis() == facing.get_axis() {
97            state
98        } else {
99            state.set_value(LOCKED, Self::is_locked_at(world, pos, state))
100        }
101    }
102
103    fn use_without_item(
104        &self,
105        state: BlockStateId,
106        world: &Arc<World>,
107        pos: BlockPos,
108        player: &Player,
109        _hit_result: &BlockHitResult,
110        _inv: &mut InventoryAccess,
111    ) -> InteractionResult {
112        if !player.abilities.lock().may_build {
113            return InteractionResult::Pass;
114        }
115
116        let delay = state.get_value(DELAY);
117        let next_delay = if delay == 4 { 1 } else { delay + 1 };
118        world.set_block(
119            pos,
120            state.set_value(DELAY, next_delay),
121            UpdateFlags::UPDATE_ALL,
122        );
123        InteractionResult::Success
124    }
125
126    fn handle_neighbor_changed(
127        &self,
128        state: BlockStateId,
129        world: &Arc<World>,
130        pos: BlockPos,
131        _source_block: BlockRef,
132        _moved_by_piston: bool,
133    ) {
134        self.diode.handle_neighbor_changed(state, world, pos, || {
135            self.check_tick_on_neighbor(state, world, pos);
136        });
137    }
138
139    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
140        self.diode.tick(
141            state,
142            world,
143            pos,
144            Self::is_locked_at(world.as_ref(), pos, state),
145            Self::should_turn_on(world.as_ref(), pos, state),
146            Self::delay(state),
147        );
148    }
149
150    fn set_placed_by(
151        &self,
152        state: BlockStateId,
153        world: &Arc<World>,
154        pos: BlockPos,
155        _source: &PlacementSource<'_>,
156    ) {
157        self.diode
158            .set_placed_by(world, pos, Self::should_turn_on(world.as_ref(), pos, state));
159    }
160
161    fn on_place(
162        &self,
163        state: BlockStateId,
164        world: &Arc<World>,
165        pos: BlockPos,
166        _old_state: BlockStateId,
167        _moved_by_piston: bool,
168    ) {
169        self.diode.on_place(state, world, pos);
170    }
171
172    fn affect_neighbors_after_removal(
173        &self,
174        state: BlockStateId,
175        world: &Arc<World>,
176        pos: BlockPos,
177        moved_by_piston: bool,
178    ) {
179        self.diode
180            .affect_neighbors_after_removal(state, world, pos, moved_by_piston);
181    }
182
183    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
184        true
185    }
186
187    fn is_diode(&self) -> bool {
188        true
189    }
190
191    fn get_own_signal(
192        &self,
193        state: BlockStateId,
194        _world: &dyn LevelReader,
195        _pos: BlockPos,
196        _context: SignalQueryContext,
197    ) -> i32 {
198        DiodeBlock::own_signal(state, MAX_REDSTONE_SIGNAL)
199    }
200
201    fn get_signal(
202        &self,
203        state: BlockStateId,
204        _world: &dyn LevelReader,
205        _pos: BlockPos,
206        direction: Direction,
207        _context: SignalQueryContext,
208    ) -> i32 {
209        DiodeBlock::signal(state, direction, MAX_REDSTONE_SIGNAL)
210    }
211
212    fn get_direct_signal(
213        &self,
214        state: BlockStateId,
215        world: &dyn LevelReader,
216        pos: BlockPos,
217        direction: Direction,
218        context: SignalQueryContext,
219    ) -> i32 {
220        self.get_signal(state, world, pos, direction, context)
221    }
222
223    // `animateTick` emits client-local dust particles only.
224}
225
226#[cfg(test)]
227mod tests {
228    use steel_registry::init_vanilla_registry;
229
230    use super::*;
231    use crate::behavior::init_behaviors;
232    use crate::test_support::TestLevel;
233    use crate::world::tick_scheduler::TickPriority;
234
235    const POWER: &IntProperty = &BlockStateProperties::POWER;
236    const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
237
238    fn repeater() -> RepeaterBlock {
239        init_vanilla_registry();
240        init_behaviors();
241        RepeaterBlock::new(&vanilla_blocks::REPEATER)
242    }
243
244    fn repeater_state(facing: Direction, powered: bool) -> BlockStateId {
245        vanilla_blocks::REPEATER
246            .default_state()
247            .set_value(HORIZONTAL_FACING, facing)
248            .set_value(POWERED, powered)
249    }
250
251    #[test]
252    fn input_reads_wire_power_in_front() {
253        let _repeater = repeater();
254        let pos = BlockPos::new(0, 64, 0);
255        let state = repeater_state(Direction::East, false);
256        let wire = vanilla_blocks::REDSTONE_WIRE
257            .default_state()
258            .set_value(POWER, 7);
259        let level = TestLevel::default().with_block(pos.east(), wire);
260
261        assert_eq!(DiodeBlock::get_input_signal(&level, pos, state), 7);
262    }
263
264    #[test]
265    fn side_lock_accepts_only_another_diode() {
266        let _repeater = repeater();
267        let pos = BlockPos::new(0, 64, 0);
268        let state = repeater_state(Direction::North, false);
269        let side_repeater = repeater_state(Direction::East, true);
270        let level = TestLevel::default().with_block(pos.east(), side_repeater);
271        assert!(RepeaterBlock::is_locked_at(&level, pos, state));
272
273        let redstone_block_level = TestLevel::default()
274            .with_block(pos.east(), vanilla_blocks::REDSTONE_BLOCK.default_state());
275        assert!(!RepeaterBlock::is_locked_at(
276            &redstone_block_level,
277            pos,
278            state
279        ));
280    }
281
282    #[test]
283    fn powered_output_is_directional() {
284        let repeater = repeater();
285        let state = repeater_state(Direction::West, true);
286        let level = TestLevel::default();
287        let pos = BlockPos::new(0, 64, 0);
288
289        assert_eq!(
290            repeater.get_signal(
291                state,
292                &level,
293                pos,
294                Direction::West,
295                SignalQueryContext::DEFAULT,
296            ),
297            15
298        );
299        assert_eq!(
300            repeater.get_signal(
301                state,
302                &level,
303                pos,
304                Direction::East,
305                SignalQueryContext::DEFAULT,
306            ),
307            0
308        );
309    }
310
311    #[test]
312    fn support_requires_vanilla_rigid_face() {
313        let repeater = repeater();
314        let pos = BlockPos::new(0, 64, 0);
315        let stone =
316            TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
317        let air = TestLevel::default();
318
319        assert!(repeater.can_survive(vanilla_blocks::REPEATER.default_state(), &stone, pos));
320        assert!(!repeater.can_survive(vanilla_blocks::REPEATER.default_state(), &air, pos));
321    }
322
323    #[test]
324    fn neighbor_tick_priority_matches_vanilla_diode_ordering() {
325        let _repeater = repeater();
326        let pos = BlockPos::new(0, 64, 0);
327        let off = repeater_state(Direction::North, false);
328        let on = repeater_state(Direction::North, true);
329        let plain_level = TestLevel::default();
330
331        assert_eq!(
332            DiodeBlock::tick_priority(&plain_level, pos, off, false),
333            TickPriority::High
334        );
335        assert_eq!(
336            DiodeBlock::tick_priority(&plain_level, pos, on, true),
337            TickPriority::VeryHigh
338        );
339
340        let crossing_diode = repeater_state(Direction::East, false);
341        let prioritized_level = TestLevel::default().with_block(pos.south(), crossing_diode);
342        assert_eq!(
343            DiodeBlock::tick_priority(&prioritized_level, pos, off, false),
344            TickPriority::ExtremelyHigh
345        );
346    }
347}