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