1use std::sync::{Arc, Weak};
8
9use glam::DVec3;
10use steel_macros::entity_behavior;
11use steel_registry::entity_type::EntityTypeRef;
12use steel_registry::item_stack::ItemStack;
13use steel_registry::vanilla_damage_types;
14use steel_registry::vanilla_entity_data::ItemEntityData;
15use steel_utils::UuidExt;
16use steel_utils::locks::SyncMutex;
17use steel_utils::{Downcast as _, DowncastType, DowncastTypeKey};
18use uuid::Uuid;
19
20use crate::entity::damage::DamageSource;
21
22use crate::entity::{
23 Entity, EntityBase, EntityBaseLoad, EntityBaseState, EntitySyncedData, RemovalReason,
24};
25use crate::inventory::container::Container;
26use crate::physics::MoverType;
27use crate::player::Player;
28use crate::world::World;
29
30use simdnbt::ToNbtTag;
31use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
32use simdnbt::owned::{NbtCompound, NbtTag};
33use steel_protocol::packets::game::CTakeItemEntity;
34use steel_registry::blocks::block_state_ext::BlockStateExt;
35use steel_utils::BlockPos;
36
37const LIFETIME: i32 = 6000;
39
40const DEFAULT_PICKUP_DELAY: i32 = 10;
43
44const INFINITE_PICKUP_DELAY: i32 = 32767;
46
47const INFINITE_LIFETIME: i32 = -32768;
49
50const DEFAULT_HEALTH: i32 = 5;
52
53const DEFAULT_GRAVITY: f64 = 0.04;
55
56const AIR_DRAG: f64 = 0.98;
58const FLUID_VERTICAL_NUDGE: f64 = 5.0e-4;
59const ITEM_FLUID_HEIGHT_THRESHOLD: f64 = 0.1;
60const ITEM_WATER_DRAG: f64 = 0.99;
61const ITEM_LAVA_DRAG: f64 = 0.95;
62const MERGE_MAX_STACK_SIZE: i32 = 64;
63
64struct ItemEntityState {
67 age: i32,
69 pickup_delay: i32,
71 health: i32,
73 thrower: Option<Uuid>,
75 owner: Option<Uuid>,
78}
79
80impl ItemEntityState {
81 const fn new() -> Self {
82 Self {
83 age: 0,
84 pickup_delay: 0,
85 health: DEFAULT_HEALTH,
86 thrower: None,
87 owner: None,
88 }
89 }
90}
91
92#[entity_behavior]
100pub struct ItemEntity {
101 base: EntityBase,
103
104 entity_type: EntityTypeRef,
106
107 entity_data: SyncMutex<ItemEntityData>,
109
110 item_state: SyncMutex<ItemEntityState>,
112}
113
114unsafe impl DowncastType for ItemEntity {
116 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item");
117}
118
119impl ItemEntity {
120 #[must_use]
124 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
125 Self::with_item_and_velocity(
126 entity_type,
127 id,
128 position,
129 ItemStack::empty(),
130 DVec3::ZERO,
131 world,
132 )
133 }
134
135 #[must_use]
137 pub fn with_item(
138 entity_type: EntityTypeRef,
139 id: i32,
140 position: DVec3,
141 item: ItemStack,
142 world: Weak<World>,
143 ) -> Self {
144 Self::with_item_and_velocity(
145 entity_type,
146 id,
147 position,
148 item,
149 Self::default_spawn_velocity(),
150 world,
151 )
152 }
153
154 #[must_use]
158 pub fn with_item_and_velocity(
159 entity_type: EntityTypeRef,
160 id: i32,
161 position: DVec3,
162 item: ItemStack,
163 velocity: DVec3,
164 world: Weak<World>,
165 ) -> Self {
166 let yaw = rand::random::<f32>() * 360.0;
168
169 let mut entity_data = ItemEntityData::new();
170 entity_data.item.set(item);
171
172 Self {
173 base: EntityBase::new_with_state(
174 id,
175 EntityBaseState::new(position, entity_type.dimensions)
176 .with_velocity(velocity)
177 .with_rotation((yaw, 0.0)),
178 world,
179 ),
180 entity_type,
181 entity_data: SyncMutex::new(entity_data),
182 item_state: SyncMutex::new(ItemEntityState::new()),
183 }
184 }
185
186 fn default_spawn_velocity() -> DVec3 {
187 DVec3::new(
188 rand::random::<f64>() * 0.2 - 0.1,
189 0.2,
190 rand::random::<f64>() * 0.2 - 0.1,
191 )
192 }
193
194 #[must_use]
199 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
200 Self {
201 base: EntityBase::from_load(load, entity_type.dimensions),
202 entity_type,
203 entity_data: SyncMutex::new(ItemEntityData::new()),
204 item_state: SyncMutex::new(ItemEntityState::new()),
205 }
206 }
207
208 #[must_use]
210 pub fn get_item(&self) -> ItemStack {
211 self.entity_data.lock().item.get().clone()
212 }
213
214 pub fn set_item(&self, item: ItemStack) {
216 self.entity_data.lock().item.set(item);
217 }
218
219 #[must_use]
221 pub fn get_age(&self) -> i32 {
222 self.item_state.lock().age
223 }
224
225 pub fn set_age(&self, age: i32) {
227 self.item_state.lock().age = age;
228 }
229
230 pub fn make_fake_item(&self) {
232 let mut state = self.item_state.lock();
233 state.pickup_delay = INFINITE_PICKUP_DELAY;
234 state.age = LIFETIME - 1;
235 }
236
237 #[must_use]
239 pub fn get_pickup_delay(&self) -> i32 {
240 self.item_state.lock().pickup_delay
241 }
242
243 pub fn set_default_pickup_delay(&self) {
245 self.item_state.lock().pickup_delay = DEFAULT_PICKUP_DELAY;
246 }
247
248 pub fn set_no_pickup_delay(&self) {
250 self.item_state.lock().pickup_delay = 0;
251 }
252
253 pub fn set_pickup_delay(&self, delay: i32) {
255 self.item_state.lock().pickup_delay = delay;
256 }
257
258 #[must_use]
260 pub fn has_pickup_delay(&self) -> bool {
261 self.item_state.lock().pickup_delay > 0
262 }
263
264 #[must_use]
266 pub fn get_health(&self) -> i32 {
267 self.item_state.lock().health
268 }
269
270 pub fn set_health(&self, health: i32) {
272 self.item_state.lock().health = health;
273 }
274
275 pub fn set_thrower(&self, uuid: Uuid) {
277 self.item_state.lock().thrower = Some(uuid);
278 }
279
280 #[must_use]
282 pub fn get_thrower(&self) -> Option<Uuid> {
283 self.item_state.lock().thrower
284 }
285
286 pub fn set_owner(&self, uuid: Option<Uuid>) {
291 self.item_state.lock().owner = uuid;
292 }
293
294 #[must_use]
298 pub fn get_owner(&self) -> Option<Uuid> {
299 self.item_state.lock().owner
300 }
301
302 pub fn try_pickup(&self, player: &Arc<Player>) -> bool {
309 if self.has_pickup_delay() {
311 return false;
312 }
313
314 if let Some(owner_uuid) = self.get_owner()
316 && owner_uuid != player.gameprofile.id
317 {
318 return false;
319 }
320
321 let mut item = self.get_item();
323 let original_count = item.count();
324
325 let added = player.inventory.lock().add(&mut item);
327
328 if item.count() == original_count {
330 return false;
331 }
332
333 let picked_up_count = original_count - item.count();
335
336 if let Some(world) = self.level() {
338 let pos = self.position();
339 let chunk_pos = steel_utils::ChunkPos::from_entity_pos(pos);
340
341 let take_packet = CTakeItemEntity::new(self.id(), player.id(), picked_up_count);
342 world.broadcast_to_nearby(chunk_pos, take_packet, None);
343 }
344
345 if added {
347 self.set_removed(RemovalReason::Discarded);
349 true
350 } else {
351 self.set_item(item);
353 false
354 }
355 }
356
357 #[must_use]
367 pub fn is_mergeable(&self) -> bool {
368 let item = self.get_item();
369 let state = self.item_state.lock();
370 !self.is_removed()
371 && state.pickup_delay != INFINITE_PICKUP_DELAY
372 && state.age != INFINITE_LIFETIME
373 && state.age < LIFETIME
374 && item.count() < item.max_stack_size()
375 }
376
377 #[must_use]
383 pub fn are_mergeable(this_stack: &ItemStack, other_stack: &ItemStack) -> bool {
384 if other_stack.count() + this_stack.count() > other_stack.max_stack_size() {
386 return false;
387 }
388 ItemStack::is_same_item_same_components(this_stack, other_stack)
390 }
391
392 fn try_to_merge(&self, other: &Self) {
397 let this_stack = self.get_item();
398 let other_stack = other.get_item();
399
400 if self.get_owner() != other.get_owner() {
402 return;
403 }
404
405 if !Self::are_mergeable(&this_stack, &other_stack) {
406 return;
407 }
408
409 if other_stack.count() < this_stack.count() {
411 Self::merge_stacks(self, &this_stack, other, &other_stack);
412 } else {
413 Self::merge_stacks(other, &other_stack, self, &this_stack);
414 }
415 }
416
417 fn merge_stacks(
421 to_item: &Self,
422 to_stack: &ItemStack,
423 from_item: &Self,
424 from_stack: &ItemStack,
425 ) {
426 let max_count = to_stack.max_stack_size().min(MERGE_MAX_STACK_SIZE);
428 if to_stack.count() >= max_count {
429 return;
430 }
431 let space_available = max_count - to_stack.count();
432 let transfer_count = space_available.min(from_stack.count());
433
434 let new_to_stack = to_stack.copy_with_count(to_stack.count() + transfer_count);
436 let mut new_from_stack = from_stack.clone();
437 new_from_stack.shrink(transfer_count);
438
439 let (from_pickup_delay, from_age) = {
441 let state = from_item.item_state.lock();
442 (state.pickup_delay, state.age)
443 };
444 to_item.set_item(new_to_stack);
445
446 {
449 let mut state = to_item.item_state.lock();
450 state.pickup_delay = state.pickup_delay.max(from_pickup_delay);
451 state.age = state.age.min(from_age);
452 }
453
454 if new_from_stack.is_empty() {
455 from_item.set_removed(RemovalReason::Discarded);
456 } else {
457 from_item.set_item(new_from_stack);
458 }
459 }
460
461 pub fn merge_with_neighbors(&self, world: &Arc<World>) {
467 if !self.is_mergeable() {
468 return;
469 }
470
471 let search_box = self.bounding_box().inflate_xyz(0.5, 0.0, 0.5);
473
474 for entity in world.get_entities_in_aabb(&search_box) {
476 if entity.id() == self.id() {
478 continue;
479 }
480
481 if let Some(other_item) = entity.downcast_ref::<Self>() {
482 if other_item.is_mergeable() {
484 self.try_to_merge(other_item);
485
486 if self.is_removed() {
488 break;
489 }
490 }
491 }
492 }
493 }
494
495 fn apply_fluid_movement_or_gravity(&self) {
496 let contact = self.fluid_contact();
497 if contact.water_height() > ITEM_FLUID_HEIGHT_THRESHOLD {
498 self.apply_fluid_movement(ITEM_WATER_DRAG);
499 } else if contact.lava_height() > ITEM_FLUID_HEIGHT_THRESHOLD {
500 self.apply_fluid_movement(ITEM_LAVA_DRAG);
501 } else {
502 self.apply_gravity();
503 }
504 }
505
506 fn apply_fluid_movement(&self, horizontal_drag: f64) {
507 let movement = self.velocity();
508 self.set_velocity(DVec3::new(
509 movement.x * horizontal_drag,
510 movement.y
511 + if movement.y < 0.06 {
512 FLUID_VERTICAL_NUDGE
513 } else {
514 0.0
515 },
516 movement.z * horizontal_drag,
517 ));
518 }
519}
520
521impl Entity for ItemEntity {
522 fn base(&self) -> &EntityBase {
523 &self.base
524 }
525
526 fn entity_type(&self) -> EntityTypeRef {
527 self.entity_type
528 }
529
530 fn tick(&self) {
531 if self.get_item().is_empty() {
533 self.set_removed(RemovalReason::Discarded);
534 return;
535 }
536
537 self.default_tick();
538
539 {
540 let mut state = self.item_state.lock();
541 if state.pickup_delay > 0 && state.pickup_delay != INFINITE_PICKUP_DELAY {
542 state.pickup_delay -= 1;
543 }
544 }
545
546 self.set_old_position_to_current();
548 let old_pos = self.old_position();
549 let old_movement = self.velocity();
551 let old_on_ground = self.on_ground();
553
554 self.apply_fluid_movement_or_gravity();
555 self.update_no_physics_from_current_collision();
556
557 let vel = self.velocity();
564 let horizontal_movement_sq = vel.x * vel.x + vel.z * vel.z;
565 let should_move = !self.on_ground()
566 || horizontal_movement_sq > 1.0e-5
567 || (self.tick_count() + self.id()) % 4 == 0;
568
569 if should_move {
570 if let Some(result) = self.move_entity(MoverType::SelfMovement, self.velocity()) {
572 self.apply_effects_from_blocks();
573 if self.is_removed() {
574 return;
575 }
576
577 if let Some(world) = self.level() {
579 let friction = if result.on_ground
581 && let Some(block_pos) = self.block_pos_below_that_affects_movement()
582 {
583 let block_state = world.get_block_state(block_pos);
584 f64::from(block_state.get_block().config.friction) * 0.98
585 } else {
586 0.98 };
588
589 let mut velocity = self.velocity();
590 velocity.x *= friction;
591 velocity.z *= friction;
592 velocity.y *= AIR_DRAG;
593
594 if result.on_ground && velocity.y < 0.0 {
596 velocity.y *= -0.5;
597 }
598
599 self.set_velocity(velocity);
600 }
601 }
602 } else {
603 self.apply_effects_from_blocks_for_last_movements();
604 if self.is_removed() {
605 return;
606 }
607 }
608
609 let current_pos = self.position();
612 let moved_block = old_pos.x.floor() as i32 != current_pos.x.floor() as i32
613 || old_pos.y.floor() as i32 != current_pos.y.floor() as i32
614 || old_pos.z.floor() as i32 != current_pos.z.floor() as i32;
615 let merge_rate = if moved_block { 2 } else { 40 };
616
617 if self.tick_count() % merge_rate == 0
618 && self.is_mergeable()
619 && let Some(world) = self.level()
620 {
621 self.merge_with_neighbors(&world);
622 }
623
624 let new_movement = self.velocity();
627 let diff = DVec3::new(
628 new_movement.x - old_movement.x,
629 new_movement.y - old_movement.y,
630 new_movement.z - old_movement.z,
631 );
632 let diff_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
633 if diff_sq > 0.01 {
634 self.mark_velocity_sync();
635 }
636
637 if self.on_ground() != old_on_ground {
640 self.mark_velocity_sync();
641 }
642
643 let should_despawn = {
644 let mut state = self.item_state.lock();
645 if state.age == INFINITE_LIFETIME {
646 false
647 } else {
648 state.age += 1;
649 state.age >= LIFETIME
650 }
651 };
652
653 if should_despawn {
654 self.set_removed(RemovalReason::Discarded);
655 }
656 }
657
658 fn get_default_gravity(&self) -> f64 {
659 DEFAULT_GRAVITY
660 }
661
662 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
663 Some(&self.entity_data)
664 }
665
666 fn block_pos_below_that_affects_movement(&self) -> Option<BlockPos> {
667 self.on_pos(0.999_999)
668 }
669
670 fn attackable(&self) -> bool {
671 false
672 }
673
674 fn should_play_lava_hurt_sound(&self) -> bool {
675 self.get_health() <= 0 || self.tick_count() % 10 == 0
676 }
677
678 fn fire_immune(&self) -> bool {
679 !self
680 .get_item()
681 .can_be_hurt_by(&vanilla_damage_types::IN_FIRE)
682 || self.entity_type().fire_immune
683 }
684
685 fn player_touch(self: Arc<Self>, player: &Arc<Player>) {
686 self.try_pickup(player);
687 }
688
689 fn hurt(&self, _world: &World, source: &DamageSource, amount: f32) -> bool {
690 if !self.get_item().can_be_hurt_by(source.damage_type) {
692 return false;
693 }
694 let new_health = {
695 let mut state = self.item_state.lock();
696 state.health = (state.health as f32 - amount) as i32;
697 state.health
698 };
699 if new_health <= 0 {
700 self.set_removed(RemovalReason::Killed);
702 }
703 true
704 }
705
706 fn save_additional(&self, nbt: &mut NbtCompound) {
707 let state = self.item_state.lock();
709 nbt.insert("Health", state.health as i16);
710 nbt.insert("Age", state.age as i16);
711 nbt.insert("PickupDelay", state.pickup_delay as i16);
712
713 if let Some(thrower) = state.thrower {
714 nbt.insert("Thrower", NbtTag::IntArray(thrower.to_int_array().to_vec()));
715 }
716 if let Some(owner) = state.owner {
717 nbt.insert("Owner", NbtTag::IntArray(owner.to_int_array().to_vec()));
718 }
719 drop(state);
720
721 let item = self.get_item();
722 if !item.is_empty() {
723 nbt.insert("Item", item.to_nbt_tag());
724 }
725 }
726
727 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
728 let mut state = self.item_state.lock();
730 if let Some(health) = nbt.short("Health") {
731 state.health = i32::from(health);
732 }
733 if let Some(age) = nbt.short("Age") {
734 state.age = i32::from(age);
735 }
736 if let Some(pickup_delay) = nbt.short("PickupDelay") {
737 state.pickup_delay = i32::from(pickup_delay);
738 }
739
740 if let Some(thrower_arr) = nbt.int_array("Thrower")
741 && let Some(uuid) = Uuid::from_int_array(&thrower_arr)
742 {
743 state.thrower = Some(uuid);
744 }
745 if let Some(owner_arr) = nbt.int_array("Owner")
746 && let Some(uuid) = Uuid::from_int_array(&owner_arr)
747 {
748 state.owner = Some(uuid);
749 }
750 drop(state);
751
752 if let Some(item_tag) = nbt.compound("Item")
753 && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
754 {
755 self.entity_data.lock().item.set(item);
756 }
757
758 if self.get_item().is_empty() {
760 self.set_removed(RemovalReason::Discarded);
761 }
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use std::sync::Weak;
768
769 use glam::DVec3;
770
771 use steel_registry::{
772 init_vanilla_registry, item_stack::ItemStack, vanilla_damage_types, vanilla_entities,
773 vanilla_items,
774 };
775
776 use crate::entity::{Entity, damage::DamageSource};
777 use crate::test_support::test_world;
778 use crate::world::World;
779
780 use super::ItemEntity;
781
782 #[test]
783 fn item_entities_do_not_obstruct_block_placement() {
784 let item = ItemEntity::new(
785 &vanilla_entities::ITEM,
786 1,
787 DVec3::ZERO,
788 Weak::<World>::new(),
789 );
790
791 assert!(!item.blocks_building());
792 }
793
794 #[test]
795 fn item_lava_hurt_sound_uses_vanilla_interval() {
796 let item = ItemEntity::new(
797 &vanilla_entities::ITEM,
798 1,
799 DVec3::ZERO,
800 Weak::<World>::new(),
801 );
802
803 assert!(item.should_play_lava_hurt_sound());
804 item.advance_tick_count();
805 assert!(!item.should_play_lava_hurt_sound());
806
807 for _ in 1..10 {
808 item.advance_tick_count();
809 }
810 assert!(item.should_play_lava_hurt_sound());
811
812 item.set_health(0);
813 item.advance_tick_count();
814 assert!(item.should_play_lava_hurt_sound());
815 }
816
817 #[test]
818 fn item_with_stack_uses_vanilla_default_velocity() {
819 let item = ItemEntity::with_item(
820 &vanilla_entities::ITEM,
821 1,
822 DVec3::ZERO,
823 ItemStack::new(&vanilla_items::STONE),
824 Weak::<World>::new(),
825 );
826 let velocity = item.velocity();
827
828 assert!(velocity.x >= -0.1);
829 assert!(velocity.x < 0.1);
830 assert_eq!(velocity.y.to_bits(), 0.2_f64.to_bits());
831 assert!(velocity.z >= -0.1);
832 assert!(velocity.z < 0.1);
833 }
834
835 #[test]
836 fn fake_item_is_never_pickable_and_expires_on_its_next_tick() {
837 let item = ItemEntity::with_item(
838 &vanilla_entities::ITEM,
839 1,
840 DVec3::ZERO,
841 ItemStack::new(&vanilla_items::STONE),
842 Weak::<World>::new(),
843 );
844
845 item.make_fake_item();
846
847 assert_eq!(item.get_pickup_delay(), 32_767);
848 assert_eq!(item.get_age(), 5_999);
849 }
850
851 #[test]
852 fn item_merge_preserves_vanilla_stack_and_timing() {
853 init_vanilla_registry();
854
855 let source = ItemEntity::with_item(
856 &vanilla_entities::ITEM,
857 1,
858 DVec3::ZERO,
859 ItemStack::with_count(&vanilla_items::STONE, 10),
860 Weak::<World>::new(),
861 );
862 source.set_pickup_delay(5);
863 source.set_age(20);
864
865 let target = ItemEntity::with_item(
866 &vanilla_entities::ITEM,
867 2,
868 DVec3::ZERO,
869 ItemStack::with_count(&vanilla_items::STONE, 20),
870 Weak::<World>::new(),
871 );
872 target.set_pickup_delay(1);
873 target.set_age(50);
874
875 source.try_to_merge(&target);
876
877 assert!(source.is_removed());
878 assert_eq!(target.get_item().count(), 30);
879 assert_eq!(target.get_pickup_delay(), 5);
880 assert_eq!(target.get_age(), 20);
881 }
882
883 #[test]
884 fn item_damage_truncates_after_fractional_subtraction() {
885 let item = ItemEntity::new(
886 &vanilla_entities::ITEM,
887 1,
888 DVec3::ZERO,
889 Weak::<World>::new(),
890 );
891
892 assert!(item.hurt(
893 test_world(),
894 &DamageSource::environment(&vanilla_damage_types::GENERIC),
895 0.75
896 ));
897
898 assert_eq!(item.get_health(), 4);
899 }
900
901 #[test]
902 fn damage_resistant_item_ignores_matching_damage() {
903 init_vanilla_registry();
904
905 let item = ItemEntity::with_item(
906 &vanilla_entities::ITEM,
907 1,
908 DVec3::ZERO,
909 ItemStack::new(&vanilla_items::NETHERITE_INGOT),
910 Weak::<World>::new(),
911 );
912
913 assert!(item.fire_immune());
914 assert!(!item.hurt(
915 test_world(),
916 &DamageSource::environment(&vanilla_damage_types::IN_FIRE),
917 1.0
918 ));
919 assert_eq!(item.get_health(), 5);
920
921 assert!(item.hurt(
922 test_world(),
923 &DamageSource::environment(&vanilla_damage_types::GENERIC),
924 1.0
925 ));
926 assert_eq!(item.get_health(), 4);
927 }
928}