Skip to main content

steel_core/behavior/blocks/redstone/rail/
base_rail_block.rs

1use 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
15/// Shared server behavior inherited from vanilla's `BaseRailBlock`.
16pub(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    /// Runs vanilla's initial topology update and straight-rail redstone check.
82    #[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    /// Returns whether the rail remains in place and subclass handling may run.
135    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}