use crate::entity::item::ItemEntity; use crate::net::ClientPlatform; use crate::world::World; use crate::{ server::Server, world::portal::{NetherPortal, PortalManager, PortalSearchResult, SourcePortalInfo}, }; use arc_swap::ArcSwap; use bytes::BufMut; use crossbeam::atomic::AtomicCell; use living::LivingEntity; use player::Player; use pumpkin_data::BlockState; use pumpkin_data::block_properties::{EnumVariants, Integer0To15}; use pumpkin_data::dimension::Dimension; use pumpkin_data::fluid::Fluid; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_data::{Block, BlockDirection}; use pumpkin_data::{ block_properties::{Facing, HorizontalFacing}, damage::DamageType, entity::{EntityPose, EntityType}, sound::{Sound, SoundCategory}, }; use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::{ codec::var_int::VarInt, java::client::play::{ CEntityPositionSync, CEntityVelocity, CHeadRot, CSetEntityMetadata, CSpawnEntity, CUpdateEntityRot, Metadata, }, }; use pumpkin_util::math::vector3::Axis; use pumpkin_util::math::{ boundingbox::{BoundingBox, EntityDimensions}, get_section_cord, position::BlockPos, vector2::Vector2, vector3::Vector3, wrap_degrees, }; use pumpkin_util::text::TextComponent; use pumpkin_util::text::hover::HoverEvent; use pumpkin_util::version::MinecraftVersion; use serde::Serialize; use std::collections::BTreeMap; use std::pin::Pin; use std::sync::{ Arc, atomic::{ AtomicBool, AtomicI32, AtomicU32, Ordering::{self, Relaxed}, }, }; use tokio::sync::Mutex; use uuid::Uuid; pub mod ai; pub mod boss; pub mod breath; pub mod decoration; pub mod effect; pub mod experience_orb; pub mod falling; pub mod hunger; pub mod item; pub mod living; pub mod mob; pub mod passive; pub mod player; pub mod projectile; pub mod projectile_deflection; pub mod tnt; pub mod r#type; mod combat; pub mod predicate; pub type EntityBaseFuture<'a, T> = Pin + Send + 'a>>; pub type TeleportFuture = Pin + Send>>; pub trait EntityBase: Send + Sync + NBTStorage { /// Called every tick for this entity. /// /// The `caller` parameter is a reference to the entity that initiated the tick. /// This can be the same entity the method is being called on (`self`), /// but in some scenarios (e.g., interactions or events), it might be a different entity. /// /// The `server` parameter provides access to the game server instance. fn tick<'a>( &'a self, caller: Arc, server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { if let Some(living) = self.get_living_entity() { living.tick(caller, server).await; } else { self.get_entity().tick(caller, server).await; } }) } fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { let entity = self.get_entity(); // If the internal age is negative, it's a baby let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { entity .send_meta_data(&[Metadata::new( TrackedData::DATA_BABY, MetaDataType::Boolean, true, )]) .await; } }) } // This method takes ownership of Arc, so the lifetime bounds are different. fn teleport( self: Arc, position: Vector3, yaw: Option, pitch: Option, world: Arc, ) -> TeleportFuture where Self: 'static, { Box::pin(async move { self.get_entity() .teleport(position, yaw, pitch, world) .await; }) } fn is_pushed_by_fluids(&self) -> bool { true } fn get_gravity(&self) -> f64 { 0.0 } fn tick_in_void<'a>(&'a self, _dyn_self: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.get_entity().remove().await }) } /// Returns if damage was successful or not fn damage<'a>( &'a self, caller: &'a dyn EntityBase, amount: f32, damage_type: DamageType, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { caller .damage_with_context(caller, amount, damage_type, None, None, None) .await }) } fn is_spectator(&self) -> bool { false } fn is_collidable(&self, _entity: Option>) -> bool { false } fn can_hit(&self) -> bool { false } fn is_flutterer(&self) -> bool { false } fn damage_with_context<'a>( &'a self, caller: &'a dyn EntityBase, amount: f32, damage_type: DamageType, position: Option>, source: Option<&'a dyn EntityBase>, cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { caller .damage_with_context(caller, amount, damage_type, position, source, cause) .await }) } /// Called when a player collides with a entity fn on_player_collision<'a>(&'a self, _player: &'a Arc) -> EntityBaseFuture<'a, ()> { Box::pin(async {}) } fn on_hit(&self, _hit: crate::entity::projectile::ProjectileHit) -> EntityBaseFuture<'_, ()> { Box::pin(async {}) } fn get_entity(&self) -> &Entity; fn get_living_entity(&self) -> Option<&LivingEntity>; fn get_item_entity(self: Arc) -> Option> { None } fn get_player(&self) -> Option<&Player> { None } /// Should return the name of the entity without click or hover events. fn get_name(&self) -> TextComponent { let entity = self.get_entity(); entity .custom_name .clone() .unwrap_or(TextComponent::translate( format!("entity.minecraft.{}", entity.entity_type.resource_name), [], )) } fn get_display_name(&self) -> EntityBaseFuture<'_, TextComponent> { Box::pin(async move { // TODO: team color let entity = self.get_entity(); let mut name = entity .custom_name .clone() .unwrap_or(TextComponent::translate( format!("entity.minecraft.{}", entity.entity_type.resource_name), [], )); let name_clone = name.clone(); name = name.hover_event(HoverEvent::show_entity( entity.entity_uuid.to_string(), entity.entity_type.resource_name.into(), Some(name_clone), )); name = name.insertion(entity.entity_uuid.to_string()); name }) } /// Kills the Entity. fn kill<'a>(&'a self, caller: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> { Box::pin(async move { if self.get_living_entity().is_some() { caller .damage(caller, f32::MAX, DamageType::GENERIC_KILL) .await; } else { // TODO this should be removed once all entities are implemented self.get_entity().remove().await; } }) } /// Returns itself as the nbt storage for saving and loading data. fn as_nbt_storage(&self) -> &dyn NBTStorage; } #[derive(Clone, Copy, Eq, PartialEq)] pub enum RemovalReason { Killed, Discarded, UnloadedToChunk, UnloadedWithPlayer, ChangedDimension, } impl RemovalReason { #[must_use] pub const fn should_destroy(&self) -> bool { match self { Self::Killed | Self::Discarded => true, Self::UnloadedToChunk | Self::UnloadedWithPlayer | Self::ChangedDimension => false, } } #[must_use] pub const fn should_save(&self) -> bool { match self { Self::Killed | Self::Discarded | Self::UnloadedWithPlayer | Self::ChangedDimension => { false } Self::UnloadedToChunk => true, } } } static CURRENT_ID: AtomicI32 = AtomicI32::new(0); /// Represents a non-living Entity (e.g. Item, Egg, Snowball...) pub struct Entity { /// A unique identifier for the entity pub entity_id: i32, /// A persistent, unique identifier for the entity pub entity_uuid: uuid::Uuid, /// The type of entity (e.g., player, zombie, item) pub entity_type: &'static EntityType, /// The world in which the entity exists. /// Uses `ArcSwap` to allow atomic updates when changing dimensions. pub world: ArcSwap, /// The entity's current position in the world pub pos: AtomicCell>, /// The last known position of the entity. pub last_pos: AtomicCell>, /// The entity's position rounded to the nearest block coordinates pub block_pos: AtomicCell, /// The block supporting the entity pub supporting_block_pos: AtomicCell>, /// The chunk coordinates of the entity's current position pub chunk_pos: AtomicCell>, /// Indicates whether the entity is sneaking pub sneaking: AtomicBool, /// Indicates whether the entity is sprinting pub sprinting: AtomicBool, /// Indicates whether the entity is invisible pub invisible: AtomicBool, /// Indicates whether the entity is flying due to a fall pub fall_flying: AtomicBool, /// The entity's current velocity vector, aka knockback pub velocity: AtomicCell>, /// Tracks a horizontal collision pub horizontal_collision: AtomicBool, /// Indicates whether the entity is on the ground (may not always be accurate). pub on_ground: AtomicBool, /// Indicates whether the entity is touching water pub touching_water: AtomicBool, /// Indicates the fluid height pub water_height: AtomicCell, /// Indicates whether the entity is touching lava pub touching_lava: AtomicBool, /// Indicates the fluid height pub lava_height: AtomicCell, /// The entity's yaw rotation (horizontal rotation) ← → pub yaw: AtomicCell, /// The entity's head yaw rotation (horizontal rotation of the head) pub head_yaw: AtomicCell, /// The entity's body yaw rotation (horizontal rotation of the body) pub body_yaw: AtomicCell, /// The entity's pitch rotation (vertical rotation) ↑ ↓ pub pitch: AtomicCell, /// The entity's current pose (e.g., standing, sitting, swimming). pub pose: AtomicCell, /// The bounding box of an entity (hitbox) pub bounding_box: AtomicCell, ///The size (width and height) of the bounding box pub entity_dimension: AtomicCell, /// Whether this entity is invulnerable to all damage pub invulnerable: AtomicBool, /// List of damage types this entity is immune to pub damage_immunities: Vec, pub fire_ticks: AtomicI32, pub has_visual_fire: AtomicBool, /// The number of ticks the entity has been frozen (in powder snow) /// Max is 140 ticks (7 seconds). Increases by 1/tick in powder snow, decreases by 2/tick outside. pub frozen_ticks: AtomicI32, pub removal_reason: AtomicCell>, // The passengers that entity has pub passengers: Mutex>>, /// The vehicle that entity is in pub vehicle: Mutex>>, /// The age of the entity in ticks. Negative values indicate a baby. pub age: AtomicI32, pub first_loaded_chunk_position: AtomicCell>>, pub portal_cooldown: AtomicU32, pub portal_manager: Mutex>>, /// Custom name for the entity pub custom_name: Option, /// Indicates whether the entity's custom name is visible pub custom_name_visible: bool, /// The data send in the Entity Spawn packet pub data: AtomicI32, /// If true, the entity cannot collide with anything (e.g. spectator) pub no_clip: AtomicBool, /// Multiplies movement for one tick before being reset pub movement_multiplier: AtomicCell>, /// Determines whether the entity's velocity needs to be sent pub velocity_dirty: AtomicBool, /// Set when an Entity is to be removed but could still be referenced pub removed: AtomicBool, } impl Entity { pub fn new( world: Arc, position: Vector3, entity_type: &'static EntityType, ) -> Self { Self::from_uuid(Uuid::new_v4(), world, position, entity_type) } pub fn from_uuid( entity_uuid: uuid::Uuid, world: Arc, position: Vector3, entity_type: &'static EntityType, ) -> Self { let floor_x = position.x.floor() as i32; let floor_y = position.y.floor() as i32; let floor_z = position.z.floor() as i32; let bounding_box_size = EntityDimensions { width: entity_type.dimension[0], height: entity_type.dimension[1], eye_height: entity_type.eye_height, }; Self { entity_id: CURRENT_ID.fetch_add(1, Relaxed), entity_uuid, entity_type, on_ground: AtomicBool::new(false), touching_water: AtomicBool::new(false), water_height: AtomicCell::new(0.0), touching_lava: AtomicBool::new(false), lava_height: AtomicCell::new(0.0), horizontal_collision: AtomicBool::new(false), pos: AtomicCell::new(position), last_pos: AtomicCell::new(position), block_pos: AtomicCell::new(BlockPos(Vector3::new(floor_x, floor_y, floor_z))), supporting_block_pos: AtomicCell::new(None), chunk_pos: AtomicCell::new(Vector2::new( get_section_cord(floor_x), get_section_cord(floor_z), )), sneaking: AtomicBool::new(false), invisible: AtomicBool::new(false), world: ArcSwap::new(world), sprinting: AtomicBool::new(false), fall_flying: AtomicBool::new(false), yaw: AtomicCell::new(0.0), head_yaw: AtomicCell::new(0.0), body_yaw: AtomicCell::new(0.0), pitch: AtomicCell::new(0.0), velocity: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), pose: AtomicCell::new(EntityPose::Standing), first_loaded_chunk_position: AtomicCell::new(None), bounding_box: AtomicCell::new(BoundingBox::new_from_pos( position.x, position.y, position.z, &bounding_box_size, )), entity_dimension: AtomicCell::new(bounding_box_size), invulnerable: AtomicBool::new(false), damage_immunities: Vec::new(), data: AtomicI32::new(0), fire_ticks: AtomicI32::new(-1), has_visual_fire: AtomicBool::new(false), frozen_ticks: AtomicI32::new(0), removal_reason: AtomicCell::new(None), passengers: Mutex::new(Vec::new()), vehicle: Mutex::new(None), age: AtomicI32::new(0), portal_cooldown: AtomicU32::new(0), portal_manager: Mutex::new(None), custom_name: None, custom_name_visible: false, no_clip: AtomicBool::new(false), movement_multiplier: AtomicCell::new(Vector3::default()), velocity_dirty: AtomicBool::new(true), removed: AtomicBool::new(false), } } pub async fn set_velocity(&self, velocity: Vector3) { self.velocity.store(velocity); self.send_velocity().await; } /// Updates the world reference for this entity. /// Called when the entity changes dimensions (e.g., through a nether portal). pub fn set_world(&self, world: Arc) { self.world.store(world); } /// Sets the entity's age in ticks. /// Negative values indicate that the entity is a baby. pub fn set_age(&self, age: i32) { self.age.store(age, Relaxed); } /// Sets a custom name for the entity, typically used with nametags pub async fn set_custom_name(&self, name: TextComponent) { self.send_meta_data(&[Metadata::new( TrackedData::DATA_CUSTOM_NAME, MetaDataType::OptionalTextComponent, Some(name), )]) .await; } pub async fn send_velocity(&self) { let velocity = self.velocity.load(); self.world .load() .broadcast_packet_all(&CEntityVelocity::new(self.entity_id.into(), velocity)) .await; } #[must_use] pub const fn get_entity_dimensions(pose: EntityPose) -> EntityDimensions { match pose { EntityPose::Sleeping => EntityDimensions::new(0.2, 0.2, 0.2), EntityPose::FallFlying | EntityPose::Swimming | EntityPose::SpinAttack => { EntityDimensions::new(0.6, 0.6, 0.4) } EntityPose::Crouching => EntityDimensions::new(0.6, 1.5, 1.27), EntityPose::Dying => EntityDimensions::new(0.2, 0.2, 1.62), _ => EntityDimensions::new(0.6, 1.8, 1.62), } } /// Updates the entity's position, block position, and chunk position. /// /// This function calculates the new position, block position, and chunk position based on the provided coordinates. If any of these values change, the corresponding fields are updated. pub fn set_pos(&self, new_position: Vector3) { let pos = self.pos.load(); if pos != new_position { self.pos.store(new_position); self.bounding_box.store(BoundingBox::new_from_pos( new_position.x, new_position.y, new_position.z, &self.entity_dimension.load(), )); let floor_x = new_position.x.floor() as i32; let floor_y = new_position.y.floor() as i32; let floor_z = new_position.z.floor() as i32; let block_pos = self.block_pos.load(); let block_pos_vec = block_pos.0; if floor_x != block_pos_vec.x || floor_y != block_pos_vec.y || floor_z != block_pos_vec.z { let new_block_pos = Vector3::new(floor_x, floor_y, floor_z); self.block_pos.store(BlockPos(new_block_pos)); let chunk_pos = self.chunk_pos.load(); if get_section_cord(floor_x) != chunk_pos.x || get_section_cord(floor_z) != chunk_pos.y { self.chunk_pos.store(Vector2::new( get_section_cord(new_block_pos.x), get_section_cord(new_block_pos.z), )); } } } } /// Returns entity rotation as vector pub fn rotation(&self) -> Vector3 { let pitch_rad = self.pitch.load().to_radians(); let yaw_rad = -self.yaw.load().to_radians(); let cos_yaw = yaw_rad.cos(); let sin_yaw = yaw_rad.sin(); let cos_pitch = pitch_rad.cos(); let sin_pitch = pitch_rad.sin(); Vector3::new(sin_yaw * cos_pitch, -sin_pitch, cos_yaw * cos_pitch) } /// Changes this entity's pitch and yaw to look at target pub fn look_at(&self, target: Vector3) { let position = self.pos.load(); let delta = target.sub(&position); let root = delta.x.hypot(delta.z); let pitch = wrap_degrees((-delta.y.atan2(root) as f32).to_degrees()); let yaw = wrap_degrees((delta.z.atan2(delta.x) as f32).to_degrees() - 90.0); self.pitch.store(pitch); self.yaw.store(yaw); } pub async fn send_rotation(&self) { let yaw = self.yaw.load(); let pitch = self.pitch.load(); // Broadcast the update packet. // TODO: Do caching to only send the packet when needed. let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0); let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0) as u8; let pitch = (pitch * 256.0 / 360.0).rem_euclid(256.0); self.world .load() .broadcast_packet_all(&CUpdateEntityRot::new( self.entity_id.into(), yaw, pitch as u8, self.on_ground.load(Relaxed), )) .await; self.send_head_rot(yaw).await; } pub async fn send_head_rot(&self, head_yaw: u8) { self.world .load() .broadcast_packet_all(&CHeadRot::new(self.entity_id.into(), head_yaw)) .await; } fn default_portal_cooldown(&self) -> u32 { if self.entity_type == &EntityType::PLAYER { 10 } else { 300 } } #[expect(clippy::float_cmp)] async fn adjust_movement_for_collisions(&self, movement: Vector3) -> Vector3 { self.on_ground.store(false, Ordering::SeqCst); self.supporting_block_pos.store(None); self.horizontal_collision.store(false, Ordering::SeqCst); if movement.length_squared() == 0.0 { return movement; } let bounding_box = self.bounding_box.load(); let (collisions, block_positions) = self .world .load() .get_block_collisions(bounding_box.stretch(movement)) .await; if collisions.is_empty() { return movement; } let mut adjusted_movement = movement; // Y-Axis adjustment if movement.get_axis(Axis::Y) != 0.0 { let mut max_time = 1.0; let mut positions = block_positions.into_iter(); let (mut collisions_len, mut position) = positions.next().unwrap(); let mut supporting_block_pos = None; for (i, inert_box) in collisions.iter().enumerate() { if i == collisions_len { (collisions_len, position) = positions.next().unwrap(); } if let Some(collision_time) = bounding_box.calculate_collision_time( inert_box, adjusted_movement, Axis::Y, max_time, ) { max_time = collision_time; supporting_block_pos = Some(position); } } if max_time != 1.0 { let changed_component = adjusted_movement.get_axis(Axis::Y) * max_time; adjusted_movement.set_axis(Axis::Y, changed_component); } self.on_ground .store(supporting_block_pos.is_some(), Ordering::SeqCst); self.supporting_block_pos.store(supporting_block_pos); } let mut horizontal_collision = false; for axis in Axis::horizontal() { if movement.get_axis(axis) == 0.0 { continue; } let mut max_time = 1.0; for inert_box in &collisions { if let Some(collision_time) = bounding_box.calculate_collision_time( inert_box, adjusted_movement, axis, max_time, ) { max_time = collision_time; } } if max_time != 1.0 { let changed_component = adjusted_movement.get_axis(axis) * max_time; adjusted_movement.set_axis(axis, changed_component); horizontal_collision = true; } } self.horizontal_collision .store(horizontal_collision, Ordering::SeqCst); adjusted_movement } /// Applies knockback to the entity, following vanilla Minecraft's mechanics. /// `LivingEntity.takeKnockback()` /// This function calculates the entity's new velocity based on the specified knockback strength and direction. pub fn apply_knockback(&self, strength: f64, mut x: f64, mut z: f64) { // TODO: strength *= 1 - Entity attribute knockback resistance if strength <= 0.0 { return; } self.velocity_dirty.store(true, Ordering::SeqCst); // This has some vanilla magic while x.mul_add(x, z * z) < 1.0E-5 { x = (rand::random::() - rand::random::()) * 0.01; z = (rand::random::() - rand::random::()) * 0.01; } let var8 = Vector3::new(x, 0.0, z).normalize() * strength; let velocity = self.velocity.load(); self.velocity.store(Vector3::new( velocity.x / 2.0 - var8.x, if self.on_ground.load(Relaxed) { (velocity.y / 2.0 + strength).min(0.4) } else { velocity.y }, velocity.z / 2.0 - var8.z, )); } // Part of LivingEntity.tickMovement() in yarn pub fn check_zero_velo(&self) { let mut motion = self.velocity.load(); if self.entity_type == &EntityType::PLAYER { if motion.horizontal_length_squared() < 9.0E-6 { motion.x = 0.0; motion.z = 0.0; } } else { if motion.x.abs() < 0.003 { motion.x = 0.0; } if motion.z.abs() < 0.003 { motion.z = 0.0; } } if motion.y.abs() < 0.003 { motion.y = 0.0; } self.velocity.store(motion); } #[expect(dead_code)] fn tick_block_underneath(_caller: &Arc) { // let world = self.world.read().await; // let (pos, block, state) = self.get_block_with_y_offset(0.2).await; // world // .block_registry // .on_stepped_on(&world, caller.as_ref(), pos, block, state) // .await; // TODO: Add this to on_stepped_on /* if self.on_ground.load(Ordering::SeqCst) { let (_pos, block, state) = self.get_block_with_y_offset(0.2).await; if let Some(live) = living { if block == Block::CAMPFIRE || block == Block::SOUL_CAMPFIRE && CampfireLikeProperties::from_state_id(state.id, &block).r#signal_fire { let _ = live.damage(1.0, DamageType::CAMPFIRE).await; } if block == Block::MAGMA_BLOCK { let _ = live.damage(1.0, DamageType::HOT_FLOOR).await; } } } */ } async fn tick_block_collisions(&self, caller: &Arc, server: &Server) -> bool { let bounding_box = self.bounding_box.load(); let aabb = bounding_box.expand(-0.001, -0.001, -0.001); let min = aabb.min_block_pos(); let max = aabb.max_block_pos(); let eye_height = f64::from(self.entity_dimension.load().eye_height); let mut eye_level_box = aabb; eye_level_box.min.y += eye_height; eye_level_box.max.y = eye_level_box.min.y; let mut suffocating = false; let pos_iter = BlockPos::iterate(min, max); let world = self.world.load(); for pos in pos_iter { let (block, state) = world.get_block_and_state(&pos).await; if state.is_air() { continue; } let check_suffocation = !suffocating && state.is_solid(); let collided = World::check_outline( &bounding_box, pos, state, check_suffocation, |collision_shape: &BoundingBox| { if collision_shape.intersects(&eye_level_box) { suffocating = true; } }, ); if collided { world .block_registry .on_entity_collision(block, &world, caller.as_ref(), &pos, state, server) .await; } } suffocating } pub async fn send_pos_rot(&self) { let old = self.update_last_pos(); let new = self.pos.load(); let converted = Vector3::new( new.x.mul_add(4096.0, -(old.x * 4096.0)) as i16, new.y.mul_add(4096.0, -(old.y * 4096.0)) as i16, new.z.mul_add(4096.0, -(old.z * 4096.0)) as i16, ); let yaw = self.yaw.load(); let pitch = self.pitch.load(); // Broadcast the update packet. // TODO: Do caching to only send the packet when needed. let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0) as u8; let pitch = (pitch * 256.0 / 360.0).rem_euclid(256.0); self.world .load() .broadcast_packet_all(&CUpdateEntityPosRot::new( self.entity_id.into(), Vector3::new(converted.x, converted.y, converted.z), yaw, pitch as u8, self.on_ground.load(Relaxed), )) .await; self.send_head_rot(yaw).await; } pub fn update_last_pos(&self) -> Vector3 { let pos = self.pos.load(); let old = self.last_pos.load(); self.last_pos.store(pos); old } pub async fn send_pos(&self) { let old = self.update_last_pos(); let new = self.pos.load(); let converted = Vector3::new( new.x.mul_add(4096.0, -(old.x * 4096.0)) as i16, new.y.mul_add(4096.0, -(old.y * 4096.0)) as i16, new.z.mul_add(4096.0, -(old.z * 4096.0)) as i16, ); self.world .load() .broadcast_packet_all(&CUpdateEntityPos::new( self.entity_id.into(), Vector3::new(converted.x, converted.y, converted.z), self.on_ground.load(Relaxed), )) .await; } // updateWaterState() in yarn async fn update_fluid_state(&self, caller: &Arc) { let is_pushed = caller.is_pushed_by_fluids(); let mut fluids = BTreeMap::new(); let water_push = Vector3::default(); let water_n = 0; let lava_push = Vector3::default(); let lava_n = 0; let mut fluid_push = [water_push, lava_push]; let mut fluid_n = [water_n, lava_n]; let mut in_fluid = [false, false]; // The maximum fluid height found let mut fluid_height: [f64; 2] = [0.0, 0.0]; let bounding_box = self.bounding_box.load().expand(-0.001, -0.001, -0.001); let min = bounding_box.min_block_pos(); let max = bounding_box.max_block_pos(); let world = self.world.load(); for x in min.0.x..=max.0.x { for y in min.0.y..=max.0.y { for z in min.0.z..=max.0.z { let pos = BlockPos::new(x, y, z); let (fluid, state) = world.get_fluid_and_fluid_state(&pos).await; if fluid.id != Fluid::EMPTY.id { let marginal_height = f64::from(state.height) + f64::from(y) - bounding_box.min.y; if marginal_height >= 0.0 { let i = usize::from( fluid.id == Fluid::FLOWING_LAVA.id || fluid.id == Fluid::LAVA.id, ); fluid_height[i] = fluid_height[i].max(marginal_height); in_fluid[i] = true; if !is_pushed { fluids.insert(fluid.id, fluid); continue; } let mut fluid_velo = world.get_fluid_velocity(pos, fluid, state).await; if fluid_height[i] < 0.4 { fluid_velo = fluid_velo * fluid_height[i]; } fluid_push[i] += fluid_velo; fluid_n[i] += 1; fluids.insert(fluid.id, fluid); } } } } } // BTreeMap auto-sorts water before lava as in vanilla for (_, fluid) in fluids { world .block_registry .on_entity_collision_fluid(fluid, caller.as_ref()) .await; } let lava_speed = if world.dimension == Dimension::THE_NETHER { 0.007 } else { 0.002_333_333 }; self.push_by_fluid(0.014, fluid_push[0], fluid_n[0]); self.push_by_fluid(lava_speed, fluid_push[1], fluid_n[1]); let water_height = fluid_height[0]; let in_water = in_fluid[0]; if in_water { if let Some(living) = caller.get_living_entity() { living.fall_distance.store(0.0); } if !self.touching_water.load(Ordering::SeqCst) { // TODO: Spawn splash particles } } self.water_height.store(water_height); self.touching_water.store(in_water, Ordering::SeqCst); let lava_height = fluid_height[1]; let in_lava = in_fluid[1]; if in_lava && let Some(living) = caller.get_living_entity() { let halved_fall = living.fall_distance.load() / 2.0; if halved_fall != 0.0 { living.fall_distance.store(halved_fall); } } self.lava_height.store(lava_height); self.touching_lava.store(in_lava, Ordering::SeqCst); } fn push_by_fluid(&self, speed: f64, mut push: Vector3, n: usize) { if push.length_squared() != 0.0 { if n > 0 { push = push * (1.0 / (n as f64)); } if self.entity_type != &EntityType::PLAYER { push = push.normalize(); } push = push * speed; let velo = self.velocity.load(); if velo.x.abs() < 0.003 && velo.z.abs() < 0.003 && velo.length_squared() < 0.000_020_25 { push = push.normalize() * 0.0045; } self.velocity.store(velo + push); } } async fn get_pos_with_y_offset( &self, offset: f64, ) -> ( BlockPos, Option<&'static Block>, Option<&'static BlockState>, ) { if let Some(mut supporting_block) = self.supporting_block_pos.load() { if offset > 1.0e-5 { let (block, state) = self .world .load() .get_block_and_state(&supporting_block) .await; // if let Some(props) = block.properties(state.id) { // let name = props.; // if offset <= 0.5 // && (name == "OakFenceLikeProperties" // || name == "ResinBrickWallLikeProperties" // || name == "OakFenceGateLikeProperties" // && OakFenceGateLikeProperties::from_state_id(state.id, &block) // .r#open) // { // return (supporting_block, Some(block), Some(state)); // } // } supporting_block.0.y = (self.pos.load().y - offset).floor() as i32; return (supporting_block, Some(block), Some(state)); } return (supporting_block, None, None); } let mut block_pos = self.block_pos.load(); block_pos.0.y = (self.pos.load().y - offset).floor() as i32; (block_pos, None, None) } async fn get_block_with_y_offset( &self, offset: f64, ) -> (BlockPos, &'static Block, &'static BlockState) { let (pos, block, state) = self.get_pos_with_y_offset(offset).await; if let (Some(b), Some(s)) = (block, state) { (pos, b, s) } else { let (b, s) = self.world.load().get_block_and_state(&pos).await; (pos, b, s) } } // Entity.updateVelocity in yarn fn update_velocity_from_input(&self, movement_input: Vector3, speed: f64) { let final_input = self.movement_input_to_velocity(movement_input, speed); self.velocity.store(self.velocity.load() + final_input); } // Entity.movementInputToVelocity in yarn fn movement_input_to_velocity(&self, movement_input: Vector3, speed: f64) -> Vector3 { let yaw = f64::from(self.yaw.load()).to_radians(); let dist = movement_input.length_squared(); if dist < 1.0e-7 { return Vector3::default(); } let input = if dist > 1.0 { movement_input.normalize() } else { movement_input * speed }; let sin = yaw.sin(); let cos = yaw.cos(); Vector3::new( input.x.mul_add(cos, -(input.z * sin)), input.y, input.z.mul_add(cos, input.x * sin), ) } #[expect(clippy::float_cmp)] async fn get_velocity_multiplier(&self) -> f32 { let block = self.world.load().get_block(&self.block_pos.load()).await; let multiplier = block.velocity_multiplier; if multiplier != 1.0 || block == &Block::WATER || block == &Block::BUBBLE_COLUMN { multiplier } else { let (_pos, block, _state) = self.get_block_with_y_offset(0.500_001).await; block.velocity_multiplier } } #[expect(clippy::float_cmp)] async fn get_jump_velocity_multiplier(&self) -> f32 { let f = self .world .load() .get_block(&self.block_pos.load()) .await .jump_velocity_multiplier; let g = self .get_block_with_y_offset(0.500_001) .await .1 .jump_velocity_multiplier; if f == 1f32 { g } else { f } } pub fn move_pos(&self, delta: Vector3) { self.set_pos(self.pos.load() + delta); } // Move by a delta, adjust for collisions, and send // Does not send movement. That must be done separately async fn move_entity(&self, caller: Arc, mut motion: Vector3) { if caller.get_player().is_some() { return; } if self.no_clip.load(Ordering::Relaxed) { self.move_pos(motion); return; } let movement_multiplier = self.movement_multiplier.swap(Vector3::default()); if movement_multiplier.length_squared() > 1.0e-7 { motion = motion.multiply( movement_multiplier.x, movement_multiplier.y, movement_multiplier.z, ); self.velocity.store(Vector3::default()); } let final_move = self.adjust_movement_for_collisions(motion).await; self.move_pos(final_move); let velocity_multiplier = f64::from(self.get_velocity_multiplier().await); self.velocity.store(final_move * velocity_multiplier); if let Some(living) = caller.get_living_entity() { living .fall( caller.clone(), final_move.y, self.on_ground.load(Ordering::SeqCst), false, ) .await; } } pub async fn push_out_of_blocks(&self, center_pos: Vector3) { let block_pos = BlockPos::floored_v(center_pos); let delta = center_pos.sub(&block_pos.0.to_f64()); let mut min_dist = f64::MAX; let mut direction = BlockDirection::Up; for dir in BlockDirection::all() { if dir == BlockDirection::Down { continue; } let offset = dir.to_offset(); if self .world .load() .get_block_state(&block_pos.offset(offset)) .await .is_full_cube() { continue; } let component = delta.get_axis(dir.to_axis().into()); let dist = if dir.positive() { 1.0 - component } else { component }; if dist < min_dist { min_dist = dist; direction = dir; } } let amplitude = rand::random::().mul_add(0.2, 0.1); let axis = direction.to_axis().into(); let sign = if direction.positive() { 1.0 } else { -1.0 }; let mut velo = self.velocity.load(); velo = velo * 0.75; velo.set_axis(axis, sign * amplitude); self.velocity.store(velo); } async fn tick_portal(&self, caller: &Arc) { if self.portal_cooldown.load(Ordering::Relaxed) > 0 { self.portal_cooldown.fetch_sub(1, Ordering::Relaxed); } let mut manager_guard = self.portal_manager.lock().await; let mut should_remove = false; if let Some(pmanager_mutex) = manager_guard.as_ref() { let mut portal_manager = pmanager_mutex.lock().await; if portal_manager.tick() { self.portal_cooldown .store(self.default_portal_cooldown(), Ordering::Relaxed); let pos = self.pos.load(); let current_yaw = self.yaw.load(); let dimensions = self.entity_dimension.load(); let scale_factor_new = portal_manager.portal_world.dimension.coordinate_scale; let scale_factor_current = self.world.load().dimension.coordinate_scale; let scale_factor = scale_factor_current / scale_factor_new; let target_pos = BlockPos::floored(pos.x * scale_factor, pos.y, pos.z * scale_factor); let dest_world = portal_manager.portal_world.clone(); let source_portal = portal_manager.source_portal.clone(); let source_axis = source_portal.as_ref().map(|p| p.axis); drop(portal_manager); let (teleport_pos, new_yaw) = if let Some(dest_result) = NetherPortal::search_for_portal(&dest_world, target_pos).await { let base_pos = source_portal.as_ref().map_or_else( || dest_result.get_teleport_position(), |source| { let source_result = PortalSearchResult { lower_corner: source.lower_corner, axis: source.axis, width: source.width, height: source.height, }; let relative_pos = source_result.entity_pos_in_portal(pos, &dimensions); dest_result.calculate_exit_position(relative_pos, &dimensions) }, ); let final_pos = dest_result .find_open_position(&dest_world, base_pos, &dimensions) .await; let yaw = dest_result.calculate_teleport_yaw(current_yaw, source_axis); (final_pos, Some(yaw)) } else if let Some((build_pos, axis, is_fallback)) = NetherPortal::find_safe_location( &dest_world, target_pos, pumpkin_data::block_properties::HorizontalAxis::X, ) .await { NetherPortal::build_portal_frame(&dest_world, build_pos, axis, is_fallback) .await; let new_portal = PortalSearchResult { lower_corner: build_pos, axis, width: 2, height: 3, }; let center_pos = new_portal.get_teleport_position(); let final_pos = new_portal .find_open_position(&dest_world, center_pos, &dimensions) .await; let yaw = new_portal.calculate_teleport_yaw(current_yaw, source_axis); (final_pos, Some(yaw)) } else { (target_pos.0.to_f64(), None) }; // Teleport the main entity caller .clone() .teleport(teleport_pos, new_yaw, None, dest_world.clone()) .await; // Teleport all passengers recursively along with the vehicle let yaw_delta = new_yaw.map(|y| y - current_yaw); Self::teleport_passengers_recursive(self, teleport_pos, yaw_delta, &dest_world) .await; } else if portal_manager.ticks_in_portal == 0 { should_remove = true; } } if should_remove { *manager_guard = None; } } /// Recursively teleports all passengers (and their passengers) to the destination fn teleport_passengers_recursive<'a>( entity: &'a Self, position: Vector3, yaw_delta: Option, dest_world: &'a Arc, ) -> std::pin::Pin + Send + 'a>> { Box::pin(async move { let passengers = entity.passengers.lock().await.clone(); for passenger in passengers { let passenger_entity = passenger.get_entity(); let passenger_yaw = yaw_delta.map(|delta| passenger_entity.yaw.load() + delta); passenger_entity.portal_cooldown.store( passenger_entity.default_portal_cooldown(), Ordering::Relaxed, ); // Get nested passengers before teleporting let nested_passengers = passenger_entity.passengers.lock().await.clone(); passenger .teleport(position, passenger_yaw, None, dest_world.clone()) .await; // Recursively teleport nested passengers for nested in nested_passengers { let nested_entity = nested.get_entity(); Self::teleport_passengers_recursive( nested_entity, position, yaw_delta, dest_world, ) .await; } } }) } pub async fn try_use_portal(&self, portal_delay: u32, portal_world: Arc, pos: BlockPos) { // Passengers don't teleport independently - they wait for their vehicle if self.has_vehicle().await { return; } if self.portal_cooldown.load(Ordering::Relaxed) > 0 { self.portal_cooldown .store(self.default_portal_cooldown(), Ordering::Relaxed); return; } if (portal_world.dimension == Dimension::THE_NETHER && !portal_world .server .upgrade() .unwrap() .basic_config .allow_nether) || (portal_world.dimension == Dimension::THE_END && !portal_world .server .upgrade() .unwrap() .basic_config .allow_end) { return; } let mut manager = self.portal_manager.lock().await; let world = self.world.load(); if manager.is_none() { let mut new_manager = PortalManager::new(portal_delay, portal_world, pos); if let Some(portal) = NetherPortal::get_on_axis( &world, &pos, pumpkin_data::block_properties::HorizontalAxis::X, ) .await && portal.was_already_valid() { new_manager.set_source_portal(SourcePortalInfo { lower_corner: portal.lower_corner(), axis: portal.axis(), width: portal.width(), height: portal.height(), }); } else if let Some(portal) = NetherPortal::get_on_axis( &world, &pos, pumpkin_data::block_properties::HorizontalAxis::Z, ) .await && portal.was_already_valid() { new_manager.set_source_portal(SourcePortalInfo { lower_corner: portal.lower_corner(), axis: portal.axis(), width: portal.width(), height: portal.height(), }); } *manager = Some(Mutex::new(new_manager)); } else if let Some(manager) = manager.as_ref() { let mut manager = manager.lock().await; manager.pos = pos; manager.in_portal = true; } } /// Extinguishes this entity. pub fn extinguish(&self) { self.fire_ticks.store(0, Ordering::Relaxed); } pub fn set_on_fire_for(&self, seconds: f32) { self.set_on_fire_for_ticks((seconds * 20.0).floor() as u32); } pub fn set_on_fire_for_ticks(&self, ticks: u32) { if self.fire_ticks.load(Ordering::Relaxed) < ticks as i32 { self.fire_ticks.store(ticks as i32, Ordering::Relaxed); } // TODO: defrost } /// Maximum freeze ticks (7 seconds at 20 tps) pub const MAX_FROZEN_TICKS: i32 = 140; /// Freeze damage is dealt every 40 ticks when fully frozen const FREEZE_DAMAGE_INTERVAL: i32 = 40; /// Check if the entity is currently in powder snow pub async fn is_in_powder_snow(&self) -> bool { let block_pos = self.block_pos.load(); self.world.load().get_block(&block_pos).await == &Block::POWDER_SNOW } /// Check if this entity type is immune to freezing pub fn is_freeze_immune(&self) -> bool { self.entity_type .has_tag(&tag::EntityType::MINECRAFT_FREEZE_IMMUNE_ENTITY_TYPES) } /// Ticks the frozen state of the entity. /// In powder snow: `frozen_ticks` increases by 1 (up to `MAX_FROZEN_TICKS`) /// Outside powder snow: `frozen_ticks` decreases by 2 (down to 0) /// When fully frozen, deals 1 damage every 40 ticks pub async fn tick_frozen(&self, caller: &dyn EntityBase) { // Freeze-immune entities don't accumulate freeze ticks if self.is_freeze_immune() { return; } let in_powder_snow = self.is_in_powder_snow().await; let old_frozen_ticks = self.frozen_ticks.load(Ordering::Relaxed); let new_frozen_ticks = if in_powder_snow { // Increase frozen ticks when in powder snow (old_frozen_ticks + 1).min(Self::MAX_FROZEN_TICKS) } else { // Decrease frozen ticks when not in powder snow (2x faster thaw rate) (old_frozen_ticks - 2).max(0) }; // Only update and send metadata if the value changed if new_frozen_ticks != old_frozen_ticks { self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed); self.send_meta_data(&[Metadata::new( TrackedData::DATA_FROZEN_TICKS, MetaDataType::Integer, VarInt(new_frozen_ticks), )]) .await; } // Deal freeze damage when fully frozen (every 40 ticks) if new_frozen_ticks >= Self::MAX_FROZEN_TICKS && self.age.load(Ordering::Relaxed) % Self::FREEZE_DAMAGE_INTERVAL == 0 { caller.damage(caller, 1.0, DamageType::FREEZE).await; } } /// Sets the `Entity` yaw & pitch rotation pub fn set_rotation(&self, yaw: f32, pitch: f32) { // TODO self.yaw.store(yaw); self.set_pitch(pitch); } pub fn set_pitch(&self, pitch: f32) { self.pitch.store(pitch.clamp(-90.0, 90.0) % 360.0); } /// Removes the `Entity` from their current `World` pub async fn remove(&self) { self.world.load().remove_entity(self).await; } pub fn create_spawn_packet(&self) -> CSpawnEntity { let entity_loc = self.pos.load(); let entity_vel = self.velocity.load(); CSpawnEntity::new( VarInt(self.entity_id), self.entity_uuid, VarInt(i32::from(self.entity_type.id)), entity_loc, self.pitch.load(), self.yaw.load(), self.head_yaw.load(), // todo: head_yaw and yaw are swapped, find out why self.data.load(Relaxed).into(), entity_vel, ) } pub fn width(&self) -> f32 { self.entity_dimension.load().width } pub fn height(&self) -> f32 { self.entity_dimension.load().height } /// Applies knockback to the entity, following vanilla Minecraft's mechanics. /// /// This function calculates the entity's new velocity based on the specified knockback strength and direction. pub fn knockback(&self, strength: f64, x: f64, z: f64) { // This has some vanilla magic let mut x = x; let mut z = z; while x.mul_add(x, z * z) < 1.0E-5 { x = (rand::random::() - rand::random::()) * 0.01; z = (rand::random::() - rand::random::()) * 0.01; } let var8 = Vector3::new(x, 0.0, z).normalize() * strength; let velocity = self.velocity.load(); self.velocity.store(Vector3::new( velocity.x / 2.0 - var8.x, if self.on_ground.load(Relaxed) { (velocity.y / 2.0 + strength).min(0.4) } else { velocity.y }, velocity.z / 2.0 - var8.z, )); } pub async fn set_sneaking(&self, sneaking: bool) { //assert!(self.sneaking.load(Relaxed) != sneaking); self.sneaking.store(sneaking, Relaxed); self.set_flag(Flag::Sneaking, sneaking).await; if sneaking { self.set_pose(EntityPose::Crouching).await; } else { self.set_pose(EntityPose::Standing).await; } } pub async fn set_invisible(&self, invisible: bool) { assert!(self.invisible.load(Relaxed) != invisible); self.invisible.store(invisible, Relaxed); self.set_flag(Flag::Invisible, invisible).await; } pub async fn set_on_fire(&self, on_fire: bool) { if self.has_visual_fire.load(Ordering::Relaxed) != on_fire { self.has_visual_fire.store(on_fire, Ordering::Relaxed); self.set_flag(Flag::OnFire, on_fire).await; } } pub fn get_horizontal_facing(&self) -> HorizontalFacing { let yaw = self.yaw.load(); // Use vanilla's formula: floor(angle / 90.0 + 0.5) & 3 let quarter_turns = ((yaw / 90.0) + 0.5).floor() as i32 & 3; match quarter_turns { 0 => HorizontalFacing::South, 1 => HorizontalFacing::West, 2 => HorizontalFacing::North, _ => HorizontalFacing::East, } } pub fn get_rotation_16(&self) -> Integer0To15 { let adjusted_yaw = self.yaw.load().rem_euclid(360.0); let index = (adjusted_yaw / 22.5).round() as u16 % 16; Integer0To15::from_index(index) } pub fn get_flipped_rotation_16(&self) -> Integer0To15 { match self.get_rotation_16() { Integer0To15::L0 => Integer0To15::L8, Integer0To15::L1 => Integer0To15::L9, Integer0To15::L2 => Integer0To15::L10, Integer0To15::L3 => Integer0To15::L11, Integer0To15::L4 => Integer0To15::L12, Integer0To15::L5 => Integer0To15::L13, Integer0To15::L6 => Integer0To15::L14, Integer0To15::L7 => Integer0To15::L15, Integer0To15::L8 => Integer0To15::L0, Integer0To15::L9 => Integer0To15::L1, Integer0To15::L10 => Integer0To15::L2, Integer0To15::L11 => Integer0To15::L3, Integer0To15::L12 => Integer0To15::L4, Integer0To15::L13 => Integer0To15::L5, Integer0To15::L14 => Integer0To15::L6, Integer0To15::L15 => Integer0To15::L7, } } pub fn get_facing(&self) -> Facing { let pitch = self.pitch.load().to_radians(); let yaw = -self.yaw.load().to_radians(); let (sin_p, cos_p) = pitch.sin_cos(); let (sin_y, cos_y) = yaw.sin_cos(); let x = sin_y * cos_p; let y = -sin_p; let z = cos_y * cos_p; let ax = x.abs(); let ay = y.abs(); let az = z.abs(); if ax > ay && ax > az { if x > 0.0 { Facing::East } else { Facing::West } } else if ay > ax && ay > az { if y > 0.0 { Facing::Up } else { Facing::Down } } else if z > 0.0 { Facing::South } else { Facing::North } } pub fn get_entity_facing_order(&self) -> [Facing; 6] { let pitch = self.pitch.load().to_radians(); let yaw = -self.yaw.load().to_radians(); let sin_p = pitch.sin(); let cos_p = pitch.cos(); let sin_y = yaw.sin(); let cos_y = yaw.cos(); let east_west = if sin_y > 0.0 { Facing::East } else { Facing::West }; let up_down = if sin_p < 0.0 { Facing::Up } else { Facing::Down }; let south_north = if cos_y > 0.0 { Facing::South } else { Facing::North }; let x_axis = sin_y.abs(); let y_axis = sin_p.abs(); let z_axis = cos_y.abs(); let x_weight = x_axis * cos_p; let z_weight = z_axis * cos_p; let (first, second, third) = if x_axis > z_axis { if y_axis > x_weight { (up_down, east_west, south_north) } else if z_weight > y_axis { (east_west, south_north, up_down) } else { (east_west, up_down, south_north) } } else if y_axis > z_weight { (up_down, south_north, east_west) } else if x_weight > y_axis { (south_north, east_west, up_down) } else { (south_north, up_down, east_west) }; [ first, second, third, third.opposite(), second.opposite(), first.opposite(), ] } pub async fn set_sprinting(&self, sprinting: bool) { //assert!(self.sprinting.load(Relaxed) != sprinting); self.sprinting.store(sprinting, Relaxed); self.set_flag(Flag::Sprinting, sprinting).await; } pub fn check_fall_flying(&self) -> bool { !self.on_ground.load(Relaxed) } pub async fn set_fall_flying(&self, fall_flying: bool) { assert!(self.fall_flying.load(Relaxed) != fall_flying); self.fall_flying.store(fall_flying, Relaxed); self.set_flag(Flag::FallFlying, fall_flying).await; } async fn set_flag(&self, flag: Flag, value: bool) { let index = flag as u8; let mut b = 0i8; if value { b |= 1 << index; } else { b &= !(1 << index); } self.send_meta_data(&[Metadata::new( TrackedData::DATA_FLAGS, MetaDataType::Byte, b, )]) .await; } /// Plays sound at this entity's position with the entity's sound category pub async fn play_sound(&self, sound: Sound) { self.world .load() .play_sound(sound, SoundCategory::Neutral, &self.pos.load()) .await; } pub async fn send_meta_data(&self, meta: &[Metadata]) { let mut buf = Vec::new(); for meta in meta { meta.write(&mut buf, &MinecraftVersion::V_1_21_11).unwrap(); } buf.put_u8(255); let world = self.world.load(); for player in world.players.load().iter() { if let ClientPlatform::Java(client) = &player.client { let mut buf = Vec::new(); for meta in meta { meta.write(&mut buf, &client.version.load()).unwrap(); } buf.put_u8(255); player .client .enqueue_packet(&CSetEntityMetadata::new(self.entity_id.into(), buf.into())) .await; } } } pub async fn set_pose(&self, pose: EntityPose) { let dimension = Self::get_entity_dimensions(pose); let position = self.pos.load(); let aabb = BoundingBox::new_from_pos(position.x, position.y, position.z, &dimension); if self .world .load() .is_space_empty(aabb.contract_all(1.0E-7)) .await { self.pose.store(pose); let dimension = Self::get_entity_dimensions(pose); self.bounding_box.store(aabb); self.entity_dimension.store(dimension); let pose = pose as i32; self.send_meta_data(&[Metadata::new( TrackedData::DATA_POSE, MetaDataType::EntityPose, VarInt(pose), )]) .await; } } pub fn is_invulnerable_to(&self, damage_type: &DamageType) -> bool { *damage_type != DamageType::GENERIC_KILL && (self.invulnerable.load(Relaxed) || self.damage_immunities.contains(damage_type)) } pub async fn check_block_collision(entity: &dyn EntityBase, server: &Server) { let aabb = entity.get_entity().bounding_box.load(); let blockpos = BlockPos::new( (aabb.min.x + 0.001).floor() as i32, (aabb.min.y + 0.001).floor() as i32, (aabb.min.z + 0.001).floor() as i32, ); let blockpos1 = BlockPos::new( (aabb.max.x - 0.001).floor() as i32, (aabb.max.y - 0.001).floor() as i32, (aabb.max.z - 0.001).floor() as i32, ); let world = entity.get_entity().world.load(); for x in blockpos.0.x..=blockpos1.0.x { for y in blockpos.0.y..=blockpos1.0.y { for z in blockpos.0.z..=blockpos1.0.z { let pos = BlockPos::new(x, y, z); let (block, state) = world.get_block_and_state(&pos).await; let block_outlines = state.get_block_outline_shapes(); if state.outline_shapes.is_empty() { world .block_registry .on_entity_collision(block, &world, entity, &pos, state, server) .await; let fluid = world.get_fluid(&pos).await; world .block_registry .on_entity_collision_fluid(fluid, entity) .await; continue; } for outline in block_outlines { let outline_aabb = outline.at_pos(pos); if outline_aabb.intersects(&aabb) { world .block_registry .on_entity_collision(block, &world, entity, &pos, state, server) .await; let fluid = world.get_fluid(&pos).await; world .block_registry .on_entity_collision_fluid(fluid, entity) .await; break; } } } } } } async fn teleport( &self, position: Vector3, yaw: Option, pitch: Option, _world: Arc, ) { // TODO: handle world change self.world .load() .broadcast_packet_all(&CEntityPositionSync::new( self.entity_id.into(), position, Vector3::new(0.0, 0.0, 0.0), yaw.unwrap_or(0.0), pitch.unwrap_or(0.0), self.on_ground.load(Ordering::SeqCst), )) .await; } pub fn get_eye_y(&self) -> f64 { self.pos.load().y + f64::from(self.entity_dimension.load().eye_height) } pub fn is_removed(&self) -> bool { self.removal_reason.load().is_some() } pub fn is_alive(&self) -> bool { !self.is_removed() } pub async fn has_passengers(&self) -> bool { !self.passengers.lock().await.is_empty() } pub async fn has_vehicle(&self) -> bool { let vehicle = self.vehicle.lock().await; vehicle.is_some() } pub async fn check_out_of_world(&self, dyn_self: &dyn EntityBase) { if self.pos.load().y < f64::from(self.world.load().dimension.min_y) - 64.0 { dyn_self.tick_in_void(dyn_self).await; } } pub async fn reset_state(&self) { self.pose.store(EntityPose::Standing); self.fall_flying.store(false, Relaxed); self.extinguish(); self.set_on_fire(false).await; } } impl NBTStorage for Entity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { let position = self.pos.load(); nbt.put_string( "id", format!("minecraft:{}", self.entity_type.resource_name), ); let uuid = self.entity_uuid.as_u128(); nbt.put( "UUID", NbtTag::IntArray(vec![ (uuid >> 96) as i32, ((uuid >> 64) & 0xFFFF_FFFF) as i32, ((uuid >> 32) & 0xFFFF_FFFF) as i32, (uuid & 0xFFFF_FFFF) as i32, ]), ); nbt.put( "Pos", NbtTag::List(vec![ position.x.into(), position.y.into(), position.z.into(), ]), ); let velocity = self.velocity.load(); nbt.put( "Motion", NbtTag::List(vec![ velocity.x.into(), velocity.y.into(), velocity.z.into(), ]), ); nbt.put( "Rotation", NbtTag::List(vec![self.yaw.load().into(), self.pitch.load().into()]), ); nbt.put_short("Fire", self.fire_ticks.load(Relaxed) as i16); nbt.put_bool("OnGround", self.on_ground.load(Relaxed)); nbt.put_bool("Invulnerable", self.invulnerable.load(Relaxed)); nbt.put_int("PortalCooldown", self.portal_cooldown.load(Relaxed) as i32); if self.has_visual_fire.load(Relaxed) { nbt.put_bool("HasVisualFire", true); } // todo more... }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { let position = nbt.get_list("Pos").unwrap(); let x = position[0].extract_double().unwrap_or(0.0); let y = position[1].extract_double().unwrap_or(0.0); let z = position[2].extract_double().unwrap_or(0.0); let pos = Vector3::new(x, y, z); self.set_pos(pos); self.first_loaded_chunk_position.store(Some(pos.to_i32())); let velocity = nbt.get_list("Motion").unwrap(); let x = velocity[0].extract_double().unwrap_or(0.0); let y = velocity[1].extract_double().unwrap_or(0.0); let z = velocity[2].extract_double().unwrap_or(0.0); self.velocity.store(Vector3::new(x, y, z)); let rotation = nbt.get_list("Rotation").unwrap(); let yaw = rotation[0].extract_float().unwrap_or(0.0); let pitch = rotation[1].extract_float().unwrap_or(0.0); self.set_rotation(yaw, pitch); self.head_yaw.store(yaw); self.fire_ticks .store(i32::from(nbt.get_short("Fire").unwrap_or(0)), Relaxed); self.on_ground .store(nbt.get_bool("OnGround").unwrap_or(false), Relaxed); self.invulnerable .store(nbt.get_bool("Invulnerable").unwrap_or(false), Relaxed); self.portal_cooldown .store(nbt.get_int("PortalCooldown").unwrap_or(0) as u32, Relaxed); self.has_visual_fire .store(nbt.get_bool("HasVisualFire").unwrap_or(false), Relaxed); // todo more... }) } } impl EntityBase for Entity { fn tick<'a>( &'a self, caller: Arc, _server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.tick_portal(&caller).await; self.update_fluid_state(&caller).await; self.check_out_of_world(&*caller).await; let fire_ticks = self.fire_ticks.load(Ordering::Relaxed); if fire_ticks > 0 { if self.entity_type.fire_immune { self.fire_ticks.store(fire_ticks - 4, Ordering::Relaxed); if self.fire_ticks.load(Ordering::Relaxed) < 0 { self.extinguish(); } } else { if fire_ticks % 20 == 0 { caller.damage(&*caller, 1.0, DamageType::ON_FIRE).await; } self.fire_ticks.store(fire_ticks - 1, Ordering::Relaxed); } } self.set_on_fire(self.fire_ticks.load(Ordering::Relaxed) > 0) .await; // Tick freeze state (powder snow) self.tick_frozen(&*caller).await; }) } fn teleport( self: Arc, position: Vector3, yaw: Option, pitch: Option, world: Arc, ) -> TeleportFuture { // TODO: handle world change Box::pin(async move { self.get_entity() .teleport(position, yaw, pitch, world) .await; }) } fn get_entity(&self) -> &Entity { self } fn get_living_entity(&self) -> Option<&LivingEntity> { None } fn as_nbt_storage(&self) -> &dyn NBTStorage { self } } pub type NbtFuture<'a, T> = Pin + Send + 'a>>; pub trait NBTStorage: Send + Sync { fn write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async {}) } fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { self.read_nbt_non_mut(nbt).await; }) } fn read_nbt_non_mut<'a>(&'a self, _nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async {}) } } pub type NBTInitFuture<'a, T> = Pin> + Send + 'a>>; pub trait NBTStorageInit: Send + Sync + Sized { fn create_from_nbt<'a>(_nbt: &'a mut NbtCompound) -> NBTInitFuture<'a, Self> where Self: 'a, { Box::pin(async move { None }) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Represents various entity flags that are sent in entity metadata. /// /// These flags are used by the client to modify the rendering of entities based on their current state. /// /// **Purpose:** /// /// This enum provides a more type-safe and readable way to represent entity flags compared to using raw integer values. pub enum Flag { /// Indicates if the entity is on fire. OnFire = 0, /// Indicates if the entity is sneaking. Sneaking = 1, /// Indicates if the entity is sprinting. Sprinting = 3, /// Indicates if the entity is swimming. Swimming = 4, /// Indicates if the entity is invisible. Invisible = 5, /// Indicates if the entity is glowing. Glowing = 6, /// Indicates if the entity is flying due to a fall. FallFlying = 7, }