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    /// A gate connects perpendicular to its facing, i.e. to a wall/fence whose
67    /// connecting axis matches the gate's clockwise-rotated facing axis.
68    #[must_use]
69    pub fn connects_to_direction(state: BlockStateId, direction: Direction) -> bool {
70        state.get_value(FACING).axis() == direction.rotate_y_clockwise().axis()
71    }
72
73    fn is_wall(state: BlockStateId) -> bool {
74        state.get_block().has_tag(&BlockTag::WALLS)
75    }
76}
77
78impl BlockBehavior for FenceGateBlock {
79    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
80        let world = context.world;
81        let pos = context.place_pos();
82        let direction = context.horizontal_direction();
83        let axis = direction.axis();
84
85        let in_wall = match axis {
86            Axis::Z => {
87                Self::is_wall(world.get_block_state(Direction::West.relative(pos)))
88                    || Self::is_wall(world.get_block_state(Direction::East.relative(pos)))
89            }
90            Axis::X => {
91                Self::is_wall(world.get_block_state(Direction::North.relative(pos)))
92                    || Self::is_wall(world.get_block_state(Direction::South.relative(pos)))
93            }
94            Axis::Y => false,
95        };
96
97        let is_open = world.has_neighbor_signal(pos);
98
99        Some(
100            self.block
101                .default_state()
102                .set_value(FACING, direction)
103                .set_value(OPEN, is_open)
104                .set_value(POWERED, is_open)
105                .set_value(IN_WALL, in_wall),
106        )
107    }
108
109    fn update_shape(
110        &self,
111        state: BlockStateId,
112        world: &dyn ScheduledTickAccess,
113        pos: BlockPos,
114        direction: Direction,
115        _neighbor_pos: BlockPos,
116        neighbor_state: BlockStateId,
117    ) -> BlockStateId {
118        // Only the axis perpendicular to the gate (its clockwise facing axis)
119        // can change whether it sits in a wall.
120        if state.get_value(FACING).rotate_y_clockwise().axis() != direction.axis() {
121            return state;
122        }
123        let opposite_neighbor = world.get_block_state(direction.opposite().relative(pos));
124        let in_wall = Self::is_wall(neighbor_state) || Self::is_wall(opposite_neighbor);
125        state.set_value(IN_WALL, in_wall)
126    }
127
128    fn use_without_item(
129        &self,
130        state: BlockStateId,
131        world: &Arc<World>,
132        pos: BlockPos,
133        player: &Player,
134        _hit_result: &BlockHitResult,
135        _inv: &mut InventoryAccess,
136    ) -> InteractionResult {
137        let mut new_state = state;
138        if new_state.get_value(OPEN) {
139            new_state = new_state.set_value(OPEN, false);
140        } else {
141            let player_direction = player.direction_yaw();
142            // Re-face the gate toward the player if they opened it from behind.
143            if new_state.get_value(FACING) == player_direction.opposite() {
144                new_state = new_state.set_value(FACING, player_direction);
145            }
146            new_state = new_state.set_value(OPEN, true);
147        }
148
149        // Vanilla flag 10 = UPDATE_CLIENTS | UPDATE_IMMEDIATE.
150        world.set_block(
151            pos,
152            new_state,
153            UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_IMMEDIATE,
154        );
155
156        let opens = new_state.get_value(OPEN);
157        let sound = if opens {
158            self.sound_open
159        } else {
160            self.sound_close
161        };
162        let pitch = rand::random_range(0.9..1.0);
163        world.play_block_sound(sound, pos, 1.0, pitch, Some(player.id()));
164        let event = if opens {
165            &vanilla_game_events::BLOCK_OPEN
166        } else {
167            &vanilla_game_events::BLOCK_CLOSE
168        };
169        world.game_event(event, pos, &GameEventContext::new(Some(player), None));
170        InteractionResult::Success
171    }
172
173    fn handle_neighbor_changed(
174        &self,
175        state: BlockStateId,
176        world: &Arc<World>,
177        pos: BlockPos,
178        _source_block: BlockRef,
179        _moved_by_piston: bool,
180    ) {
181        let has_power = world.has_neighbor_signal(pos);
182        if state.get_value(POWERED) == has_power {
183            return;
184        }
185
186        world.set_block(
187            pos,
188            state
189                .set_value(POWERED, has_power)
190                .set_value(OPEN, has_power),
191            UpdateFlags::UPDATE_CLIENTS,
192        );
193        if state.get_value(OPEN) == has_power {
194            return;
195        }
196
197        let sound = if has_power {
198            self.sound_open
199        } else {
200            self.sound_close
201        };
202        let pitch = rand::random_range(0.9..1.0);
203        world.play_block_sound(sound, pos, 1.0, pitch, None);
204        let event = if has_power {
205            &vanilla_game_events::BLOCK_OPEN
206        } else {
207            &vanilla_game_events::BLOCK_CLOSE
208        };
209        world.game_event(event, pos, &GameEventContext::default());
210    }
211
212    fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
213        match computation_type {
214            PathComputationType::Land | PathComputationType::Air => state.get_value(OPEN),
215            PathComputationType::Water => false,
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use steel_registry::{init_vanilla_registry, vanilla_blocks};
223    use steel_utils::{ChunkPos, types::UpdateFlags};
224
225    use super::*;
226    use crate::{
227        behavior::init_behaviors,
228        test_support::{fresh_test_world, insert_ready_full_chunk},
229    };
230
231    #[test]
232    fn redstone_power_opens_and_closes_fence_gate() {
233        init_vanilla_registry();
234        init_behaviors();
235        let world_fixture = fresh_test_world("fence_gate_redstone");
236        let world = &world_fixture.world;
237        let pos = BlockPos::new(8, 64, 8);
238        let power_pos = pos.west();
239        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
240        assert!(world.set_block(
241            pos,
242            vanilla_blocks::OAK_FENCE_GATE.default_state(),
243            UpdateFlags::UPDATE_NONE,
244        ));
245
246        assert!(world.set_block(
247            power_pos,
248            vanilla_blocks::REDSTONE_BLOCK.default_state(),
249            UpdateFlags::UPDATE_ALL,
250        ));
251        let powered = world.get_block_state(pos);
252        assert!(powered.get_value(POWERED));
253        assert!(powered.get_value(OPEN));
254
255        assert!(world.remove_block(power_pos, false));
256        let unpowered = world.get_block_state(pos);
257        assert!(!unpowered.get_value(POWERED));
258        assert!(!unpowered.get_value(OPEN));
259    }
260}