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::{
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
18/// Shared server behavior inherited from vanilla's `BaseRailBlock`.
19pub(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    /// 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(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    /// 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.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}