Skip to main content

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

1use std::sync::Arc;
2
3use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_registry::blocks::properties::{BlockStateProperties, RailShape};
5use steel_utils::types::UpdateFlags;
6use steel_utils::{BlockPos, BlockStateId, Direction};
7
8use crate::behavior::BLOCK_BEHAVIORS;
9use crate::world::{LevelReader as _, World};
10
11use super::base_rail_block::BaseRailBlock;
12
13/// Vanilla's ordered, mutable rail connection resolver.
14///
15/// `connections` intentionally remains a `Vec`: insertion and traversal order
16/// affects curve selection and synchronous neighbor updates.
17pub(super) struct RailState<'a> {
18    world: &'a Arc<World>,
19    pos: BlockPos,
20    state: BlockStateId,
21    is_straight: bool,
22    connections: Vec<BlockPos>,
23}
24
25impl<'a> RailState<'a> {
26    pub(super) fn new(world: &'a Arc<World>, pos: BlockPos, state: BlockStateId) -> Option<Self> {
27        if !BaseRailBlock::is_rail_state(state) {
28            return None;
29        }
30        let is_straight = BLOCK_BEHAVIORS
31            .get_behavior(state.get_block())
32            .as_rail()?
33            .is_straight();
34        let shape = state.get_value(&BlockStateProperties::RAIL_SHAPE);
35        let mut rail = Self {
36            world,
37            pos,
38            state,
39            is_straight,
40            connections: Vec::with_capacity(2),
41        };
42        rail.update_connections(shape);
43        Some(rail)
44    }
45
46    #[must_use]
47    pub(super) fn connections(&self) -> &[BlockPos] {
48        &self.connections
49    }
50
51    fn update_connections(&mut self, shape: RailShape) {
52        self.connections.clear();
53        match shape {
54            RailShape::NorthSouth => {
55                self.connections.push(self.pos.north());
56                self.connections.push(self.pos.south());
57            }
58            RailShape::EastWest => {
59                self.connections.push(self.pos.west());
60                self.connections.push(self.pos.east());
61            }
62            RailShape::AscendingEast => {
63                self.connections.push(self.pos.west());
64                self.connections.push(self.pos.east().above());
65            }
66            RailShape::AscendingWest => {
67                self.connections.push(self.pos.west().above());
68                self.connections.push(self.pos.east());
69            }
70            RailShape::AscendingNorth => {
71                self.connections.push(self.pos.north().above());
72                self.connections.push(self.pos.south());
73            }
74            RailShape::AscendingSouth => {
75                self.connections.push(self.pos.north());
76                self.connections.push(self.pos.south().above());
77            }
78            RailShape::SouthEast => {
79                self.connections.push(self.pos.east());
80                self.connections.push(self.pos.south());
81            }
82            RailShape::SouthWest => {
83                self.connections.push(self.pos.west());
84                self.connections.push(self.pos.south());
85            }
86            RailShape::NorthWest => {
87                self.connections.push(self.pos.west());
88                self.connections.push(self.pos.north());
89            }
90            RailShape::NorthEast => {
91                self.connections.push(self.pos.east());
92                self.connections.push(self.pos.north());
93            }
94        }
95    }
96
97    fn remove_soft_connections(&mut self) {
98        let mut index = 0;
99        while index < self.connections.len() {
100            let Some(rail) = self.get_rail(self.connections[index]) else {
101                self.connections.remove(index);
102                continue;
103            };
104            if rail.connects_to(self) {
105                self.connections[index] = rail.pos;
106                index += 1;
107            } else {
108                self.connections.remove(index);
109            }
110        }
111    }
112
113    fn has_rail(&self, pos: BlockPos) -> bool {
114        BaseRailBlock::is_rail_state(self.world.get_block_state(pos))
115            || BaseRailBlock::is_rail_state(self.world.get_block_state(pos.above()))
116            || BaseRailBlock::is_rail_state(self.world.get_block_state(pos.below()))
117    }
118
119    fn get_rail(&self, pos: BlockPos) -> Option<Self> {
120        for test_pos in [pos, pos.above(), pos.below()] {
121            let state = self.world.get_block_state(test_pos);
122            if let Some(rail) = Self::new(self.world, test_pos, state) {
123                return Some(rail);
124            }
125        }
126        None
127    }
128
129    fn connects_to(&self, rail: &Self) -> bool {
130        self.has_connection(rail.pos)
131    }
132
133    fn has_connection(&self, rail_pos: BlockPos) -> bool {
134        self.connections
135            .iter()
136            .any(|pos| pos.x() == rail_pos.x() && pos.z() == rail_pos.z())
137    }
138
139    #[must_use]
140    pub(super) fn count_potential_connections(&self) -> usize {
141        Direction::HORIZONTAL
142            .into_iter()
143            .filter(|direction| self.has_rail(self.pos.relative(*direction)))
144            .count()
145    }
146
147    fn can_connect_to(&self, rail: &Self) -> bool {
148        self.connects_to(rail) || self.connections.len() != 2
149    }
150
151    fn connect_to(&mut self, rail: &Self) {
152        self.connections.push(rail.pos);
153        let north = self.pos.north();
154        let south = self.pos.south();
155        let west = self.pos.west();
156        let east = self.pos.east();
157        let n = self.has_connection(north);
158        let s = self.has_connection(south);
159        let w = self.has_connection(west);
160        let e = self.has_connection(east);
161
162        let mut shape = None;
163        if n || s {
164            shape = Some(RailShape::NorthSouth);
165        }
166        if w || e {
167            shape = Some(RailShape::EastWest);
168        }
169        if !self.is_straight {
170            if s && e && !n && !w {
171                shape = Some(RailShape::SouthEast);
172            }
173            if s && w && !n && !e {
174                shape = Some(RailShape::SouthWest);
175            }
176            if n && w && !s && !e {
177                shape = Some(RailShape::NorthWest);
178            }
179            if n && e && !s && !w {
180                shape = Some(RailShape::NorthEast);
181            }
182        }
183
184        if shape == Some(RailShape::NorthSouth) {
185            if BaseRailBlock::is_rail_state(self.world.get_block_state(north.above())) {
186                shape = Some(RailShape::AscendingNorth);
187            }
188            if BaseRailBlock::is_rail_state(self.world.get_block_state(south.above())) {
189                shape = Some(RailShape::AscendingSouth);
190            }
191        }
192        if shape == Some(RailShape::EastWest) {
193            if BaseRailBlock::is_rail_state(self.world.get_block_state(east.above())) {
194                shape = Some(RailShape::AscendingEast);
195            }
196            if BaseRailBlock::is_rail_state(self.world.get_block_state(west.above())) {
197                shape = Some(RailShape::AscendingWest);
198            }
199        }
200
201        let shape = shape.unwrap_or(RailShape::NorthSouth);
202        self.state = self
203            .state
204            .set_value(&BlockStateProperties::RAIL_SHAPE, shape);
205        self.world
206            .set_block(self.pos, self.state, UpdateFlags::UPDATE_ALL);
207    }
208
209    fn has_neighbor_rail(&self, rail_pos: BlockPos) -> bool {
210        let Some(mut neighbor) = self.get_rail(rail_pos) else {
211            return false;
212        };
213        neighbor.remove_soft_connections();
214        neighbor.can_connect_to(self)
215    }
216
217    /// Places this rail and synchronously connects neighbors in vanilla order.
218    #[expect(
219        clippy::too_many_lines,
220        reason = "keeping vanilla's sequential shape overwrites together makes their order auditable"
221    )]
222    pub(super) fn place(
223        &mut self,
224        has_signal: bool,
225        first: bool,
226        default_shape: RailShape,
227    ) -> BlockStateId {
228        let north = self.pos.north();
229        let south = self.pos.south();
230        let west = self.pos.west();
231        let east = self.pos.east();
232        let n = self.has_neighbor_rail(north);
233        let s = self.has_neighbor_rail(south);
234        let w = self.has_neighbor_rail(west);
235        let e = self.has_neighbor_rail(east);
236
237        let north_or_south = n || s;
238        let west_or_east = w || e;
239        let mut shape = None;
240        if north_or_south && !west_or_east {
241            shape = Some(RailShape::NorthSouth);
242        }
243        if west_or_east && !north_or_south {
244            shape = Some(RailShape::EastWest);
245        }
246
247        let south_and_east = s && e;
248        let south_and_west = s && w;
249        let north_and_east = n && e;
250        let north_and_west = n && w;
251        if !self.is_straight {
252            if south_and_east && !n && !w {
253                shape = Some(RailShape::SouthEast);
254            }
255            if south_and_west && !n && !e {
256                shape = Some(RailShape::SouthWest);
257            }
258            if north_and_west && !s && !e {
259                shape = Some(RailShape::NorthWest);
260            }
261            if north_and_east && !s && !w {
262                shape = Some(RailShape::NorthEast);
263            }
264        }
265
266        if shape.is_none() {
267            if north_or_south && west_or_east {
268                shape = Some(default_shape);
269            } else if north_or_south {
270                shape = Some(RailShape::NorthSouth);
271            } else if west_or_east {
272                shape = Some(RailShape::EastWest);
273            }
274
275            if !self.is_straight {
276                if has_signal {
277                    if south_and_east {
278                        shape = Some(RailShape::SouthEast);
279                    }
280                    if south_and_west {
281                        shape = Some(RailShape::SouthWest);
282                    }
283                    if north_and_east {
284                        shape = Some(RailShape::NorthEast);
285                    }
286                    if north_and_west {
287                        shape = Some(RailShape::NorthWest);
288                    }
289                } else {
290                    if north_and_west {
291                        shape = Some(RailShape::NorthWest);
292                    }
293                    if north_and_east {
294                        shape = Some(RailShape::NorthEast);
295                    }
296                    if south_and_west {
297                        shape = Some(RailShape::SouthWest);
298                    }
299                    if south_and_east {
300                        shape = Some(RailShape::SouthEast);
301                    }
302                }
303            }
304        }
305
306        if shape == Some(RailShape::NorthSouth) {
307            if BaseRailBlock::is_rail_state(self.world.get_block_state(north.above())) {
308                shape = Some(RailShape::AscendingNorth);
309            }
310            if BaseRailBlock::is_rail_state(self.world.get_block_state(south.above())) {
311                shape = Some(RailShape::AscendingSouth);
312            }
313        }
314        if shape == Some(RailShape::EastWest) {
315            if BaseRailBlock::is_rail_state(self.world.get_block_state(east.above())) {
316                shape = Some(RailShape::AscendingEast);
317            }
318            if BaseRailBlock::is_rail_state(self.world.get_block_state(west.above())) {
319                shape = Some(RailShape::AscendingWest);
320            }
321        }
322
323        let shape = shape.unwrap_or(default_shape);
324        self.update_connections(shape);
325        self.state = self
326            .state
327            .set_value(&BlockStateProperties::RAIL_SHAPE, shape);
328        if first || self.world.get_block_state(self.pos) != self.state {
329            self.world
330                .set_block(self.pos, self.state, UpdateFlags::UPDATE_ALL);
331            for index in 0..self.connections.len() {
332                let connection = self.connections[index];
333                let Some(mut neighbor) = self.get_rail(connection) else {
334                    continue;
335                };
336                neighbor.remove_soft_connections();
337                if neighbor.can_connect_to(self) {
338                    neighbor.connect_to(self);
339                }
340            }
341        }
342        self.state
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use steel_registry::init_vanilla_registry;
349    use steel_registry::vanilla_blocks;
350    use steel_utils::ChunkPos;
351
352    use super::*;
353    use crate::behavior::init_behaviors;
354    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
355
356    fn raw_flags() -> UpdateFlags {
357        UpdateFlags::UPDATE_NONE | UpdateFlags::UPDATE_SKIP_ON_PLACE
358    }
359
360    fn topology_world(key: &'static str) -> (Arc<World>, BlockPos) {
361        init_vanilla_registry();
362        init_behaviors();
363        let world = fresh_test_world(key);
364        let center = BlockPos::new(8, 64, 8);
365        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(center));
366        for offset in [
367            BlockPos::ZERO,
368            BlockPos::new(0, 0, -1),
369            BlockPos::new(0, 0, 1),
370            BlockPos::new(-1, 0, 0),
371            BlockPos::new(1, 0, 0),
372        ] {
373            world.set_block(
374                center.offset(offset.x(), -1, offset.z()),
375                vanilla_blocks::STONE.default_state(),
376                raw_flags(),
377            );
378        }
379        (world, center)
380    }
381
382    fn set_raw_rail(world: &Arc<World>, pos: BlockPos, shape: RailShape) -> BlockStateId {
383        let state = vanilla_blocks::RAIL
384            .default_state()
385            .set_value(&BlockStateProperties::RAIL_SHAPE, shape);
386        world.set_block(pos, state, raw_flags());
387        state
388    }
389
390    fn set_four_way_junction(world: &Arc<World>, center: BlockPos) -> BlockStateId {
391        set_raw_rail(world, center.north(), RailShape::NorthSouth);
392        set_raw_rail(world, center.south(), RailShape::NorthSouth);
393        set_raw_rail(world, center.west(), RailShape::EastWest);
394        set_raw_rail(world, center.east(), RailShape::EastWest);
395        set_raw_rail(world, center, RailShape::NorthSouth)
396    }
397
398    #[test]
399    fn four_way_curve_tie_uses_vanilla_sequential_overwrite_order() {
400        let (unpowered_world, center) = topology_world("rail_unpowered_curve_tie");
401        let state = set_four_way_junction(&unpowered_world, center);
402        let mut rail = RailState::new(&unpowered_world, center, state)
403            .expect("ordinary rail should expose rail capability");
404        let unpowered = rail.place(false, true, RailShape::NorthSouth);
405        assert_eq!(
406            unpowered.get_value(&BlockStateProperties::RAIL_SHAPE),
407            RailShape::SouthEast
408        );
409
410        let (powered_world, center) = topology_world("rail_powered_curve_tie");
411        let state = set_four_way_junction(&powered_world, center);
412        let mut rail = RailState::new(&powered_world, center, state)
413            .expect("ordinary rail should expose rail capability");
414        let powered = rail.place(true, true, RailShape::NorthSouth);
415        assert_eq!(
416            powered.get_value(&BlockStateProperties::RAIL_SHAPE),
417            RailShape::NorthWest
418        );
419    }
420
421    #[test]
422    fn east_upper_neighbor_creates_slope_with_ordered_connections() {
423        let (world, center) = topology_world("rail_ascending_east");
424        world.set_block(
425            center.east(),
426            vanilla_blocks::STONE.default_state(),
427            raw_flags(),
428        );
429        set_raw_rail(&world, center.east().above(), RailShape::EastWest);
430        let state = set_raw_rail(&world, center, RailShape::EastWest);
431        let mut rail = RailState::new(&world, center, state)
432            .expect("ordinary rail should expose rail capability");
433        let placed = rail.place(false, true, RailShape::EastWest);
434
435        assert_eq!(
436            placed.get_value(&BlockStateProperties::RAIL_SHAPE),
437            RailShape::AscendingEast
438        );
439        assert_eq!(rail.connections(), &[center.west(), center.east().above()]);
440    }
441}