steel_core/behavior/blocks/redstone/rail/
base_rail_block.rs1use std::sync::Arc;
2
3use steel_registry::blocks::BlockRef;
4use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_registry::blocks::properties::{BlockStateProperties, RailShape};
6use steel_registry::blocks::shapes::SupportType;
7use steel_registry::{vanilla_block_tags::BlockTag, vanilla_fluids};
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::behavior::{BLOCK_BEHAVIORS, BlockPlaceContext};
11use crate::world::{LevelReader, ScheduledTickAccess, SignalGetter as _, World};
12
13use super::rail_state::RailState;
14
15pub(super) struct BaseRailBlock {
17 pub(super) block: BlockRef,
18 is_straight: bool,
19}
20
21impl BaseRailBlock {
22 #[must_use]
23 pub(super) const fn new(block: BlockRef, is_straight: bool) -> Self {
24 Self { block, is_straight }
25 }
26
27 #[must_use]
28 pub(super) const fn is_straight(&self) -> bool {
29 self.is_straight
30 }
31
32 #[must_use]
33 pub(super) fn is_rail_state(state: BlockStateId) -> bool {
34 let block = state.get_block();
35 block.has_tag(&BlockTag::RAILS) && BLOCK_BEHAVIORS.get_behavior(block).as_rail().is_some()
36 }
37
38 fn can_support_rigid_block(level: &dyn LevelReader, pos: BlockPos) -> bool {
39 level.is_face_sturdy_for(
40 level.get_block_state(pos),
41 pos,
42 Direction::Up,
43 SupportType::Rigid,
44 )
45 }
46
47 #[must_use]
48 pub(super) fn can_survive(level: &dyn LevelReader, pos: BlockPos) -> bool {
49 Self::can_support_rigid_block(level, pos.below())
50 }
51
52 #[must_use]
53 pub(super) fn state_for_placement(&self, context: &BlockPlaceContext<'_>) -> BlockStateId {
54 let horizontal = context.horizontal_direction();
55 let shape = if matches!(horizontal, Direction::East | Direction::West) {
56 RailShape::EastWest
57 } else {
58 RailShape::NorthSouth
59 };
60 self.block
61 .default_state()
62 .set_value(&BlockStateProperties::RAIL_SHAPE, shape)
63 .set_value(
64 &BlockStateProperties::WATERLOGGED,
65 context.is_water_source(),
66 )
67 }
68
69 pub(super) fn update_shape(
70 state: BlockStateId,
71 level: &dyn ScheduledTickAccess,
72 pos: BlockPos,
73 ) -> BlockStateId {
74 if state.get_value(&BlockStateProperties::WATERLOGGED) {
75 let delay = level.fluid_tick_delay(&vanilla_fluids::WATER);
76 level.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
77 }
78 state
79 }
80
81 #[must_use]
83 pub(super) fn update_state_on_place(
84 &self,
85 state: BlockStateId,
86 world: &Arc<World>,
87 pos: BlockPos,
88 moved_by_piston: bool,
89 ) -> BlockStateId {
90 let state = Self::update_dir(world, pos, state, true);
91 if self.is_straight {
92 world.neighbor_changed_with_state(state, pos, self.block, moved_by_piston);
93 }
94 state
95 }
96
97 #[must_use]
98 pub(super) fn update_dir(
99 world: &Arc<World>,
100 pos: BlockPos,
101 state: BlockStateId,
102 first: bool,
103 ) -> BlockStateId {
104 let current = state.get_value(&BlockStateProperties::RAIL_SHAPE);
105 let Some(mut rail) = RailState::new(world, pos, state) else {
106 return state;
107 };
108 rail.place(world.has_neighbor_signal(pos), first, current)
109 }
110
111 fn should_be_removed(state: BlockStateId, world: &Arc<World>, pos: BlockPos) -> bool {
112 if !Self::can_support_rigid_block(world.as_ref(), pos.below()) {
113 return true;
114 }
115
116 match state.get_value(&BlockStateProperties::RAIL_SHAPE) {
117 RailShape::AscendingEast => !Self::can_support_rigid_block(world.as_ref(), pos.east()),
118 RailShape::AscendingWest => !Self::can_support_rigid_block(world.as_ref(), pos.west()),
119 RailShape::AscendingNorth => {
120 !Self::can_support_rigid_block(world.as_ref(), pos.north())
121 }
122 RailShape::AscendingSouth => {
123 !Self::can_support_rigid_block(world.as_ref(), pos.south())
124 }
125 RailShape::NorthSouth
126 | RailShape::EastWest
127 | RailShape::SouthEast
128 | RailShape::SouthWest
129 | RailShape::NorthWest
130 | RailShape::NorthEast => false,
131 }
132 }
133
134 pub(super) fn handle_neighbor_changed(
136 &self,
137 state: BlockStateId,
138 world: &Arc<World>,
139 pos: BlockPos,
140 moved_by_piston: bool,
141 ) -> bool {
142 if world.get_block_state(pos).get_block() != self.block {
143 return false;
144 }
145 if !Self::should_be_removed(state, world, pos) {
146 return true;
147 }
148
149 world.drop_resources(state, pos);
150 world.remove_block(pos, moved_by_piston);
151 false
152 }
153
154 pub(super) fn affect_neighbors_after_removal(
155 &self,
156 state: BlockStateId,
157 world: &Arc<World>,
158 pos: BlockPos,
159 moved_by_piston: bool,
160 ) {
161 if moved_by_piston {
162 return;
163 }
164 if state
165 .get_value(&BlockStateProperties::RAIL_SHAPE)
166 .is_slope()
167 {
168 world.update_neighbors_at(pos.above(), self.block);
169 }
170 if self.is_straight {
171 world.update_neighbors_at(pos, self.block);
172 world.update_neighbors_at(pos.below(), self.block);
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use steel_registry::init_vanilla_registry;
180 use steel_registry::vanilla_blocks;
181
182 use super::*;
183 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
184
185 #[test]
186 fn rail_capability_requires_both_tag_and_behavior() {
187 init_vanilla_registry();
188 init_behaviors();
189
190 assert!(BaseRailBlock::is_rail_state(
191 vanilla_blocks::RAIL.default_state()
192 ));
193 assert!(!BaseRailBlock::is_rail_state(
194 vanilla_blocks::STONE.default_state()
195 ));
196 assert!(
197 BLOCK_BEHAVIORS
198 .get_behavior(&vanilla_blocks::POWERED_RAIL)
199 .is_rail()
200 );
201 }
202}