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::{
6 BlockStateProperties, BoolProperty, EnumProperty, RailShape,
7};
8use steel_registry::blocks::shapes::SupportType;
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_utils::{BlockPos, BlockStateId, Direction};
11
12use crate::behavior::block::schedule_water_tick_if_waterlogged;
13use crate::behavior::{BLOCK_BEHAVIORS, BlockPlaceContext};
14use crate::world::{LevelReader, ScheduledTickAccess, SignalGetter as _, World};
15
16use super::rail_state::RailState;
17
18pub(super) struct BaseRailBlock {
20 pub(super) block: BlockRef,
21 is_straight: bool,
22}
23
24const RAIL_SHAPE: &EnumProperty<RailShape> = &BlockStateProperties::RAIL_SHAPE;
25const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
26
27impl BaseRailBlock {
28 #[must_use]
29 pub(super) const fn new(block: BlockRef, is_straight: bool) -> Self {
30 Self { block, is_straight }
31 }
32
33 #[must_use]
34 pub(super) const fn is_straight(&self) -> bool {
35 self.is_straight
36 }
37
38 #[must_use]
39 pub(super) fn is_rail_state(state: BlockStateId) -> bool {
40 let block = state.get_block();
41 block.has_tag(&BlockTag::RAILS) && BLOCK_BEHAVIORS.get_behavior(block).as_rail().is_some()
42 }
43
44 fn can_support_rigid_block(level: &dyn LevelReader, pos: BlockPos) -> bool {
45 level.is_face_sturdy_for(
46 level.get_block_state(pos),
47 pos,
48 Direction::Up,
49 SupportType::Rigid,
50 )
51 }
52
53 #[must_use]
54 pub(super) fn can_survive(level: &dyn LevelReader, pos: BlockPos) -> bool {
55 Self::can_support_rigid_block(level, pos.below())
56 }
57
58 #[must_use]
59 pub(super) fn state_for_placement(&self, context: &BlockPlaceContext<'_>) -> BlockStateId {
60 let horizontal = context.horizontal_direction();
61 let shape = if matches!(horizontal, Direction::East | Direction::West) {
62 RailShape::EastWest
63 } else {
64 RailShape::NorthSouth
65 };
66 self.block
67 .default_state()
68 .set_value(RAIL_SHAPE, shape)
69 .set_value(WATERLOGGED, context.is_water_source())
70 }
71
72 pub(super) fn update_shape(
73 state: BlockStateId,
74 level: &dyn ScheduledTickAccess,
75 pos: BlockPos,
76 ) -> BlockStateId {
77 schedule_water_tick_if_waterlogged(state, level, pos);
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(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(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.get_value(RAIL_SHAPE).is_slope() {
165 world.update_neighbors_at(pos.above(), self.block);
166 }
167 if self.is_straight {
168 world.update_neighbors_at(pos, self.block);
169 world.update_neighbors_at(pos.below(), self.block);
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use steel_registry::init_vanilla_registry;
177 use steel_registry::vanilla_blocks;
178
179 use super::*;
180 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
181
182 #[test]
183 fn rail_capability_requires_both_tag_and_behavior() {
184 init_vanilla_registry();
185 init_behaviors();
186
187 assert!(BaseRailBlock::is_rail_state(
188 vanilla_blocks::RAIL.default_state()
189 ));
190 assert!(!BaseRailBlock::is_rail_state(
191 vanilla_blocks::STONE.default_state()
192 ));
193 assert!(
194 BLOCK_BEHAVIORS
195 .get_behavior(&vanilla_blocks::POWERED_RAIL)
196 .is_rail()
197 );
198 }
199}