Skip to main content

steel_core/behavior/blocks/building/
fence_gate_block.rs

1//! Fence gate block behavior.
2//!
3//! Vanilla equivalent: `FenceGateBlock` + `HorizontalDirectionalBlock`.
4//!
5//! Fence gates open/close on use, sit flush in walls (`IN_WALL`), and react to
6//! redstone (`POWERED`/`OPEN` driven by `Level.hasNeighborSignal`).
7
8use crate::behavior::InventoryAccess;
9use crate::behavior::block::BlockBehavior;
10use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
11use crate::entity::Entity;
12use crate::entity::ai::path::PathComputationType;
13use crate::player::Player;
14use crate::world::game_event::GameEventContext;
15use crate::world::{ScheduledTickAccess, SignalGetter as _, World};
16use std::sync::Arc;
17use steel_macros::block_behavior;
18use steel_registry::blocks::BlockRef;
19use steel_registry::blocks::block_state_ext::BlockStateExt;
20use steel_registry::blocks::properties::{
21    BlockStateProperties, BoolProperty, Direction, EnumProperty,
22};
23use steel_registry::sound_event::SoundEventRef;
24use steel_registry::vanilla_block_tags::BlockTag;
25use steel_registry::vanilla_game_events;
26use steel_utils::axis::Axis;
27use steel_utils::types::UpdateFlags;
28use steel_utils::{BlockPos, BlockStateId};
29
30/// Behavior for all fence gate variants.
31#[block_behavior]
32pub struct FenceGateBlock {
33    block: BlockRef,
34    #[json_arg(sound_events, json = "type_fence_gate_open")]
35    sound_open: SoundEventRef,
36    #[json_arg(sound_events, json = "type_fence_gate_close")]
37    sound_close: SoundEventRef,
38}
39
40/// Horizontal facing of the gate.
41const FACING: EnumProperty<Direction> = BlockStateProperties::HORIZONTAL_FACING;
42/// Whether the gate is open.
43const OPEN: BoolProperty = BlockStateProperties::OPEN;
44/// Whether the gate is powered by redstone.
45const POWERED: BoolProperty = BlockStateProperties::POWERED;
46/// Whether the gate is lowered to sit flush inside a wall.
47const IN_WALL: BoolProperty = BlockStateProperties::IN_WALL;
48
49impl FenceGateBlock {
50    /// Creates a new fence gate behavior.
51    ///
52    /// Sound events are provided by the build system from `classes.json`.
53    #[must_use]
54    pub const fn new(
55        block: BlockRef,
56        sound_open: SoundEventRef,
57        sound_close: SoundEventRef,
58    ) -> Self {
59        Self {
60            block,
61            sound_open,
62            sound_close,
63        }
64    }
65
66    /// Vanilla `FenceGateBlock.connectsToDirection`.
67    ///
68    /// A gate connects perpendicular to its facing, i.e. to a wall/fence whose
69    /// connecting axis matches the gate's clockwise-rotated facing axis.
70    #[must_use]
71    pub fn connects_to_direction(state: BlockStateId, direction: Direction) -> bool {
72        state.get_value(&FACING).axis() == direction.rotate_y_clockwise().axis()
73    }
74
75    /// Vanilla `FenceGateBlock.isWall`.
76    fn is_wall(state: BlockStateId) -> bool {
77        state.get_block().has_tag(&BlockTag::WALLS)
78    }
79}
80
81impl BlockBehavior for FenceGateBlock {
82    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
83        let world = context.world;
84        let pos = context.place_pos();
85        let direction = context.horizontal_direction();
86        let axis = direction.axis();
87
88        let in_wall = match axis {
89            Axis::Z => {
90                Self::is_wall(world.get_block_state(Direction::West.relative(pos)))
91                    || Self::is_wall(world.get_block_state(Direction::East.relative(pos)))
92            }
93            Axis::X => {
94                Self::is_wall(world.get_block_state(Direction::North.relative(pos)))
95                    || Self::is_wall(world.get_block_state(Direction::South.relative(pos)))
96            }
97            Axis::Y => false,
98        };
99
100        let is_open = world.has_neighbor_signal(pos);
101
102        Some(
103            self.block
104                .default_state()
105                .set_value(&FACING, direction)
106                .set_value(&OPEN, is_open)
107                .set_value(&POWERED, is_open)
108                .set_value(&IN_WALL, in_wall),
109        )
110    }
111
112    fn update_shape(
113        &self,
114        state: BlockStateId,
115        world: &dyn ScheduledTickAccess,
116        pos: BlockPos,
117        direction: Direction,
118        _neighbor_pos: BlockPos,
119        neighbor_state: BlockStateId,
120    ) -> BlockStateId {
121        // Only the axis perpendicular to the gate (its clockwise facing axis)
122        // can change whether it sits in a wall.
123        if state.get_value(&FACING).rotate_y_clockwise().axis() != direction.axis() {
124            return state;
125        }
126        let opposite_neighbor = world.get_block_state(direction.opposite().relative(pos));
127        let in_wall = Self::is_wall(neighbor_state) || Self::is_wall(opposite_neighbor);
128        state.set_value(&IN_WALL, in_wall)
129    }
130
131    fn use_without_item(
132        &self,
133        state: BlockStateId,
134        world: &Arc<World>,
135        pos: BlockPos,
136        player: &Player,
137        _hit_result: &BlockHitResult,
138        _inv: &mut InventoryAccess,
139    ) -> InteractionResult {
140        let mut new_state = state;
141        if new_state.get_value(&OPEN) {
142            new_state = new_state.set_value(&OPEN, false);
143        } else {
144            let player_direction = player.direction_yaw();
145            // Re-face the gate toward the player if they opened it from behind.
146            if new_state.get_value(&FACING) == player_direction.opposite() {
147                new_state = new_state.set_value(&FACING, player_direction);
148            }
149            new_state = new_state.set_value(&OPEN, true);
150        }
151
152        // Vanilla flag 10 = UPDATE_CLIENTS | UPDATE_IMMEDIATE.
153        world.set_block(
154            pos,
155            new_state,
156            UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_IMMEDIATE,
157        );
158
159        let opens = new_state.get_value(&OPEN);
160        let sound = if opens {
161            self.sound_open
162        } else {
163            self.sound_close
164        };
165        let pitch = rand::random::<f32>() * 0.1 + 0.9;
166        world.play_block_sound(sound, pos, 1.0, pitch, Some(player.id()));
167        let event = if opens {
168            &vanilla_game_events::BLOCK_OPEN
169        } else {
170            &vanilla_game_events::BLOCK_CLOSE
171        };
172        world.game_event(event, pos, &GameEventContext::new(Some(player), None));
173        InteractionResult::Success
174    }
175
176    fn handle_neighbor_changed(
177        &self,
178        state: BlockStateId,
179        world: &Arc<World>,
180        pos: BlockPos,
181        _source_block: BlockRef,
182        _moved_by_piston: bool,
183    ) {
184        let has_power = world.has_neighbor_signal(pos);
185        if state.get_value(&POWERED) == has_power {
186            return;
187        }
188
189        world.set_block(
190            pos,
191            state
192                .set_value(&POWERED, has_power)
193                .set_value(&OPEN, has_power),
194            UpdateFlags::UPDATE_CLIENTS,
195        );
196        if state.get_value(&OPEN) == has_power {
197            return;
198        }
199
200        let sound = if has_power {
201            self.sound_open
202        } else {
203            self.sound_close
204        };
205        let pitch = rand::random::<f32>() * 0.1 + 0.9;
206        world.play_block_sound(sound, pos, 1.0, pitch, None);
207        let event = if has_power {
208            &vanilla_game_events::BLOCK_OPEN
209        } else {
210            &vanilla_game_events::BLOCK_CLOSE
211        };
212        world.game_event(event, pos, &GameEventContext::default());
213    }
214
215    fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
216        match computation_type {
217            PathComputationType::Land | PathComputationType::Air => state.get_value(&OPEN),
218            PathComputationType::Water => false,
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use steel_registry::{
226        blocks::properties::BlockStateProperties, init_vanilla_registry, vanilla_blocks,
227    };
228    use steel_utils::{ChunkPos, types::UpdateFlags};
229
230    use super::*;
231    use crate::{
232        behavior::init_behaviors,
233        test_support::{fresh_test_world, insert_ready_full_chunk},
234    };
235
236    #[test]
237    fn redstone_power_opens_and_closes_fence_gate() {
238        init_vanilla_registry();
239        init_behaviors();
240        let world = fresh_test_world("fence_gate_redstone");
241        let pos = BlockPos::new(8, 64, 8);
242        let power_pos = pos.west();
243        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
244        assert!(world.set_block(
245            pos,
246            vanilla_blocks::OAK_FENCE_GATE.default_state(),
247            UpdateFlags::UPDATE_NONE,
248        ));
249
250        assert!(world.set_block(
251            power_pos,
252            vanilla_blocks::REDSTONE_BLOCK.default_state(),
253            UpdateFlags::UPDATE_ALL,
254        ));
255        let powered = world.get_block_state(pos);
256        assert!(powered.get_value(&BlockStateProperties::POWERED));
257        assert!(powered.get_value(&BlockStateProperties::OPEN));
258
259        assert!(world.remove_block(power_pos, false));
260        let unpowered = world.get_block_state(pos);
261        assert!(!unpowered.get_value(&BlockStateProperties::POWERED));
262        assert!(!unpowered.get_value(&BlockStateProperties::OPEN));
263    }
264}