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