Skip to main content

steel_core/behavior/blocks/building/
fence_block.rs

1//! Fence block behavior implementation.
2//!
3//! Fences connect to adjacent fences, fence gates, and solid blocks.
4
5use std::sync::Arc;
6
7use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
8use crate::behavior::context::BlockPlaceContext;
9use crate::behavior::items::LeadItem;
10use crate::behavior::{InteractionResult, InventoryAccess};
11use crate::player::Player;
12use crate::world::{LevelReader, ScheduledTickAccess, World};
13use steel_macros::block_behavior;
14use steel_registry::blocks::BlockRef;
15use steel_registry::blocks::block_state_ext::BlockStateExt;
16use steel_registry::blocks::properties::{
17    BlockStateProperties, BoolProperty, Direction, EnumProperty,
18};
19use steel_registry::items::item::BlockHitResult;
20use steel_registry::vanilla_block_tags::BlockTag;
21use steel_utils::{BlockPos, BlockStateId};
22
23/// Behavior for fence blocks.
24///
25/// Fences have 4 boolean properties (north, east, south, west) that indicate
26/// whether the fence connects in that direction. A fence connects to:
27/// - Other fences of the same type
28/// - Fence gates facing the appropriate direction
29/// - Blocks with a sturdy face on the connecting side
30#[block_behavior]
31pub struct FenceBlock {
32    block: BlockRef,
33}
34
35const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
36/// North connection property.
37pub const NORTH: &BoolProperty = &BlockStateProperties::NORTH;
38/// East connection property.
39pub const EAST: &BoolProperty = &BlockStateProperties::EAST;
40/// South connection property.
41pub const SOUTH: &BoolProperty = &BlockStateProperties::SOUTH;
42/// West connection property.
43pub const WEST: &BoolProperty = &BlockStateProperties::WEST;
44/// Waterlogged property.
45pub const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
46
47impl FenceBlock {
48    /// Creates a new fence block behavior for the given block.
49    #[must_use]
50    pub const fn new(block: BlockRef) -> Self {
51        Self { block }
52    }
53
54    /// Checks if this fence should connect to the given neighbor state.
55    fn connects_to(
56        world: &dyn LevelReader,
57        neighbor_state: BlockStateId,
58        neighbor_pos: BlockPos,
59        direction: Direction,
60    ) -> bool {
61        let neighbor_block = neighbor_state.get_block();
62
63        // Check if it's a fence (same tag)
64        if neighbor_block.has_tag(&BlockTag::FENCES) {
65            return true;
66        }
67
68        // Check if it's a fence gate facing the right direction
69        if neighbor_block.has_tag(&BlockTag::FENCE_GATES) {
70            // Fence gates connect perpendicular to their facing direction
71            // A gate facing north/south connects to fences to its east/west
72            // A gate facing east/west connects to fences to its north/south
73            if let Some(gate_facing) = neighbor_state.try_get_value(HORIZONTAL_FACING) {
74                // Gate connects perpendicular to its facing
75                let connects = match (gate_facing, direction) {
76                    // Gate facing N/S connects to blocks on E/W sides,
77                    // Gate facing E/W connects to blocks on N/S sides
78                    (Direction::North | Direction::South, Direction::East | Direction::West)
79                    | (Direction::East | Direction::West, Direction::North | Direction::South) => {
80                        true
81                    }
82                    _ => false,
83                };
84                if connects {
85                    return true;
86                }
87            }
88        }
89
90        // Check if the neighbor has a sturdy face on the opposite side
91        let opposite = match direction {
92            Direction::North => Direction::South,
93            Direction::South => Direction::North,
94            Direction::East => Direction::West,
95            Direction::West => Direction::East,
96            Direction::Up => Direction::Down,
97            Direction::Down => Direction::Up,
98        };
99        world.is_face_sturdy(neighbor_state, neighbor_pos, opposite)
100    }
101
102    /// Gets the connection state for a position by checking all 4 horizontal neighbors.
103    fn get_connection_state(&self, world: &Arc<World>, pos: BlockPos) -> BlockStateId {
104        let mut state = self.block.default_state();
105
106        // Check north
107        let north_pos = Direction::North.relative(pos);
108        let north_state = world.get_block_state(north_pos);
109        let connects_north = Self::connects_to(world, north_state, north_pos, Direction::North);
110        state = state.set_value(NORTH, connects_north);
111
112        // Check east
113        let east_pos = Direction::East.relative(pos);
114        let east_state = world.get_block_state(east_pos);
115        let connects_east = Self::connects_to(world, east_state, east_pos, Direction::East);
116        state = state.set_value(EAST, connects_east);
117
118        // Check south
119        let south_pos = Direction::South.relative(pos);
120        let south_state = world.get_block_state(south_pos);
121        let connects_south = Self::connects_to(world, south_state, south_pos, Direction::South);
122        state = state.set_value(SOUTH, connects_south);
123
124        // Check west
125        let west_pos = Direction::West.relative(pos);
126        let west_state = world.get_block_state(west_pos);
127        let connects_west = Self::connects_to(world, west_state, west_pos, Direction::West);
128        state = state.set_value(WEST, connects_west);
129
130        state
131    }
132}
133
134impl BlockBehavior for FenceBlock {
135    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
136        log::debug!(
137            "FenceBlock::get_state_for_placement called for {:?} at {:?}",
138            self.block.key,
139            context.place_pos()
140        );
141        Some(
142            self.get_connection_state(context.world, context.place_pos())
143                .set_value(WATERLOGGED, context.is_water_source()),
144        )
145    }
146
147    fn update_shape(
148        &self,
149        state: BlockStateId,
150        world: &dyn ScheduledTickAccess,
151        pos: BlockPos,
152        direction: Direction,
153        neighbor_pos: BlockPos,
154        neighbor_state: BlockStateId,
155    ) -> BlockStateId {
156        schedule_water_tick_if_waterlogged(state, world, pos);
157
158        // Only update for horizontal directions
159        match direction {
160            Direction::North => {
161                let connects =
162                    Self::connects_to(world, neighbor_state, neighbor_pos, Direction::North);
163                state.set_value(NORTH, connects)
164            }
165            Direction::East => {
166                let connects =
167                    Self::connects_to(world, neighbor_state, neighbor_pos, Direction::East);
168                state.set_value(EAST, connects)
169            }
170            Direction::South => {
171                let connects =
172                    Self::connects_to(world, neighbor_state, neighbor_pos, Direction::South);
173                state.set_value(SOUTH, connects)
174            }
175            Direction::West => {
176                let connects =
177                    Self::connects_to(world, neighbor_state, neighbor_pos, Direction::West);
178                state.set_value(WEST, connects)
179            }
180            // Vertical directions don't affect fence connections
181            Direction::Up | Direction::Down => state,
182        }
183    }
184
185    fn use_without_item(
186        &self,
187        _state: BlockStateId,
188        world: &Arc<World>,
189        pos: BlockPos,
190        player: &Player,
191        _hit_result: &BlockHitResult,
192        _inv: &mut InventoryAccess,
193    ) -> InteractionResult {
194        LeadItem::bind_player_mobs(player, world, pos)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
201    use steel_utils::BlockPos;
202
203    use crate::test_support::TestLevel;
204
205    use super::*;
206
207    #[test]
208    fn waterlogged_fence_update_shape_schedules_water_tick() {
209        init_vanilla_registry();
210
211        let behavior = FenceBlock::new(&vanilla_blocks::OAK_FENCE);
212        let state = vanilla_blocks::OAK_FENCE
213            .default_state()
214            .set_value(WATERLOGGED, true);
215        let level = TestLevel::default();
216
217        let updated = behavior.update_shape(
218            state,
219            &level,
220            BlockPos::ZERO,
221            Direction::Up,
222            Direction::Up.relative(BlockPos::ZERO),
223            vanilla_blocks::AIR.default_state(),
224        );
225
226        assert_eq!(updated, state);
227        assert_eq!(
228            level
229                .scheduled_fluid_ticks
230                .borrow()
231                .iter()
232                .map(|tick| (tick.fluid, tick.delay))
233                .collect::<Vec<_>>(),
234            vec![(&vanilla_fluids::WATER, 5)]
235        );
236    }
237}