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, EnumProperty, 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
25const RAIL_SHAPE: &EnumProperty<RailShape> = &BlockStateProperties::RAIL_SHAPE;
26
27impl<'a> RailState<'a> {
28    pub(super) fn new(world: &'a Arc<World>, pos: BlockPos, state: BlockStateId) -> Option<Self> {
29        if !BaseRailBlock::is_rail_state(state) {
30            return None;
31        }
32        let is_straight = BLOCK_BEHAVIORS
33            .get_behavior(state.get_block())
34            .as_rail()?
35            .is_straight();
36        let shape = state.get_value(RAIL_SHAPE);
37        let mut rail = Self {
38            world,
39            pos,
40            state,
41            is_straight,
42            connections: Vec::with_capacity(2),
43        };
44        rail.update_connections(shape);
45        Some(rail)
46    }
47
48    #[must_use]
49    pub(super) fn connections(&self) -> &[BlockPos] {
50        &self.connections
51    }
52
53    fn update_connections(&mut self, shape: RailShape) {
54        self.connections.clear();
55        match shape {
56            RailShape::NorthSouth => {
57                self.connections.push(self.pos.north());
58                self.connections.push(self.pos.south());
59            }
60            RailShape::EastWest => {
61                self.connections.push(self.pos.west());
62                self.connections.push(self.pos.east());
63            }
64            RailShape::AscendingEast => {
65                self.connections.push(self.pos.west());
66                self.connections.push(self.pos.east().above());
67            }
68            RailShape::AscendingWest => {
69                self.connections.push(self.pos.west().above());
70                self.connections.push(self.pos.east());
71            }
72            RailShape::AscendingNorth => {
73                self.connections.push(self.pos.north().above());
74                self.connections.push(self.pos.south());
75            }
76            RailShape::AscendingSouth => {
77                self.connections.push(self.pos.north());
78                self.connections.push(self.pos.south().above());
79            }
80            RailShape::SouthEast => {
81                self.connections.push(self.pos.east());
82                self.connections.push(self.pos.south());
83            }
84            RailShape::SouthWest => {
85                self.connections.push(self.pos.west());
86                self.connections.push(self.pos.south());
87            }
88            RailShape::NorthWest => {
89                self.connections.push(self.pos.west());
90                self.connections.push(self.pos.north());
91            }
92            RailShape::NorthEast => {
93                self.connections.push(self.pos.east());
94                self.connections.push(self.pos.north());
95            }
96        }
97    }
98
99    fn remove_soft_connections(&mut self) {
100        let mut index = 0;
101        while index < self.connections.len() {
102            let Some(rail) = self.get_rail(self.connections[index]) else {
103                self.connections.remove(index);
104                continue;
105            };
106            if rail.connects_to(self) {
107                self.connections[index] = rail.pos;
108                index += 1;
109            } else {
110                self.connections.remove(index);
111            }
112        }
113    }
114
115    fn has_rail(&self, pos: BlockPos) -> bool {
116        BaseRailBlock::is_rail_state(self.world.get_block_state(pos))
117            || BaseRailBlock::is_rail_state(self.world.get_block_state(pos.above()))
118            || BaseRailBlock::is_rail_state(self.world.get_block_state(pos.below()))
119    }
120
121    fn get_rail(&self, pos: BlockPos) -> Option<Self> {
122        for test_pos in [pos, pos.above(), pos.below()] {
123            let state = self.world.get_block_state(test_pos);
124            if let Some(rail) = Self::new(self.world, test_pos, state) {
125                return Some(rail);
126            }
127        }
128        None
129    }
130
131    fn connects_to(&self, rail: &Self) -> bool {
132        self.has_connection(rail.pos)
133    }
134
135    fn has_connection(&self, rail_pos: BlockPos) -> bool {
136        self.connections
137            .iter()
138            .any(|pos| pos.x() == rail_pos.x() && pos.z() == rail_pos.z())
139    }
140
141    #[must_use]
142    pub(super) fn count_potential_connections(&self) -> usize {
143        Direction::HORIZONTAL
144            .into_iter()
145            .filter(|direction| self.has_rail(self.pos.relative(*direction)))
146            .count()
147    }
148
149    fn can_connect_to(&self, rail: &Self) -> bool {
150        self.connects_to(rail) || self.connections.len() != 2
151    }
152
153    fn connect_to(&mut self, rail: &Self) {
154        self.connections.push(rail.pos);
155        let north = self.pos.north();
156        let south = self.pos.south();
157        let west = self.pos.west();
158        let east = self.pos.east();
159        let n = self.has_connection(north);
160        let s = self.has_connection(south);
161        let w = self.has_connection(west);
162        let e = self.has_connection(east);
163
164        let mut shape = None;
165        if n || s {
166            shape = Some(RailShape::NorthSouth);
167        }
168        if w || e {
169            shape = Some(RailShape::EastWest);
170        }
171        if !self.is_straight {
172            if s && e && !n && !w {
173                shape = Some(RailShape::SouthEast);
174            }
175            if s && w && !n && !e {
176                shape = Some(RailShape::SouthWest);
177            }
178            if n && w && !s && !e {
179                shape = Some(RailShape::NorthWest);
180            }
181            if n && e && !s && !w {
182                shape = Some(RailShape::NorthEast);
183            }
184        }
185
186        if shape == Some(RailShape::NorthSouth) {
187            if BaseRailBlock::is_rail_state(self.world.get_block_state(north.above())) {
188                shape = Some(RailShape::AscendingNorth);
189            }
190            if BaseRailBlock::is_rail_state(self.world.get_block_state(south.above())) {
191                shape = Some(RailShape::AscendingSouth);
192            }
193        }
194        if shape == Some(RailShape::EastWest) {
195            if BaseRailBlock::is_rail_state(self.world.get_block_state(east.above())) {
196                shape = Some(RailShape::AscendingEast);
197            }
198            if BaseRailBlock::is_rail_state(self.world.get_block_state(west.above())) {
199                shape = Some(RailShape::AscendingWest);
200            }
201        }
202
203        let shape = shape.unwrap_or(RailShape::NorthSouth);
204        self.state = self.state.set_value(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.state.set_value(RAIL_SHAPE, shape);
326        if first || self.world.get_block_state(self.pos) != self.state {
327            self.world
328                .set_block(self.pos, self.state, UpdateFlags::UPDATE_ALL);
329            for index in 0..self.connections.len() {
330                let connection = self.connections[index];
331                let Some(mut neighbor) = self.get_rail(connection) else {
332                    continue;
333                };
334                neighbor.remove_soft_connections();
335                if neighbor.can_connect_to(self) {
336                    neighbor.connect_to(self);
337                }
338            }
339        }
340        self.state
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use steel_registry::init_vanilla_registry;
347    use steel_registry::vanilla_blocks;
348    use steel_utils::ChunkPos;
349
350    use super::*;
351    use crate::behavior::init_behaviors;
352    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
353
354    const RAIL_SHAPE: &EnumProperty<RailShape> = &BlockStateProperties::RAIL_SHAPE;
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(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!(unpowered.get_value(RAIL_SHAPE), RailShape::SouthEast);
406
407        let (powered_world, center) = topology_world("rail_powered_curve_tie");
408        let state = set_four_way_junction(&powered_world, center);
409        let mut rail = RailState::new(&powered_world, center, state)
410            .expect("ordinary rail should expose rail capability");
411        let powered = rail.place(true, true, RailShape::NorthSouth);
412        assert_eq!(powered.get_value(RAIL_SHAPE), RailShape::NorthWest);
413    }
414
415    #[test]
416    fn east_upper_neighbor_creates_slope_with_ordered_connections() {
417        let (world, center) = topology_world("rail_ascending_east");
418        world.set_block(
419            center.east(),
420            vanilla_blocks::STONE.default_state(),
421            raw_flags(),
422        );
423        set_raw_rail(&world, center.east().above(), RailShape::EastWest);
424        let state = set_raw_rail(&world, center, RailShape::EastWest);
425        let mut rail = RailState::new(&world, center, state)
426            .expect("ordinary rail should expose rail capability");
427        let placed = rail.place(false, true, RailShape::EastWest);
428
429        assert_eq!(placed.get_value(RAIL_SHAPE), RailShape::AscendingEast);
430        assert_eq!(rail.connections(), &[center.west(), center.east().above()]);
431    }
432}