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