Skip to main content

steel_core/behavior/blocks/building/
door_block.rs

1//! Door block behavior implementation.
2//!
3//! Doors keep their upper and lower halves synchronized through vanilla
4//! neighbor-shape updates and react to redstone power on either half.
5
6use std::sync::Arc;
7
8use steel_macros::block_behavior;
9use steel_registry::{
10    blocks::{
11        BlockRef,
12        block_state_ext::BlockStateExt as _,
13        properties::{
14            BlockStateProperties, BoolProperty, Direction, DoorHingeSide, DoubleBlockHalf,
15            EnumProperty,
16        },
17        shapes,
18    },
19    sound_event::SoundEventRef,
20    vanilla_blocks, vanilla_game_events,
21};
22use steel_utils::{
23    BlockPos, BlockStateId,
24    axis::Axis,
25    types::{InteractionHand, UpdateFlags},
26};
27
28use super::weathering_block::{WeatherState, WeatheringCopper};
29use crate::{
30    behavior::{
31        BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
32        PlacementSource,
33    },
34    entity::Entity,
35    entity::ai::path::PathComputationType,
36    fluid::fluid_state_to_block,
37    player::Player,
38    world::{
39        LevelReader, ScheduledTickAccess, SignalGetter as _, World, game_event::GameEventContext,
40    },
41};
42
43/// Behavior for vanilla door blocks.
44#[block_behavior]
45pub struct DoorBlock {
46    block: BlockRef,
47    #[json_arg(value, json = "type_can_open_by_hand")]
48    can_open_by_hand: bool,
49    #[json_arg(sound_events, json = "type_door_open")]
50    sound_open: SoundEventRef,
51    #[json_arg(sound_events, json = "type_door_close")]
52    sound_close: SoundEventRef,
53}
54
55const DOOR_HINGE: &EnumProperty<DoorHingeSide> = &BlockStateProperties::DOOR_HINGE;
56const DOUBLE_BLOCK_HALF: &EnumProperty<DoubleBlockHalf> = &BlockStateProperties::DOUBLE_BLOCK_HALF;
57const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
58const OPEN: &BoolProperty = &BlockStateProperties::OPEN;
59const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
60
61impl DoorBlock {
62    const USE_UPDATE_FLAGS: UpdateFlags =
63        UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_IMMEDIATE);
64
65    /// Creates a new door block behavior.
66    #[must_use]
67    pub const fn new(
68        block: BlockRef,
69        can_open_by_hand: bool,
70        sound_open: SoundEventRef,
71        sound_close: SoundEventRef,
72    ) -> Self {
73        Self {
74            block,
75            can_open_by_hand,
76            sound_open,
77            sound_close,
78        }
79    }
80
81    fn is_door(state: BlockStateId) -> bool {
82        state.try_get_value(DOOR_HINGE).is_some()
83            && state.try_get_value(DOUBLE_BLOCK_HALF).is_some()
84    }
85
86    fn is_lower_door(state: BlockStateId) -> bool {
87        Self::is_door(state) && state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower
88    }
89
90    fn hinge_for_placement(context: &BlockPlaceContext<'_>) -> DoorHingeSide {
91        let pos = context.place_pos();
92        let above_pos = pos.above();
93        let place_direction = context.horizontal_direction();
94
95        let left_direction = place_direction.rotate_y_counter_clockwise();
96        let left_pos = left_direction.relative(pos);
97        let left_state = context.world.get_block_state(left_pos);
98        let left_above_pos = left_direction.relative(above_pos);
99        let left_above_state = context.world.get_block_state(left_above_pos);
100
101        let right_direction = place_direction.rotate_y_clockwise();
102        let right_pos = right_direction.relative(pos);
103        let right_state = context.world.get_block_state(right_pos);
104        let right_above_pos = right_direction.relative(above_pos);
105        let right_above_state = context.world.get_block_state(right_above_pos);
106
107        let solid_block_balance = i32::from(shapes::is_offset_shape_full_block(
108            right_state.get_collision_shape_at(right_pos),
109        )) + i32::from(shapes::is_offset_shape_full_block(
110            right_above_state.get_collision_shape_at(right_above_pos),
111        )) - i32::from(shapes::is_offset_shape_full_block(
112            left_state.get_collision_shape_at(left_pos),
113        )) - i32::from(shapes::is_offset_shape_full_block(
114            left_above_state.get_collision_shape_at(left_above_pos),
115        ));
116
117        let door_left = Self::is_lower_door(left_state);
118        let door_right = Self::is_lower_door(right_state);
119
120        if (!door_left || door_right) && solid_block_balance <= 0 {
121            if (!door_right || door_left) && solid_block_balance >= 0 {
122                let (step_x, step_z) = place_direction.offset_xz();
123                let click_x = context.click_location().x - f64::from(pos.x());
124                let click_z = context.click_location().z - f64::from(pos.z());
125
126                if (step_x >= 0 || click_z >= 0.5)
127                    && (step_x <= 0 || click_z <= 0.5)
128                    && (step_z >= 0 || click_x <= 0.5)
129                    && (step_z <= 0 || click_x >= 0.5)
130                {
131                    DoorHingeSide::Left
132                } else {
133                    DoorHingeSide::Right
134                }
135            } else {
136                DoorHingeSide::Left
137            }
138        } else {
139            DoorHingeSide::Right
140        }
141    }
142
143    fn has_correct_tool_for_drops(player: &Player, state: BlockStateId) -> bool {
144        let inv = player.inventory.lock();
145        let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
146        main_hand.is_correct_tool_for_drops(state)
147            || !state.get_block().config.requires_correct_tool_for_drops
148    }
149
150    fn prevent_drop_from_bottom_part(
151        world: &Arc<World>,
152        pos: BlockPos,
153        state: BlockStateId,
154        player: &Player,
155    ) {
156        if state.get_value(DOUBLE_BLOCK_HALF) != DoubleBlockHalf::Upper {
157            return;
158        }
159
160        let bottom_pos = pos.below();
161        let bottom_state = world.get_block_state(bottom_pos);
162        if bottom_state.get_block() != state.get_block()
163            || bottom_state.get_value(DOUBLE_BLOCK_HALF) != DoubleBlockHalf::Lower
164        {
165            return;
166        }
167
168        let replacement = fluid_state_to_block(bottom_state.get_fluid_state());
169        world.set_block(
170            bottom_pos,
171            replacement,
172            UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_SUPPRESS_DROPS,
173        );
174        world.destroy_block_effect(bottom_pos, u32::from(bottom_state.0), Some(player.id()));
175    }
176
177    fn play_sound(&self, world: &Arc<World>, pos: BlockPos, open: bool, exclude: Option<i32>) {
178        let sound = if open {
179            self.sound_open
180        } else {
181            self.sound_close
182        };
183        world.play_block_sound(sound, pos, 1.0, 1.0, exclude);
184    }
185}
186
187impl BlockBehavior for DoorBlock {
188    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
189        let pos = context.place_pos();
190        if pos.y() >= context.world.max_y_exclusive() - 1 {
191            return None;
192        }
193        if !context.world.get_block_state(pos.above()).is_replaceable() {
194            return None;
195        }
196
197        let powered = context.world.has_neighbor_signal(pos)
198            || context.world.has_neighbor_signal(pos.above());
199        Some(
200            self.block
201                .default_state()
202                .set_value(HORIZONTAL_FACING, context.horizontal_direction())
203                .set_value(DOOR_HINGE, Self::hinge_for_placement(context))
204                .set_value(POWERED, powered)
205                .set_value(OPEN, powered)
206                .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Lower),
207        )
208    }
209
210    fn update_shape(
211        &self,
212        state: BlockStateId,
213        world: &dyn ScheduledTickAccess,
214        pos: BlockPos,
215        direction: Direction,
216        _neighbor_pos: BlockPos,
217        neighbor_state: BlockStateId,
218    ) -> BlockStateId {
219        let half = state.get_value(DOUBLE_BLOCK_HALF);
220        if direction.get_axis() == Axis::Y
221            && (half == DoubleBlockHalf::Lower) == (direction == Direction::Up)
222        {
223            if Self::is_door(neighbor_state) && neighbor_state.get_value(DOUBLE_BLOCK_HALF) != half
224            {
225                return neighbor_state.set_value(DOUBLE_BLOCK_HALF, half);
226            }
227            return vanilla_blocks::AIR.default_state();
228        }
229
230        if half == DoubleBlockHalf::Lower
231            && direction == Direction::Down
232            && !self.can_survive(state, world, pos)
233        {
234            return vanilla_blocks::AIR.default_state();
235        }
236
237        state
238    }
239
240    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
241        let below_pos = pos.below();
242        let below_state = world.get_block_state(below_pos);
243        if state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower {
244            world.is_face_sturdy(below_state, below_pos, Direction::Up)
245        } else {
246            below_state.get_block() == self.block
247        }
248    }
249
250    fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
251        match computation_type {
252            PathComputationType::Land | PathComputationType::Air => state.get_value(OPEN),
253            PathComputationType::Water => false,
254        }
255    }
256
257    fn is_wooden_door(&self, state: BlockStateId) -> bool {
258        self.can_open_by_hand && Self::is_door(state)
259    }
260
261    fn set_door_open(
262        &self,
263        state: BlockStateId,
264        world: &Arc<World>,
265        pos: BlockPos,
266        source_entity: Option<&dyn Entity>,
267        open: bool,
268    ) -> bool {
269        if !Self::is_door(state) || state.get_value(OPEN) == open {
270            return false;
271        }
272
273        let new_state = state.set_value(OPEN, open);
274        if !world.set_block(pos, new_state, Self::USE_UPDATE_FLAGS) {
275            return false;
276        }
277
278        self.play_sound(world, pos, open, source_entity.map(Entity::id));
279        let event = if open {
280            &vanilla_game_events::BLOCK_OPEN
281        } else {
282            &vanilla_game_events::BLOCK_CLOSE
283        };
284        world.game_event(event, pos, &GameEventContext::new(source_entity, None));
285        true
286    }
287
288    fn set_placed_by(
289        &self,
290        state: BlockStateId,
291        world: &Arc<World>,
292        pos: BlockPos,
293        _source: &PlacementSource<'_>,
294    ) {
295        world.set_block(
296            pos.above(),
297            state.set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Upper),
298            UpdateFlags::UPDATE_ALL,
299        );
300    }
301
302    fn player_will_destroy(
303        &self,
304        state: BlockStateId,
305        world: &Arc<World>,
306        pos: BlockPos,
307        player: &Player,
308    ) -> BlockStateId {
309        if player.has_infinite_materials() || !Self::has_correct_tool_for_drops(player, state) {
310            Self::prevent_drop_from_bottom_part(world, pos, state, player);
311        }
312        state
313    }
314
315    fn use_without_item(
316        &self,
317        state: BlockStateId,
318        world: &Arc<World>,
319        pos: BlockPos,
320        player: &Player,
321        _hit_result: &BlockHitResult,
322        _inv: &mut InventoryAccess,
323    ) -> InteractionResult {
324        if !self.can_open_by_hand {
325            return InteractionResult::Pass;
326        }
327
328        let open = !state.get_value(OPEN);
329        let new_state = state.set_value(OPEN, open);
330        world.set_block(pos, new_state, Self::USE_UPDATE_FLAGS);
331        self.play_sound(world, pos, open, Some(player.id()));
332        let event = if open {
333            &vanilla_game_events::BLOCK_OPEN
334        } else {
335            &vanilla_game_events::BLOCK_CLOSE
336        };
337        world.game_event(event, pos, &GameEventContext::new(Some(player), None));
338        InteractionResult::Success
339    }
340
341    fn handle_neighbor_changed(
342        &self,
343        state: BlockStateId,
344        world: &Arc<World>,
345        pos: BlockPos,
346        source_block: BlockRef,
347        _moved_by_piston: bool,
348    ) {
349        if source_block == self.block {
350            return;
351        }
352
353        let half = state.get_value(DOUBLE_BLOCK_HALF);
354        let other_half_pos = if half == DoubleBlockHalf::Lower {
355            pos.above()
356        } else {
357            pos.below()
358        };
359        let signal = world.has_neighbor_signal(pos) || world.has_neighbor_signal(other_half_pos);
360        if signal == state.get_value(POWERED) {
361            return;
362        }
363
364        if signal != state.get_value(OPEN) {
365            self.play_sound(world, pos, signal, None);
366            let event = if signal {
367                &vanilla_game_events::BLOCK_OPEN
368            } else {
369                &vanilla_game_events::BLOCK_CLOSE
370            };
371            world.game_event(event, pos, &GameEventContext::default());
372        }
373
374        let new_state = state.set_value(POWERED, signal).set_value(OPEN, signal);
375        world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
376    }
377}
378
379/// Weathering copper doors share door behavior and add copper aging.
380#[block_behavior]
381pub struct WeatheringCopperDoorBlock {
382    block: BlockRef,
383    #[json_arg(r#enum = "WeatherState", json = "weather_state")]
384    weathering: WeatheringCopper,
385    #[json_arg(value, json = "type_can_open_by_hand")]
386    can_open_by_hand: bool,
387    #[json_arg(sound_events, json = "type_door_open")]
388    sound_open: SoundEventRef,
389    #[json_arg(sound_events, json = "type_door_close")]
390    sound_close: SoundEventRef,
391}
392
393impl WeatheringCopperDoorBlock {
394    /// Creates a new weathering copper door behavior.
395    #[must_use]
396    pub const fn new(
397        block: BlockRef,
398        weather_state: WeatherState,
399        can_open_by_hand: bool,
400        sound_open: SoundEventRef,
401        sound_close: SoundEventRef,
402    ) -> Self {
403        Self {
404            block,
405            weathering: WeatheringCopper::new(weather_state),
406            can_open_by_hand,
407            sound_open,
408            sound_close,
409        }
410    }
411
412    const fn door(&self) -> DoorBlock {
413        DoorBlock::new(
414            self.block,
415            self.can_open_by_hand,
416            self.sound_open,
417            self.sound_close,
418        )
419    }
420}
421
422impl BlockBehavior for WeatheringCopperDoorBlock {
423    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
424        self.door().get_state_for_placement(context)
425    }
426
427    fn update_shape(
428        &self,
429        state: BlockStateId,
430        world: &dyn ScheduledTickAccess,
431        pos: BlockPos,
432        direction: Direction,
433        neighbor_pos: BlockPos,
434        neighbor_state: BlockStateId,
435    ) -> BlockStateId {
436        self.door()
437            .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
438    }
439
440    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
441        self.door().can_survive(state, world, pos)
442    }
443
444    fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
445        self.door().is_pathfindable(state, computation_type)
446    }
447
448    fn is_wooden_door(&self, state: BlockStateId) -> bool {
449        self.door().is_wooden_door(state)
450    }
451
452    fn set_door_open(
453        &self,
454        state: BlockStateId,
455        world: &Arc<World>,
456        pos: BlockPos,
457        source_entity: Option<&dyn Entity>,
458        open: bool,
459    ) -> bool {
460        self.door()
461            .set_door_open(state, world, pos, source_entity, open)
462    }
463
464    fn set_placed_by(
465        &self,
466        state: BlockStateId,
467        world: &Arc<World>,
468        pos: BlockPos,
469        source: &PlacementSource<'_>,
470    ) {
471        self.door().set_placed_by(state, world, pos, source);
472    }
473
474    fn player_will_destroy(
475        &self,
476        state: BlockStateId,
477        world: &Arc<World>,
478        pos: BlockPos,
479        player: &Player,
480    ) -> BlockStateId {
481        self.door().player_will_destroy(state, world, pos, player)
482    }
483
484    fn use_without_item(
485        &self,
486        state: BlockStateId,
487        world: &Arc<World>,
488        pos: BlockPos,
489        player: &Player,
490        hit_result: &BlockHitResult,
491        inv: &mut InventoryAccess,
492    ) -> InteractionResult {
493        self.door()
494            .use_without_item(state, world, pos, player, hit_result, inv)
495    }
496
497    fn handle_neighbor_changed(
498        &self,
499        state: BlockStateId,
500        world: &Arc<World>,
501        pos: BlockPos,
502        source_block: BlockRef,
503        moved_by_piston: bool,
504    ) {
505        self.door()
506            .handle_neighbor_changed(state, world, pos, source_block, moved_by_piston);
507    }
508
509    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
510        if state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower {
511            self.weathering.change_over_time(state, world, pos);
512        }
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks};
519    use steel_utils::BlockPos;
520
521    use crate::test_support::TestLevel;
522
523    use super::*;
524
525    #[test]
526    fn lower_half_copies_transformed_upper_half_state() {
527        init_vanilla_registry();
528        let behavior = DoorBlock::new(
529            &vanilla_blocks::SPRUCE_DOOR,
530            true,
531            &sound_events::BLOCK_WOODEN_DOOR_OPEN,
532            &sound_events::BLOCK_WOODEN_DOOR_CLOSE,
533        );
534        let lower = vanilla_blocks::SPRUCE_DOOR
535            .default_state()
536            .set_value(HORIZONTAL_FACING, Direction::West)
537            .set_value(DOOR_HINGE, DoorHingeSide::Right)
538            .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Lower)
539            .set_value(OPEN, false)
540            .set_value(POWERED, false);
541        let upper = vanilla_blocks::SPRUCE_DOOR
542            .default_state()
543            .set_value(HORIZONTAL_FACING, Direction::South)
544            .set_value(DOOR_HINGE, DoorHingeSide::Left)
545            .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Upper)
546            .set_value(OPEN, false)
547            .set_value(POWERED, false);
548        let level = TestLevel::default();
549
550        let updated = behavior.update_shape(
551            lower,
552            &level,
553            BlockPos::ZERO,
554            Direction::Up,
555            BlockPos::ZERO.above(),
556            upper,
557        );
558
559        assert_eq!(updated.get_value(DOUBLE_BLOCK_HALF), DoubleBlockHalf::Lower);
560        assert_eq!(updated.get_value(HORIZONTAL_FACING), Direction::South);
561        assert_eq!(updated.get_value(DOOR_HINGE), DoorHingeSide::Left);
562    }
563
564    #[test]
565    fn door_wooden_query_uses_can_open_by_hand_like_vanilla() {
566        init_vanilla_registry();
567        let oak = DoorBlock::new(
568            &vanilla_blocks::OAK_DOOR,
569            true,
570            &sound_events::BLOCK_WOODEN_DOOR_OPEN,
571            &sound_events::BLOCK_WOODEN_DOOR_CLOSE,
572        );
573        let iron = DoorBlock::new(
574            &vanilla_blocks::IRON_DOOR,
575            false,
576            &sound_events::BLOCK_IRON_DOOR_OPEN,
577            &sound_events::BLOCK_IRON_DOOR_CLOSE,
578        );
579
580        assert!(oak.is_wooden_door(vanilla_blocks::OAK_DOOR.default_state()));
581        assert!(!iron.is_wooden_door(vanilla_blocks::IRON_DOOR.default_state()));
582        assert!(!oak.is_wooden_door(vanilla_blocks::STONE.default_state()));
583    }
584}