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#[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 #[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}