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#[block_behavior]
17pub struct PoweredRailBlock {
18 base: BaseRailBlock,
19}
20
21impl PoweredRailBlock {
22 const MAX_SEARCH_DEPTH: i32 = 8;
23
24 #[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}