Skip to main content

steel_core/behavior/blocks/container/
shulker_box_block.rs

1use std::sync::{Arc, Weak};
2
3use steel_macros::block_behavior;
4use steel_registry::{
5    block_entity_type::BlockEntityTypeRef,
6    blocks::{
7        BlockRef,
8        block_state_ext::BlockStateExt,
9        properties::{BlockStateProperties, EnumProperty},
10        shapes::VoxelShape,
11    },
12    item_stack::ItemStack,
13    items::item::BlockHitResult,
14    vanilla_block_entity_types, vanilla_custom_stats,
15};
16use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId, Direction, Downcast, translations};
17use text_components::TextComponent;
18
19use crate::{
20    behavior::{
21        BlockBehavior, BlockCollisionBoxes, BlockCollisionContext, BlockEntityCreation,
22        BlockLootContext, BlockPlaceContext, InteractionResult, InventoryAccess,
23    },
24    block_entity::{
25        BLOCK_ENTITIES, BlockEntity, BlockEntityTicker,
26        entities::{AnimationStatus, ShulkerBoxBlockEntity},
27    },
28    inventory::{
29        container::calculate_redstone_signal_from_container,
30        lock::{ContainerLockGuard, ContainerRef},
31        menu::kinds::shulker_box,
32    },
33    player::Player,
34    world::{LevelReader, World},
35};
36
37/// Behavior for shulker box blocks.
38#[block_behavior]
39pub struct ShulkerBoxBlock {
40    block: BlockRef,
41}
42
43impl ShulkerBoxBlock {
44    /// Face the shulker box's lid opens towards.
45    pub const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
46
47    /// Creates a new shulker block behavior.
48    #[must_use]
49    pub const fn new(block: BlockRef) -> Self {
50        Self { block }
51    }
52}
53
54// Vanilla `ShulkerBoxBlock.SHAPES_OPEN_SUPPORT`, derived there with
55// `Shapes.rotateAll(Block.boxZ(16.0, 0.0, 1.0))`. Rotation is not available at
56// compile time here, so each rotated result is written out directly.
57const OPEN_SUPPORT_DOWN_BOXES: &[BlockLocalAabb] =
58    &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.0625, 1.0)];
59const OPEN_SUPPORT_UP_BOXES: &[BlockLocalAabb] =
60    &[BlockLocalAabb::new(0.0, 0.9375, 0.0, 1.0, 1.0, 1.0)];
61const OPEN_SUPPORT_NORTH_BOXES: &[BlockLocalAabb] =
62    &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0625)];
63const OPEN_SUPPORT_SOUTH_BOXES: &[BlockLocalAabb] =
64    &[BlockLocalAabb::new(0.0, 0.0, 0.9375, 1.0, 1.0, 1.0)];
65const OPEN_SUPPORT_WEST_BOXES: &[BlockLocalAabb] =
66    &[BlockLocalAabb::new(0.0, 0.0, 0.0, 0.0625, 1.0, 1.0)];
67const OPEN_SUPPORT_EAST_BOXES: &[BlockLocalAabb] =
68    &[BlockLocalAabb::new(0.9375, 0.0, 0.0, 1.0, 1.0, 1.0)];
69
70/// The one-pixel slab covering `direction`'s face, all that still supports the
71/// block once the lid has started opening.
72const fn open_support_shape(direction: Direction) -> VoxelShape {
73    VoxelShape::from_boxes(match direction {
74        Direction::Down => OPEN_SUPPORT_DOWN_BOXES,
75        Direction::Up => OPEN_SUPPORT_UP_BOXES,
76        Direction::North => OPEN_SUPPORT_NORTH_BOXES,
77        Direction::South => OPEN_SUPPORT_SOUTH_BOXES,
78        Direction::West => OPEN_SUPPORT_WEST_BOXES,
79        Direction::East => OPEN_SUPPORT_EAST_BOXES,
80    })
81}
82
83impl BlockBehavior for ShulkerBoxBlock {
84    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
85        let facing = context.clicked_face();
86        Some(
87            self.block
88                .default_state()
89                .set_value(&BlockStateProperties::FACING, facing),
90        )
91    }
92
93    fn use_without_item(
94        &self,
95        state: BlockStateId,
96        world: &Arc<World>,
97        pos: BlockPos,
98        player: &Player,
99        _hit_result: &BlockHitResult,
100        _inv: &mut InventoryAccess,
101    ) -> InteractionResult {
102        if let Some(block_entity) = world.get_block_entity(pos)
103            && let Some(block_entity) = block_entity.downcast_ref::<ShulkerBoxBlockEntity>()
104            && let Some(container_ref) = block_entity.container_ref()
105            && block_entity.can_open(state, world, pos)
106        {
107            let inventory = player.inventory.clone();
108            player.open_menu(
109                TextComponent::translated(translations::CONTAINER_SHULKER_BOX.msg()),
110                move |context| shulker_box(inventory, context.container_id, container_ref),
111            );
112
113            player.award_custom_stat(&vanilla_custom_stats::OPEN_SHULKER_BOX);
114            // TODO: Anger nearby piglins (PiglinAi.angerNearbyPiglins)
115        }
116        InteractionResult::Success
117    }
118
119    fn player_will_destroy(
120        &self,
121        state: BlockStateId,
122        world: &Arc<World>,
123        pos: BlockPos,
124        player: &Player,
125    ) -> BlockStateId {
126        let Some(block_entity) = world.get_block_entity(pos) else {
127            return state;
128        };
129
130        let Some(shulker_box_block_entity) = block_entity.downcast_ref::<ShulkerBoxBlockEntity>()
131        else {
132            return state;
133        };
134
135        if player.prevents_block_drops() && !shulker_box_block_entity.is_empty() {
136            let item = shulker_box_block_entity.shulker_box_as_item(state);
137            world.pop_resource(pos, item);
138        } else {
139            // TODO: maybe? shulkerBoxBlockEntity.unpackLootTable(player);
140        }
141
142        state
143    }
144
145    fn get_drops(
146        &self,
147        state: BlockStateId,
148        context: &BlockLootContext<'_>,
149    ) -> Option<Vec<ItemStack>> {
150        let block_entity = context.block_entity()?;
151        let shulker_box_block_entity = block_entity.downcast_ref::<ShulkerBoxBlockEntity>()?;
152
153        let item = shulker_box_block_entity.shulker_box_as_item(state);
154        Some(vec![item])
155    }
156
157    fn new_block_entity(
158        &self,
159        level: Weak<World>,
160        pos: BlockPos,
161        state: BlockStateId,
162    ) -> BlockEntityCreation {
163        BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
164            &vanilla_block_entity_types::SHULKER_BOX,
165            level,
166            pos,
167            state,
168        ))
169    }
170
171    fn get_block_entity_ticker(
172        &self,
173        _world: &Arc<World>,
174        _state: BlockStateId,
175        block_entity_type: BlockEntityTypeRef,
176    ) -> Option<BlockEntityTicker> {
177        BlockEntityTicker::for_matching_entity_tick(
178            block_entity_type,
179            &vanilla_block_entity_types::SHULKER_BOX,
180        )
181    }
182
183    fn trigger_event(
184        &self,
185        _state: BlockStateId,
186        world: &Arc<World>,
187        pos: BlockPos,
188        event: i32,
189        data: i32,
190    ) -> bool {
191        let Some(block_entity) = world.get_block_entity(pos) else {
192            return false;
193        };
194        block_entity.trigger_event(event, data)
195    }
196
197    /// Vanilla `ShulkerBoxBlock.getShape`, which the collision shape inherits.
198    ///
199    /// The lid box is computed from live animation progress, so the boxes are
200    /// produced directly instead of going through a static [`VoxelShape`].
201    fn get_collision_boxes(
202        &self,
203        state: BlockStateId,
204        world: &dyn LevelReader,
205        pos: BlockPos,
206        _context: BlockCollisionContext,
207    ) -> BlockCollisionBoxes {
208        if let Some(block_entity) = world.get_block_entity(pos)
209            && let Some(shulker_box_block_entity) =
210                block_entity.downcast_ref::<ShulkerBoxBlockEntity>()
211        {
212            return BlockCollisionBoxes::from_slice(&[
213                shulker_box_block_entity.get_bounding_box(state)
214            ]);
215        }
216
217        BlockCollisionBoxes::from_slice(VoxelShape::FULL_BLOCK.boxes())
218    }
219
220    fn get_block_support_boxes(
221        &self,
222        state: BlockStateId,
223        world: &dyn LevelReader,
224        pos: BlockPos,
225    ) -> BlockCollisionBoxes {
226        let shape = if let Some(block_entity) = world.get_block_entity(pos)
227            && let Some(shulker_box_block_entity) =
228                block_entity.downcast_ref::<ShulkerBoxBlockEntity>()
229            && !matches!(
230                shulker_box_block_entity.animation_status(),
231                AnimationStatus::Closed
232            ) {
233            open_support_shape(state.get_value(Self::FACING).opposite())
234        } else {
235            VoxelShape::FULL_BLOCK
236        };
237
238        BlockCollisionBoxes::from_slice(shape.boxes())
239    }
240
241    fn affect_neighbors_after_removal(
242        &self,
243        _state: BlockStateId,
244        world: &Arc<World>,
245        pos: BlockPos,
246        _moved_by_piston: bool,
247    ) {
248        world.update_neighbor_for_output_signal(pos, self.block);
249    }
250
251    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
252        true
253    }
254
255    fn get_analog_output_signal(
256        &self,
257        _state: BlockStateId,
258        world: &dyn LevelReader,
259        pos: BlockPos,
260        _direction: Direction,
261    ) -> i32 {
262        let Some(container_ref) = world
263            .get_block_entity(pos)
264            .and_then(ContainerRef::from_block_entity)
265        else {
266            return 0;
267        };
268        let guard = ContainerLockGuard::lock_all(&[&container_ref]);
269        guard
270            .get(container_ref.container_id())
271            .map_or(0, |container| {
272                calculate_redstone_signal_from_container(container)
273            })
274    }
275
276    fn fits_inside_container_items(&self) -> bool {
277        false
278    }
279}