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