Skip to main content

steel_core/behavior/blocks/redstone/rail/
powered_rail_block.rs

1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::blocks::BlockRef;
5use steel_registry::blocks::block_state_ext::BlockStateExt as _;
6use steel_registry::blocks::properties::{BlockStateProperties, RailShape};
7use steel_utils::types::UpdateFlags;
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::behavior::{BlockBehavior, BlockPlaceContext, RailBehavior};
11use crate::world::{LevelReader, ScheduledTickAccess, SignalGetter as _, World};
12
13use super::base_rail_block::BaseRailBlock;
14
15/// Vanilla powered-rail behavior, also used by activator rails.
16#[block_behavior]
17pub struct PoweredRailBlock {
18    base: BaseRailBlock,
19}
20
21impl PoweredRailBlock {
22    const MAX_SEARCH_DEPTH: i32 = 8;
23
24    /// Creates powered or activator rail behavior for `block`.
25    #[must_use]
26    pub const fn new(block: BlockRef) -> Self {
27        Self {
28            base: BaseRailBlock::new(block, true),
29        }
30    }
31
32    fn find_powered_rail_signal(
33        &self,
34        world: &Arc<World>,
35        pos: BlockPos,
36        state: BlockStateId,
37        forward: bool,
38        search_depth: i32,
39    ) -> bool {
40        if search_depth >= Self::MAX_SEARCH_DEPTH {
41            return false;
42        }
43
44        let mut x = pos.x();
45        let mut y = pos.y();
46        let mut z = pos.z();
47        let mut check_below = true;
48        let expected_shape = match state.get_value(&BlockStateProperties::RAIL_SHAPE) {
49            RailShape::NorthSouth => {
50                z += if forward { 1 } else { -1 };
51                RailShape::NorthSouth
52            }
53            RailShape::EastWest => {
54                x += if forward { -1 } else { 1 };
55                RailShape::EastWest
56            }
57            RailShape::AscendingEast => {
58                if forward {
59                    x -= 1;
60                } else {
61                    x += 1;
62                    y += 1;
63                    check_below = false;
64                }
65                RailShape::EastWest
66            }
67            RailShape::AscendingWest => {
68                if forward {
69                    x -= 1;
70                    y += 1;
71                    check_below = false;
72                } else {
73                    x += 1;
74                }
75                RailShape::EastWest
76            }
77            RailShape::AscendingNorth => {
78                if forward {
79                    z += 1;
80                } else {
81                    z -= 1;
82                    y += 1;
83                    check_below = false;
84                }
85                RailShape::NorthSouth
86            }
87            RailShape::AscendingSouth => {
88                if forward {
89                    z += 1;
90                    y += 1;
91                    check_below = false;
92                } else {
93                    z -= 1;
94                }
95                RailShape::NorthSouth
96            }
97            RailShape::SouthEast
98            | RailShape::SouthWest
99            | RailShape::NorthWest
100            | RailShape::NorthEast => return false,
101        };
102
103        let next = BlockPos::new(x, y, z);
104        self.is_same_rail_with_power(world, next, forward, search_depth, expected_shape)
105            || (check_below
106                && self.is_same_rail_with_power(
107                    world,
108                    next.below(),
109                    forward,
110                    search_depth,
111                    expected_shape,
112                ))
113    }
114
115    fn is_same_rail_with_power(
116        &self,
117        world: &Arc<World>,
118        pos: BlockPos,
119        forward: bool,
120        search_depth: i32,
121        expected_shape: RailShape,
122    ) -> bool {
123        let state = world.get_block_state(pos);
124        if state.get_block() != self.base.block {
125            return false;
126        }
127
128        let shape = state.get_value(&BlockStateProperties::RAIL_SHAPE);
129        let incompatible = match expected_shape {
130            RailShape::EastWest => matches!(
131                shape,
132                RailShape::NorthSouth | RailShape::AscendingNorth | RailShape::AscendingSouth
133            ),
134            RailShape::NorthSouth => matches!(
135                shape,
136                RailShape::EastWest | RailShape::AscendingEast | RailShape::AscendingWest
137            ),
138            RailShape::AscendingEast
139            | RailShape::AscendingWest
140            | RailShape::AscendingNorth
141            | RailShape::AscendingSouth
142            | RailShape::SouthEast
143            | RailShape::SouthWest
144            | RailShape::NorthWest
145            | RailShape::NorthEast => true,
146        };
147        if incompatible || !state.get_value(&BlockStateProperties::POWERED) {
148            return false;
149        }
150
151        world.has_neighbor_signal(pos)
152            || self.find_powered_rail_signal(world, pos, state, forward, search_depth + 1)
153    }
154
155    fn update_powered_state(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
156        let was_powered = state.get_value(&BlockStateProperties::POWERED);
157        let should_power = world.has_neighbor_signal(pos)
158            || self.find_powered_rail_signal(world, pos, state, true, 0)
159            || self.find_powered_rail_signal(world, pos, state, false, 0);
160        if should_power == was_powered {
161            return;
162        }
163
164        world.set_block(
165            pos,
166            state.set_value(&BlockStateProperties::POWERED, should_power),
167            UpdateFlags::UPDATE_ALL,
168        );
169        world.update_neighbors_at(pos.below(), self.base.block);
170        if state
171            .get_value(&BlockStateProperties::RAIL_SHAPE)
172            .is_slope()
173        {
174            world.update_neighbors_at(pos.above(), self.base.block);
175        }
176    }
177}
178
179impl RailBehavior for PoweredRailBlock {
180    fn is_straight(&self) -> bool {
181        self.base.is_straight()
182    }
183}
184
185impl BlockBehavior for PoweredRailBlock {
186    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
187        Some(self.base.state_for_placement(context))
188    }
189
190    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
191        BaseRailBlock::can_survive(world, pos)
192    }
193
194    fn update_shape(
195        &self,
196        state: BlockStateId,
197        world: &dyn ScheduledTickAccess,
198        pos: BlockPos,
199        _direction: Direction,
200        _neighbor_pos: BlockPos,
201        _neighbor_state: BlockStateId,
202    ) -> BlockStateId {
203        BaseRailBlock::update_shape(state, world, pos)
204    }
205
206    fn on_place(
207        &self,
208        state: BlockStateId,
209        world: &Arc<World>,
210        pos: BlockPos,
211        old_state: BlockStateId,
212        moved_by_piston: bool,
213    ) {
214        if old_state.get_block() != self.base.block {
215            let _ = self
216                .base
217                .update_state_on_place(state, world, pos, moved_by_piston);
218        }
219    }
220
221    fn handle_neighbor_changed(
222        &self,
223        state: BlockStateId,
224        world: &Arc<World>,
225        pos: BlockPos,
226        _source_block: BlockRef,
227        moved_by_piston: bool,
228    ) {
229        if self
230            .base
231            .handle_neighbor_changed(state, world, pos, moved_by_piston)
232        {
233            self.update_powered_state(state, world, pos);
234        }
235    }
236
237    fn affect_neighbors_after_removal(
238        &self,
239        state: BlockStateId,
240        world: &Arc<World>,
241        pos: BlockPos,
242        moved_by_piston: bool,
243    ) {
244        self.base
245            .affect_neighbors_after_removal(state, world, pos, moved_by_piston);
246    }
247
248    fn as_rail(&self) -> Option<&dyn RailBehavior> {
249        Some(self)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use steel_registry::init_vanilla_registry;
256    use steel_registry::vanilla_blocks;
257    use steel_utils::ChunkPos;
258
259    use super::*;
260    use crate::behavior::init_behaviors;
261    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
262
263    fn raw_flags() -> UpdateFlags {
264        UpdateFlags::UPDATE_NONE | UpdateFlags::UPDATE_SKIP_ON_PLACE
265    }
266
267    fn powered_chain_world(key: &'static str, last_x: i32) -> (Arc<World>, BlockPos) {
268        init_vanilla_registry();
269        init_behaviors();
270        let world = fresh_test_world(key);
271        let start = BlockPos::new(8, 64, 8);
272        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(start));
273        let end_chunk = ChunkPos::from_block_pos(start.offset(last_x, 0, 0));
274        if end_chunk != ChunkPos::from_block_pos(start) {
275            insert_ready_full_chunk(&world, end_chunk);
276        }
277        for x in 0..=last_x {
278            let pos = start.offset(x, 0, 0);
279            world.set_block(
280                pos.below(),
281                vanilla_blocks::STONE.default_state(),
282                raw_flags(),
283            );
284            let state = vanilla_blocks::POWERED_RAIL
285                .default_state()
286                .set_value(&BlockStateProperties::RAIL_SHAPE, RailShape::EastWest)
287                .set_value(&BlockStateProperties::POWERED, x != 0);
288            world.set_block(pos, state, raw_flags());
289        }
290        world.set_block(
291            start.offset(last_x, 1, 0),
292            vanilla_blocks::REDSTONE_BLOCK.default_state(),
293            raw_flags(),
294        );
295        (world, start)
296    }
297
298    #[test]
299    fn powered_signal_reaches_exact_vanilla_depth_limit() {
300        let behavior = PoweredRailBlock::new(&vanilla_blocks::POWERED_RAIL);
301
302        let (within_world, start) = powered_chain_world("powered_rail_depth_eight", 8);
303        let start_state = within_world.get_block_state(start);
304        assert!(behavior.find_powered_rail_signal(&within_world, start, start_state, false, 0,));
305
306        let (outside_world, start) = powered_chain_world("powered_rail_depth_nine", 9);
307        let start_state = outside_world.get_block_state(start);
308        assert!(!behavior.find_powered_rail_signal(&outside_world, start, start_state, false, 0,));
309    }
310
311    #[test]
312    fn powered_propagation_requires_exact_block_identity() {
313        init_vanilla_registry();
314        init_behaviors();
315        let world = fresh_test_world("powered_rail_activator_isolation");
316        let start = BlockPos::new(8, 64, 8);
317        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(start));
318        for pos in [start, start.east()] {
319            world.set_block(
320                pos.below(),
321                vanilla_blocks::STONE.default_state(),
322                raw_flags(),
323            );
324        }
325        let start_state = vanilla_blocks::POWERED_RAIL
326            .default_state()
327            .set_value(&BlockStateProperties::RAIL_SHAPE, RailShape::EastWest);
328        world.set_block(start, start_state, raw_flags());
329        world.set_block(
330            start.east(),
331            vanilla_blocks::ACTIVATOR_RAIL
332                .default_state()
333                .set_value(&BlockStateProperties::RAIL_SHAPE, RailShape::EastWest)
334                .set_value(&BlockStateProperties::POWERED, true),
335            raw_flags(),
336        );
337        world.set_block(
338            start.east().above(),
339            vanilla_blocks::REDSTONE_BLOCK.default_state(),
340            raw_flags(),
341        );
342
343        let behavior = PoweredRailBlock::new(&vanilla_blocks::POWERED_RAIL);
344        assert!(!behavior.find_powered_rail_signal(&world, start, start_state, false, 0,));
345    }
346}