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