Skip to main content

steel_core/block_entity/entities/
shulker_box.rs

1use std::sync::{
2    Arc, Weak,
3    atomic::{AtomicI32, Ordering},
4};
5
6use glam::DVec3;
7use simdnbt::{
8    ToNbtTag,
9    borrow::{BaseNbtCompound, NbtCompound as NbtCompoundView},
10    owned::{NbtCompound, NbtList, NbtTag},
11};
12use steel_registry::{
13    ItemStackTemplate, REGISTRY,
14    blocks::{
15        behavior::PushReaction, block_state_ext::BlockStateExt, properties::BlockStateProperties,
16    },
17    data_components::{DataComponentPatch, ItemContainerContents, vanilla_components::CONTAINER},
18    item_stack::ItemStack,
19    vanilla_block_entity_types,
20};
21use steel_utils::{
22    BlockLocalAabb, BlockPos, BlockStateId, Direction, DowncastType, DowncastTypeKey, WorldAabb,
23    geometry::{Aabb, Space},
24    locks::SyncMutex,
25    types::UpdateFlags,
26};
27
28use crate::{
29    behavior::blocks::ShulkerBoxBlock,
30    block_entity::{BlockEntity, BlockEntityBase},
31    inventory::{
32        container::Container,
33        lock::{ContainerRef, SharedContainer},
34    },
35    physics::{CollisionWorld as _, MoverType, WorldCollisionProvider},
36    world::World,
37};
38
39/// Number of slots in a shulker box (3 rows of 9).
40pub const SHULKER_BOX_SLOTS: usize = 27;
41const ANIMATION_STEPS: u8 = 10;
42
43/// The current animation state of a shulker box.
44#[derive(Debug, Clone, Copy)]
45pub enum AnimationStatus {
46    /// Fully closed.
47    Closed,
48    /// Opening.
49    Opening,
50    /// Fully open.
51    Opened,
52    /// Closing.
53    Closing,
54}
55
56pub struct ShulkerBoxAnimation {
57    animation_status: AnimationStatus,
58    progress: u8,
59    old_progress: u8,
60}
61
62impl ShulkerBoxAnimation {
63    fn progress(&self) -> f32 {
64        f32::from(self.progress) / f32::from(ANIMATION_STEPS)
65    }
66
67    fn old_progress(&self) -> f32 {
68        f32::from(self.old_progress) / f32::from(ANIMATION_STEPS)
69    }
70}
71
72/// Block entity backing every shulker box color, including the undyed one.
73pub struct ShulkerBoxBlockEntity {
74    base: Arc<BlockEntityBase>,
75    container: Arc<SyncMutex<ShulkerBoxContainer>>,
76    container_ref: ContainerRef,
77    animation: SyncMutex<ShulkerBoxAnimation>,
78    open_count: AtomicI32,
79}
80
81struct ShulkerBoxContainer {
82    items: Vec<ItemStack>,
83}
84
85// SAFETY: This key is owned by Steel and uniquely identifies `ShulkerBoxEntity`.
86unsafe impl DowncastType for ShulkerBoxBlockEntity {
87    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/shulker_box");
88}
89
90// SAFETY: This key is owned by Steel and uniquely identifies the independently
91// lockable inventory data used by a shulker box block entity.
92unsafe impl DowncastType for ShulkerBoxContainer {
93    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:container/shulker_box");
94}
95
96fn do_neighbor_updates(world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
97    world.update_neighbour_shapes(state, pos, UpdateFlags::UPDATE_ALL, 512);
98    world.update_neighbors_at(pos, state.get_block());
99}
100
101impl ShulkerBoxBlockEntity {
102    /// Creates a new shulker box block entity.
103    #[must_use]
104    pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
105        let base = Arc::new(BlockEntityBase::new(
106            &vanilla_block_entity_types::SHULKER_BOX,
107            level,
108            pos,
109            state,
110        ));
111        let container = Arc::new(SyncMutex::new(ShulkerBoxContainer {
112            items: vec![ItemStack::empty(); SHULKER_BOX_SLOTS],
113        }));
114        let shared_container: SharedContainer = container.clone();
115        Self {
116            container_ref: ContainerRef::owned_by_block_entity(shared_container, Arc::clone(&base)),
117            base,
118            container,
119            animation: SyncMutex::new(ShulkerBoxAnimation {
120                animation_status: AnimationStatus::Closed,
121                progress: 0,
122                old_progress: 0,
123            }),
124            open_count: AtomicI32::new(0),
125        }
126    }
127
128    fn update_animation(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
129        let mut animation = self.animation.lock();
130        animation.old_progress = animation.progress;
131        match animation.animation_status {
132            AnimationStatus::Closed => animation.progress = 0,
133            AnimationStatus::Opening => {
134                animation.progress += 1;
135                if animation.old_progress == 0 {
136                    do_neighbor_updates(world, pos, state);
137                }
138
139                if animation.progress >= ANIMATION_STEPS {
140                    animation.animation_status = AnimationStatus::Opened;
141                    animation.progress = ANIMATION_STEPS;
142                    do_neighbor_updates(world, pos, state);
143                }
144
145                drop(animation);
146                self.move_collided_entities(world, pos, state);
147            }
148            AnimationStatus::Opened => animation.progress = ANIMATION_STEPS,
149            AnimationStatus::Closing => {
150                animation.progress = animation.progress.saturating_sub(1);
151                if animation.old_progress == ANIMATION_STEPS {
152                    do_neighbor_updates(world, pos, state);
153                }
154
155                if animation.progress == 0 {
156                    animation.animation_status = AnimationStatus::Closed;
157                    animation.progress = 0;
158                    do_neighbor_updates(world, pos, state);
159                }
160            }
161        }
162    }
163
164    fn move_collided_entities(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
165        let direction = state.get_value(&BlockStateProperties::FACING);
166
167        let aabb: WorldAabb = {
168            let animation = self.animation.lock();
169            Self::get_progress_delta_aabb(
170                1.0,
171                direction,
172                animation.old_progress(),
173                animation.progress(),
174                DVec3::from(pos.get_bottom_center()),
175            )
176        };
177
178        let entities = world.get_entities_in_aabb(&aabb);
179        for entity in entities {
180            if entity.piston_push_reaction() == PushReaction::Ignore {
181                continue;
182            }
183            let (offset_x, offset_y, offset_z) = direction.offset();
184            entity.move_entity(
185                MoverType::ShulkerBox,
186                DVec3::new(
187                    (aabb.width() + 0.01) * f64::from(offset_x),
188                    (aabb.height() + 0.01) * f64::from(offset_y),
189                    (aabb.depth() + 0.01) * f64::from(offset_z),
190                ),
191            );
192        }
193    }
194
195    /// Builds the item form of a placed, possibly-filled shulker box.
196    /// Vanilla `ShulkerBoxBlockEntity.collectComponents()` +
197    /// `BaseContainerBlockEntity.collectImplicitComponents(CONTAINER)`.
198    pub fn shulker_box_as_item(&self, state: BlockStateId) -> ItemStack {
199        let block_item = REGISTRY.items.by_block(state.get_block());
200
201        let contents = self.collect_components();
202
203        let mut patch = DataComponentPatch::new();
204        patch.set(CONTAINER, contents);
205
206        ItemStack::with_count_and_patch(block_item, 1, patch)
207    }
208
209    /// Calculates the bounding box of a shulker during its opening animation.
210    ///
211    /// The bounds are expanded in the opening direction based on the animation progress.
212    #[must_use]
213    pub fn get_progress_delta_aabb<I: Space>(
214        size: f32,
215        direction: Direction,
216        progress_from: f32,
217        progress_to: f32,
218        position: DVec3,
219    ) -> Aabb<DVec3, I> {
220        let size = f64::from(size);
221        let bounds =
222            Aabb::<DVec3, I>::new(-size * 0.5, 0.0, -size * 0.5, size * 0.5, size, size * 0.5);
223
224        let max_movement = f64::from(progress_from.max(progress_to));
225        let min_movement = f64::from(progress_from.min(progress_to));
226
227        let dir = DVec3::from(direction.offset_vec());
228
229        bounds
230            .expand_towards(dir * max_movement * size)
231            .contract(-dir * (1.0 + min_movement) * size)
232            .translate(position)
233    }
234
235    /// Block-local bounds of the box including the lid at its current progress.
236    #[must_use]
237    pub fn get_bounding_box(&self, state: BlockStateId) -> BlockLocalAabb {
238        let bottom_center = DVec3::new(0.5, 0.0, 0.5);
239        Self::get_progress_delta_aabb(
240            1.0,
241            state.get_value(ShulkerBoxBlock::FACING),
242            -1.0,
243            0.5 * self.progress(1.0),
244            bottom_center,
245        )
246    }
247
248    /// Returns whether the shulker box can open
249    #[must_use]
250    pub fn can_open(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) -> bool {
251        if !matches!(self.animation_status(), AnimationStatus::Closed) {
252            return true;
253        }
254
255        let direction = state.get_value(ShulkerBoxBlock::FACING);
256
257        let lid_open_bounding_box: WorldAabb = Self::get_progress_delta_aabb(
258            1.0,
259            direction,
260            0.0,
261            0.5,
262            DVec3::from(pos.get_bottom_center()),
263        )
264        .deflate(1.0E-6);
265
266        let collision = WorldCollisionProvider::new(world);
267        !collision.has_block_collision(&lid_open_bounding_box)
268    }
269
270    /// Checks if the block entity's container has any items
271    #[must_use]
272    pub fn is_empty(&self) -> bool {
273        self.container.lock().is_empty()
274    }
275
276    /// Collect all the items inside the shulker box into an `ItemContainerContents`
277    ///
278    /// # Panics
279    /// Panics if shulker box somehow has more than 256 slots. This should never happen
280    #[must_use]
281    pub fn collect_components(&self) -> ItemContainerContents {
282        let container = self.container.lock();
283
284        let size = container.get_container_size();
285        let slots: Vec<Option<ItemStackTemplate>> = (0..size)
286            .map(|i| {
287                let item = container.get_item(i);
288                if item.is_empty() {
289                    None
290                } else {
291                    ItemStackTemplate::from_stack(item).ok()
292                }
293            })
294            .collect();
295
296        ItemContainerContents::new(slots)
297            .expect("shulker box slot count is always within the 256 container-contents limit")
298    }
299
300    /// Interpolated progress for smooth rendering between ticks.
301    #[must_use]
302    pub fn progress(&self, partial_tick: f32) -> f32 {
303        let animation = self.animation.lock();
304        let new = animation.progress();
305        let old = animation.old_progress();
306        old + partial_tick * (new - old)
307    }
308
309    /// Get the current animation state
310    #[must_use]
311    pub fn animation_status(&self) -> AnimationStatus {
312        self.animation.lock().animation_status
313    }
314}
315
316impl BlockEntity for ShulkerBoxBlockEntity {
317    fn base(&self) -> &BlockEntityBase {
318        &self.base
319    }
320
321    fn tick(&self, world: &Arc<World>) {
322        self.update_animation(world, self.base.pos, self.base.block_state());
323    }
324
325    fn trigger_event(&self, kind: i32, data: i32) -> bool {
326        if kind == 1 {
327            self.open_count.store(data, Ordering::Relaxed);
328            if data == 0 {
329                self.animation.lock().animation_status = AnimationStatus::Closing;
330            }
331
332            if data == 1 {
333                self.animation.lock().animation_status = AnimationStatus::Opening;
334            }
335
336            true
337        } else {
338            false
339        }
340    }
341
342    fn load_additional(&self, nbt: &BaseNbtCompound<'_>) {
343        let nbt_view: NbtCompoundView<'_, '_> = nbt.into();
344        let mut container = self.container.lock();
345        container.items.fill(ItemStack::empty());
346
347        let Some(items_list) = nbt_view.list("Items") else {
348            return;
349        };
350        let Some(compounds) = items_list.compounds() else {
351            return;
352        };
353
354        for compound in compounds {
355            let Some(slot) = compound.byte("Slot") else {
356                continue;
357            };
358            let slot = slot as usize;
359            if slot >= SHULKER_BOX_SLOTS {
360                continue;
361            }
362            if let Some(item) = ItemStack::from_borrowed_compound(&compound) {
363                container.items[slot] = item;
364            }
365        }
366    }
367
368    fn save_additional(&self, nbt: &mut NbtCompound) {
369        let container = self.container.lock();
370        let mut items: Vec<NbtCompound> = Vec::new();
371        for (slot, item) in container.items().iter().enumerate() {
372            if item.is_empty() {
373                continue;
374            }
375            let NbtTag::Compound(mut item_nbt) = item.clone().to_nbt_tag() else {
376                continue;
377            };
378            item_nbt.insert("Slot", slot as i8);
379            items.push(item_nbt);
380        }
381        nbt.insert("Items", NbtList::Compound(items));
382    }
383
384    fn apply_components_from_item(&self, item: &ItemStack) {
385        let Some(contents) = item.get(CONTAINER) else {
386            return;
387        };
388
389        let mut container = self.container.lock();
390        container.items.fill(ItemStack::empty());
391        for (slot, template) in contents.items().iter().enumerate() {
392            if slot >= SHULKER_BOX_SLOTS {
393                break;
394            }
395            if let Some(template) = template {
396                container.items_mut()[slot] = ItemStack::with_count_and_patch(
397                    template.item(),
398                    template.count(),
399                    template.components().clone(),
400                );
401            }
402        }
403    }
404
405    fn container_ref(&self) -> Option<ContainerRef> {
406        Some(self.container_ref.clone())
407    }
408}
409
410impl Container for ShulkerBoxContainer {
411    fn items(&self) -> &[ItemStack] {
412        &self.items
413    }
414
415    fn items_mut(&mut self) -> &mut [ItemStack] {
416        &mut self.items
417    }
418
419    fn get_container_size(&self) -> usize {
420        SHULKER_BOX_SLOTS
421    }
422
423    fn set_item(&mut self, slot: usize, mut stack: ItemStack) {
424        if slot < SHULKER_BOX_SLOTS {
425            let max_stack_size = self.get_max_stack_size_for_item(&stack);
426            if !stack.is_empty() && stack.count() > max_stack_size {
427                stack.set_count(max_stack_size);
428            }
429            self.items[slot] = stack;
430        }
431    }
432
433    fn get_max_stack_size(&self) -> i32 {
434        64
435    }
436
437    fn set_changed(&mut self) {}
438}