use crate::entity::player::statistics::StatisticCategory; use crate::{entity::EntityBaseFuture, server::Server}; use core::f32; use pumpkin_data::damage::DamageType; use pumpkin_data::data_component_impl::DamageResistantImpl; use pumpkin_data::data_component_impl::DamageResistantType; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::packet::CURRENT_MC_VERSION; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::bedrock::client::CAddItemActor; use pumpkin_protocol::bedrock::network_item::ItemStackWrapper; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::var_long::VarLong; use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata}; use pumpkin_util::math::atomic_f32::AtomicF32; use pumpkin_util::math::vector3::Vector3; use std::sync::atomic::Ordering::{AcqRel, Relaxed}; use std::sync::{ Arc, Mutex, atomic::{ AtomicBool, AtomicU8, AtomicU32, Ordering::{self}, }, }; use super::{Entity, EntityBase, living::LivingEntity, player::Player}; pub struct ItemEntity { entity: Entity, item_age: AtomicU32, // These cannot be atomic values because we mutate their state based on what they are; we run // into the ABA problem item_stack: Mutex, pickup_delay: AtomicU8, health: AtomicF32, never_despawn: AtomicBool, never_pickup: AtomicBool, } const ITEM_UPDATE_INTERVAL: u32 = 20; impl ItemEntity { pub fn new(entity: Entity, item_stack: ItemStack) -> Self { entity.velocity.store(Vector3::new( rand::random::().mul_add(0.2, -0.1), 0.2, rand::random::().mul_add(0.2, -0.1), )); entity.yaw.store(rand::random::() * 360.0); // Set fire immunity for certain items if let Some(res) = item_stack.get_data_component::() && res.res_type == DamageResistantType::Fire { entity.fire_immune.store(true, Ordering::Relaxed); } Self { entity, item_stack: Mutex::new(item_stack), item_age: AtomicU32::new(0), pickup_delay: AtomicU8::new(10), // Vanilla pickup delay is 10 ticks health: AtomicF32::new(5.0), never_despawn: AtomicBool::new(false), never_pickup: AtomicBool::new(false), } } pub fn new_with_velocity( entity: Entity, item_stack: ItemStack, velocity: Vector3, pickup_delay: u8, ) -> Self { entity.velocity.store(velocity); entity.yaw.store(rand::random::() * 360.0); // Set fire immunity for certain items if let Some(res) = item_stack.get_data_component::() && res.res_type == DamageResistantType::Fire { entity.fire_immune.store(true, Ordering::Relaxed); } Self { entity, item_stack: Mutex::new(item_stack), item_age: AtomicU32::new(0), pickup_delay: AtomicU8::new(pickup_delay), // Vanilla pickup delay is 10 ticks health: AtomicF32::new(5.0), never_despawn: AtomicBool::new(false), never_pickup: AtomicBool::new(false), } } /// Creates an `ItemEntity` for restoring from NBT without random velocity. /// The velocity and position will be set by `Entity::read_nbt_non_mut`. pub fn new_empty(entity: Entity) -> Self { Self { entity, item_stack: Mutex::new(ItemStack::new(1, &pumpkin_data::item::Item::AIR)), item_age: AtomicU32::new(0), pickup_delay: AtomicU8::new(0), health: AtomicF32::new(5.0), never_despawn: AtomicBool::new(false), never_pickup: AtomicBool::new(false), } } pub const fn get_item_stack(&self) -> &Mutex { &self.item_stack } pub fn get_pickup_delay(&self) -> u8 { self.pickup_delay.load(Ordering::Relaxed) } pub fn set_pickup_delay(&self, pickup_delay: u8) { self.pickup_delay.store(pickup_delay, Ordering::Relaxed); } pub const fn get_entity(&self) -> &Entity { &self.entity } pub fn can_merge(&self) -> bool { let Ok(item_stack) = self.item_stack.try_lock() else { return false; }; item_stack.item_count < item_stack.get_max_stack_size() } pub fn try_merge(&self) { if !self.can_merge() || self.never_despawn.load(Ordering::Relaxed) { return; } let bounding_box = self.entity.bounding_box.load().expand(0.5, 0.0, 0.5); let world = self.entity.world.load(); let entities = world.entities.load(); let items: Vec> = entities .iter() .filter_map(|entity: &Arc| { entity.clone().get_item_entity().filter(|item| { item.entity.entity_id != self.entity.entity_id && !item.never_despawn.load(Ordering::Relaxed) && item.entity.bounding_box.load().intersects(&bounding_box) }) }) .collect(); for item in items { if item.can_merge() { if let Some(this_base) = world.get_entity_by_id(self.entity.entity_id) && let Some(this_item) = this_base.get_item_entity() { this_item.try_merge_with(&item); } if self.entity.removed.load(Ordering::SeqCst) { break; } } } } fn try_merge_with(&self, other: &Self) { // Always lock in entity_id order to prevent deadlock when two // items try to merge with each other concurrently. let (low, high) = if self.entity.entity_id < other.entity.entity_id { (self, other) } else { (other, self) }; let low_stack = low .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let high_stack = high .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (self_stack, other_stack) = if self.entity.entity_id < other.entity.entity_id { (low_stack, high_stack) } else { (high_stack, low_stack) }; if !self_stack.are_equal(&other_stack) || self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size() { return; } let (target, mut stack1, source, mut stack2) = if other_stack.item_count < self_stack.item_count { (self, self_stack, other, other_stack) } else { (other, other_stack, self, self_stack) }; let mut event = crate::plugin::api::events::entity::item_merge::ItemMergeEvent { entity_id: target.entity.entity_id, target_id: source.entity.entity_id, cancelled: false, }; if let Some(server) = self.entity.world.load().server.upgrade() { server.plugin_manager.fire_blocking(&server, &mut event); } if event.cancelled { return; } // Vanilla code adds a .min(64). Not needed with Vanilla item data let max_size = stack1.get_max_stack_size(); let j = stack2.item_count.min(max_size - stack1.item_count); stack1.increment(j); stack2.decrement(j); let empty1 = stack1.item_count == 0; let empty2 = stack2.item_count == 0; drop(stack1); drop(stack2); let never_despawn = source.never_despawn.load(Ordering::Relaxed); target.never_despawn.store(never_despawn, Ordering::Relaxed); if !never_despawn { let age = target .item_age .load(Ordering::Relaxed) .min(source.item_age.load(Ordering::Relaxed)); target.item_age.store(age, Ordering::Relaxed); } let never_pickup = source.never_pickup.load(Ordering::Relaxed); target.never_pickup.store(never_pickup, Ordering::Relaxed); if !never_pickup { let source_delay = source.pickup_delay.load(Ordering::Relaxed); target .pickup_delay .fetch_max(source_delay, Ordering::Relaxed); } if empty1 { target.entity.remove(); } else { target.init_data_tracker(); } if empty2 { source.entity.remove(); } else { source.init_data_tracker(); } } fn decrement_pickup_delay(&self) { self.pickup_delay .try_update(Ordering::Relaxed, Ordering::Relaxed, |val| { Some(val.saturating_sub(1)) }) .ok(); } fn apply_fluid_drag_or_gravity(&self, mut velo: Vector3) -> Vector3 { let entity = &self.entity; if entity.touching_water.load(Ordering::SeqCst) && entity.water_height.load() > 0.1 { velo.x *= 0.99; velo.z *= 0.99; if velo.y < 0.06 { velo.y += 5.0e-4; } } else if entity.touching_lava.load(Ordering::SeqCst) && entity.lava_height.load() > 0.1 { velo.x *= 0.95; velo.z *= 0.95; if velo.y < 0.06 { velo.y += 5.0e-4; } } else { velo.y -= ::get_gravity(self); } velo } fn update_no_physics_and_push_out(&self) { let entity = &self.entity; let pos = entity.pos.load(); let bounding_box = entity.bounding_box.load(); let no_physics = !entity .world .load() .is_space_empty(bounding_box.expand(-1.0e-7, -1.0e-7, -1.0e-7)); entity.no_physics.store(no_physics, Ordering::Relaxed); if no_physics { entity.push_out_of_blocks(Vector3::new( pos.x, f64::midpoint(bounding_box.min.y, bounding_box.max.y), pos.z, )); } } fn should_tick_move(&self, move_velo: Vector3) -> Option { let entity = &self.entity; let mut tick_move = !entity.on_ground.load(Ordering::SeqCst) || move_velo.horizontal_length_squared() > 1.0e-5; if !tick_move { let Ok(item_age) = i32::try_from(self.item_age.load(Ordering::Relaxed)) else { entity.remove(); return None; }; tick_move = (item_age + entity.entity_id) % 4 == 0; } Some(tick_move) } fn move_and_apply_friction( &self, caller: &Arc, server: &Server, move_velo: Vector3, ) { let entity = &self.entity; entity.move_entity(caller, move_velo); entity.tick_block_collisions(caller, server); let mut friction = 0.98; let on_ground = entity.on_ground.load(Ordering::SeqCst); let mut velo = entity.velocity.load(); if on_ground { let block_affecting_velo = entity.get_block_with_y_offset(0.999_999).1; friction *= f64::from(block_affecting_velo.slipperiness) * 0.98; } velo = velo.multiply(friction, 0.98, friction); if on_ground && velo.y < 0.0 { velo.y = 0.0; } entity.velocity.store(velo); } fn process_age_and_merge(&self) -> bool { if self.never_despawn.load(Ordering::Relaxed) { return true; } let entity = &self.entity; let age = self.item_age.fetch_add(1, Ordering::Relaxed) + 1; if age >= 6000 { let entity_id = entity.entity_id; let world = entity.world.load_full(); let mut despawn_event = crate::plugin::api::events::entity::item_despawn::ItemDespawnEvent::new(entity_id); if let Some(server) = world.server.upgrade() { server .plugin_manager .fire_blocking(&server, &mut despawn_event); } if !despawn_event.cancelled && let Some(e) = world.get_entity_by_id(entity_id) { e.get_entity().remove(); } return false; } let n = if entity .last_pos .load() .sub(&entity.pos.load()) .length_squared() == 0.0 { 40 } else { 2 }; if age.is_multiple_of(n) && self.can_merge() { self.try_merge(); } true } fn sync_motion_if_dirty(&self, caller: &Arc, original_velo: Vector3) { let entity = &self.entity; entity.update_fluid_state(caller); let velocity_dirty = entity.velocity_dirty.swap(false, Ordering::SeqCst) || entity.touching_water.load(Ordering::SeqCst) || entity.touching_lava.load(Ordering::SeqCst) || entity.velocity.load().sub(&original_velo).length_squared() > 0.1; let moved = entity.pos.load() != entity.last_sent_pos.load(); let position_dirty = moved && self .item_age .load(Ordering::Relaxed) .is_multiple_of(ITEM_UPDATE_INTERVAL); if position_dirty || velocity_dirty { entity.send_pos_rot(); } else if moved { entity.send_bedrock_pos(); } if velocity_dirty { entity.send_velocity(); } } } impl EntityBase for ItemEntity { fn tick(&self, caller: &Arc, server: &Server) { let entity = &self.entity; self.decrement_pickup_delay(); let original_velo = entity.velocity.load(); entity .velocity .store(self.apply_fluid_drag_or_gravity(original_velo)); self.update_no_physics_and_push_out(); let move_velo = entity.velocity.load(); // In case push_out_of_blocks modifies it let Some(tick_move) = self.should_tick_move(move_velo) else { return; }; if tick_move { self.move_and_apply_friction(caller, server, move_velo); } if self.process_age_and_merge() { self.sync_motion_if_dirty(caller, original_velo); } } fn init_data_tracker(&self) { self.entity.send_meta_data( &[Metadata::new( pumpkin_data::tracked_data::item::ITEM, &ItemStackSerializer::from( self.item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(), ), )], None, ); } fn damage_with_context( &self, _caller: &dyn EntityBase, amount: f32, damage_type: DamageType, _position: Option>, _source: Option<&dyn EntityBase>, _cause: Option<&dyn EntityBase>, ) -> bool { // Check if entity is fire_immune let is_fire_damage = damage_type == DamageType::IN_FIRE || damage_type == DamageType::ON_FIRE || damage_type == DamageType::LAVA; if is_fire_damage && self.entity.fire_immune.load(Ordering::Relaxed) { return false; } loop { let current = self.health.load(Relaxed); let new = current - amount; if self .health .compare_exchange(current, new, AcqRel, Relaxed) .is_ok() { if new <= 0.0 { self.entity.remove(); } return true; } } } fn on_player_collision(&self, player: &Arc) { if self.pickup_delay.load(Ordering::Relaxed) > 0 || player.living_entity.health.load() <= 0.0 || player.is_spectator() { return; } let (item_id, count_before) = { let stack = self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); (stack.item.id, stack.item_count) }; let mut local_stack = self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); let inserted = player.inventory.insert_stack_anywhere(&mut local_stack); let count_after = local_stack.item_count; let is_empty = local_stack.is_empty(); *self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = local_stack; if inserted || player.is_creative() { player.inventory_changed.store(true, Ordering::Relaxed); let amount_picked_up = if player.is_creative() { count_before } else { count_before - count_after }; if amount_picked_up > 0 { player.increment_stat( StatisticCategory::PickedUp, item_id as i32, amount_picked_up as i32, ); } player .living_entity .pickup(&self.entity, amount_picked_up.into()); let player_clone = player.clone(); tokio::spawn(async move { player_clone .current_screen_handler .lock() .await .lock() .await .send_content_updates() .await; }); if is_empty { self.entity.remove(); } else { self.init_data_tracker(); } } } fn get_entity(&self) -> &Entity { &self.entity } fn get_living_entity(&self) -> Option<&LivingEntity> { None } fn get_item_entity(self: Arc) -> Option> { Some(self) } fn get_gravity(&self) -> f64 { 0.04 } fn write_custom_nbt(&self, nbt: &mut NbtCompound) { let item = self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let mut item_compound = NbtCompound::new(); item.write_item_stack(&mut item_compound); nbt.put_compound("Item", item_compound); nbt.put_short("Age", self.item_age.load(Ordering::Relaxed) as i16); nbt.put_short( "PickupDelay", self.pickup_delay.load(Ordering::Relaxed) as i16, ); nbt.put_short("Health", self.health.load(Relaxed) as i16); } fn read_custom_nbt(&self, nbt: &NbtCompound) { // Restore the item stack from the "Item" compound if let Some(item_compound) = nbt.get_compound("Item") && let Some(stack) = ItemStack::read_item_stack(item_compound) { *self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = stack; } // Vanilla stores Age as a short self.item_age .store(nbt.get_short("Age").unwrap_or(0) as u32, Ordering::Relaxed); // Vanilla stores PickupDelay as a short if let Some(delay) = nbt.get_short("PickupDelay") { self.pickup_delay.store(delay as u8, Ordering::Relaxed); } // Vanilla stores Health as a short if let Some(health) = nbt.get_short("Health") { self.health.store(health as f32, Relaxed); } } fn cast_any(&self) -> &dyn std::any::Any { self } fn send_bedrock_spawn_packet<'a>( &'a self, client: &'a crate::net::bedrock::BedrockClient, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { let entity = &self.entity; let runtime_id = entity.entity_id as u64; let data = { let item_stack = self .item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let packet = CAddItemActor { target_actor_id: VarLong(runtime_id as i64), target_runtime_id: VarULong(runtime_id), item: ItemStackWrapper::from(&*item_stack), position: entity.pos.load().to_f32_lossy(), velocity: entity.velocity.load().to_f32_lossy(), entity_data: entity.bedrock_metadata(), is_from_fishing: false, }; client.serialize_packet(&packet).ok() }; if let Some(data) = data { client.send_game_packet(data).await; } }) } fn send_java_spawn_packet<'a>( &'a self, client: &'a crate::net::java::JavaClient, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { let spawn_packet = self.entity.create_spawn_packet(); if let Ok(data) = client.serialize_packet(&spawn_packet) { client.enqueue_packet(data).await; } if client.version.load() >= CURRENT_MC_VERSION { let metadata = Metadata::new( pumpkin_data::tracked_data::item::ITEM, ItemStackSerializer::from( self.item_stack .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(), ), ); let mut data = Vec::new(); if metadata.write(&mut data, &client.version.load()).is_ok() { data.push(255); let meta_packet = CSetEntityMetadata::new(self.entity.entity_id.into(), data.into()); if let Ok(meta_data) = client.serialize_packet(&meta_packet) { client.enqueue_packet(meta_data).await; } } } }) } }