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