steel_core/behavior/blocks/redstone/rail/
rail_state.rs1use 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
13pub(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 #[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}