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