steel_core/block_entity/entities/
shulker_box.rs1use 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
39pub const SHULKER_BOX_SLOTS: usize = 27;
41const ANIMATION_STEPS: u8 = 10;
42
43#[derive(Debug, Clone, Copy)]
45pub enum AnimationStatus {
46 Closed,
48 Opening,
50 Opened,
52 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
72pub 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
85unsafe impl DowncastType for ShulkerBoxBlockEntity {
87 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/shulker_box");
88}
89
90unsafe 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 #[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 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 #[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 #[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 #[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 #[must_use]
272 pub fn is_empty(&self) -> bool {
273 self.container.lock().is_empty()
274 }
275
276 #[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 #[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 #[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}