From 3f2484672cc94b9463ea0009e5a7e5d6e8375a25 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 1 Aug 2025 15:25:18 +0200 Subject: [PATCH] Add Entity physics Thanks to akashbeh for his work https://github.com/Pumpkin-MC/Pumpkin/pull/824 Co-Authored-By: akashbeh <112662680+akashbeh@users.noreply.github.com> --- pumpkin-data/src/block_direction.rs | 25 +- pumpkin-data/src/collision_shape.rs | 7 + pumpkin-util/src/math/boundingbox.rs | 117 ++++ pumpkin-util/src/math/position.rs | 16 + pumpkin-util/src/math/vector3.rs | 48 ++ pumpkin/src/entity/ai/path/mod.rs | 24 +- pumpkin/src/entity/item.rs | 278 ++++++++- pumpkin/src/entity/living.rs | 41 +- pumpkin/src/entity/mod.rs | 832 ++++++++++++++++++++++++++- pumpkin/src/entity/player.rs | 7 +- pumpkin/src/entity/tnt.rs | 33 +- pumpkin/src/net/bedrock/play.rs | 2 +- pumpkin/src/net/java/play.rs | 6 +- pumpkin/src/world/mod.rs | 345 ++++++++++- 14 files changed, 1728 insertions(+), 53 deletions(-) diff --git a/pumpkin-data/src/block_direction.rs b/pumpkin-data/src/block_direction.rs index d13e4d21d..0b70dc386 100644 --- a/pumpkin-data/src/block_direction.rs +++ b/pumpkin-data/src/block_direction.rs @@ -1,6 +1,6 @@ use crate::block_properties::{Axis, Facing, HorizontalAxis, HorizontalFacing}; use pumpkin_util::{ - math::vector3::Vector3, + math::vector3::{Axis as MathAxis, Vector3}, random::{RandomGenerator, RandomImpl}, }; use serde::Deserialize; @@ -17,6 +17,25 @@ pub enum BlockDirection { East, } +impl From for Axis { + fn from(a: MathAxis) -> Self { + match a { + MathAxis::X => Self::X, + MathAxis::Y => Self::Y, + MathAxis::Z => Self::Z, + } + } +} +impl From for MathAxis { + fn from(a: Axis) -> MathAxis { + match a { + Axis::X => Self::X, + Axis::Y => Self::Y, + Axis::Z => Self::Z, + } + } +} + pub struct InvalidBlockFace; impl TryFrom for BlockDirection { @@ -94,6 +113,10 @@ impl BlockDirection { } } + pub fn positive(&self) -> bool { + matches!(self, Self::South | Self::East | Self::Up) + } + pub fn all() -> [BlockDirection; 6] { [ BlockDirection::Down, diff --git a/pumpkin-data/src/collision_shape.rs b/pumpkin-data/src/collision_shape.rs index 3d2f3c876..66c7403c2 100644 --- a/pumpkin-data/src/collision_shape.rs +++ b/pumpkin-data/src/collision_shape.rs @@ -11,6 +11,13 @@ impl CollisionShape { unimplemented!() } + pub fn to_bounding_box(&self) -> BoundingBox { + BoundingBox { + min: self.min, + max: self.max, + } + } + pub fn new(min: Vector3, max: Vector3) -> Self { Self { min, max } } diff --git a/pumpkin-util/src/math/boundingbox.rs b/pumpkin-util/src/math/boundingbox.rs index fa806739c..2a97ed122 100644 --- a/pumpkin-util/src/math/boundingbox.rs +++ b/pumpkin-util/src/math/boundingbox.rs @@ -1,3 +1,5 @@ +use crate::math::{vector2::Vector2, vector3::Axis}; + use super::{position::BlockPos, vector3::Vector3}; #[derive(Clone, Copy, Debug)] @@ -6,6 +8,39 @@ pub struct BoundingBox { pub max: Vector3, } +#[derive(Clone, Copy, Debug)] + +struct BoundingPlane { + pub min: Vector2, + pub max: Vector2, +} + +impl BoundingPlane { + pub fn intersects(&self, other: &Self) -> bool { + self.min.x < other.max.x + && self.max.x > other.min.x + && self.min.y < other.max.y + && self.max.y > other.min.y + } + + // Projecting a 3D box into 2D + pub fn from_box(bounding_box: &BoundingBox, excluded: Axis) -> Self { + let [axis1, axis2] = Axis::excluding(excluded); + + Self { + min: Vector2::new( + bounding_box.get_side(false).get_axis(axis1), + bounding_box.get_side(false).get_axis(axis2), + ), + + max: Vector2::new( + bounding_box.get_side(true).get_axis(axis1), + bounding_box.get_side(true).get_axis(axis2), + ), + } + } +} + impl BoundingBox { pub fn new_default(size: &EntityDimensions) -> Self { Self::new_from_pos(0., 0., 0., size) @@ -56,6 +91,88 @@ impl BoundingBox { } } + pub fn get_side(&self, max: bool) -> Vector3 { + if max { self.max } else { self.min } + } + + pub fn calculate_collision_time( + &self, + other: &Self, + movement: Vector3, + axis: Axis, + max_time: f64, // Start with 1.0 + ) -> Option { + let movement_on_axis = movement.get_axis(axis); + + if movement_on_axis == 0.0 { + return None; + } + + let move_positive = movement_on_axis.is_sign_positive(); + + let self_plane_const = self.get_side(move_positive).get_axis(axis); + + let other_plane_const = other.get_side(!move_positive).get_axis(axis); + + let collision_time = (other_plane_const - self_plane_const) / movement_on_axis; + + if collision_time < 0.0 || collision_time >= max_time { + return None; + } + + let self_moved = self.shift(movement * collision_time); + + let self_plane_moved = BoundingPlane::from_box(&self_moved, axis); + + let other_plane = BoundingPlane::from_box(other, axis); + + if !self_plane_moved.intersects(&other_plane) { + return None; + } + + Some(collision_time) + } + + pub fn min_block_pos(&self) -> BlockPos { + BlockPos::floored_v(self.min) + } + + pub fn max_block_pos(&self) -> BlockPos { + BlockPos::ceiled_v(self.max) + } + + pub fn shift(&self, delta: Vector3) -> Self { + Self { + min: self.min + delta, + + max: self.max + delta, + } + } + + pub fn stretch(&self, other: Vector3) -> Self { + let mut new = *self; + + if other.x < 0.0 { + new.min.x += other.x; + } else if other.x > 0.0 { + new.max.x += other.x; + } + + if other.y < 0.0 { + new.min.y += other.y; + } else if other.y > 0.0 { + new.max.y += other.y; + } + + if other.z < 0.0 { + new.min.z += other.z; + } else if other.z > 0.0 { + new.max.z += other.z; + } + + new + } + pub fn from_block_raw(position: &BlockPos) -> Self { let position = position.0; Self { diff --git a/pumpkin-util/src/math/position.rs b/pumpkin-util/src/math/position.rs index 16e2eaa7d..1cc8e4c07 100644 --- a/pumpkin-util/src/math/position.rs +++ b/pumpkin-util/src/math/position.rs @@ -270,6 +270,22 @@ impl BlockPos { )) } + pub fn floored_v(pos: Vector3) -> Self { + Self(Vector3::new( + pos.x.floor() as i32, + pos.y.floor() as i32, + pos.z.floor() as i32, + )) + } + + pub fn ceiled_v(pos: Vector3) -> Self { + Self(Vector3::new( + pos.x.ceil() as i32, + pos.y.ceil() as i32, + pos.z.ceil() as i32, + )) + } + pub fn to_f64(&self) -> Vector3 { Vector3::new( self.0.x as f64 + 0.5, diff --git a/pumpkin-util/src/math/vector3.rs b/pumpkin-util/src/math/vector3.rs index 0a6bfaf6d..0240084fb 100644 --- a/pumpkin-util/src/math/vector3.rs +++ b/pumpkin-util/src/math/vector3.rs @@ -13,6 +13,54 @@ pub struct Vector3 { pub z: T, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] + +pub enum Axis { + X, + + Y, + + Z, +} + +impl Axis { + pub fn all() -> [Self; 3] { + [Self::Y, Self::X, Self::Z] + } + + pub fn horizontal() -> [Self; 2] { + [Self::X, Self::Z] + } + + pub fn excluding(axis: Self) -> [Self; 2] { + match axis { + Self::X => [Self::Y, Self::Z], + + Self::Y => [Self::X, Self::Z], + + Self::Z => [Self::X, Self::Y], + } + } +} + +impl Vector3 { + pub fn get_axis(&self, a: Axis) -> T { + match a { + Axis::X => self.x, + Axis::Y => self.y, + Axis::Z => self.z, + } + } + + pub fn set_axis(&mut self, a: Axis, value: T) { + match a { + Axis::X => self.x = value, + Axis::Y => self.y = value, + Axis::Z => self.z = value, + }; + } +} + impl Vector3 { pub const fn new(x: T, y: T, z: T) -> Self { Vector3 { x, y, z } diff --git a/pumpkin/src/entity/ai/path/mod.rs b/pumpkin/src/entity/ai/path/mod.rs index 313647103..48a84183f 100644 --- a/pumpkin/src/entity/ai/path/mod.rs +++ b/pumpkin/src/entity/ai/path/mod.rs @@ -1,6 +1,3 @@ -use std::sync::atomic::Ordering; - -use pumpkin_protocol::java::client::play::CUpdateEntityPos; use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; use crate::entity::living::LivingEntity; @@ -77,25 +74,8 @@ impl Navigator { goal.current_progress += best_move.normalize() * goal.speed; // Now let's move - entity.set_pos(goal.current_progress); - let pos = entity.entity.pos.load(); - let last_pos = entity.last_pos.load(); - - entity - .entity - .world - .read() - .await - .broadcast_packet_all(&CUpdateEntityPos::new( - entity.entity.entity_id.into(), - Vector3::new( - pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, - pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, - pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, - ), - entity.entity.on_ground.load(Ordering::Relaxed), - )) - .await; + entity.entity.set_pos(goal.current_progress); + entity.entity.send_pos().await; } } diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index bb67478f0..17e3320bb 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -1,7 +1,10 @@ use core::f32; use std::sync::{ Arc, - atomic::{AtomicU32, Ordering::Relaxed}, + atomic::{ + AtomicBool, AtomicU32, + Ordering::{self}, + }, }; use async_trait::async_trait; @@ -27,6 +30,8 @@ pub struct ItemEntity { item_stack: Mutex, pickup_delay: Mutex, health: AtomicCell, + never_despawn: AtomicBool, + never_pickup: AtomicBool, } impl ItemEntity { @@ -45,6 +50,8 @@ impl ItemEntity { item_age: AtomicU32::new(0), pickup_delay: Mutex::new(10), // Vanilla pickup delay is 10 ticks health: AtomicCell::new(5.0), + never_despawn: AtomicBool::new(false), + never_pickup: AtomicBool::new(false), } } @@ -62,6 +69,127 @@ impl ItemEntity { item_age: AtomicU32::new(0), pickup_delay: Mutex::new(pickup_delay), // Vanilla pickup delay is 10 ticks health: AtomicCell::new(5.0), + never_despawn: AtomicBool::new(false), + never_pickup: AtomicBool::new(false), + } + } + + async fn can_merge(&self) -> bool { + if self.never_pickup.load(Ordering::Relaxed) || self.entity.removed.load(Ordering::Relaxed) + { + return false; + } + + let item_stack = self.item_stack.lock().await; + + item_stack.item_count < item_stack.get_max_stack_size() + } + + async fn try_merge(&self) { + let bounding_box = self.entity.bounding_box.load().expand(0.5, 0.0, 0.5); + + let items: Vec<_> = self + .entity + .world + .read() + .await + .entities + .read() + .await + .values() + .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().await { + self.try_merge_with(&item).await; + + if self.entity.removed.load(Ordering::SeqCst) { + break; + } + } + } + } + + async fn try_merge_with(&self, other: &Self) { + // Check if merge is possible + + let self_stack = self.item_stack.lock().await; + + let other_stack = other.item_stack.lock().await; + + 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) + }; + + // 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 mut target_delay = target.pickup_delay.lock().await; + + let delay = (*target_delay).max(*source.pickup_delay.lock().await); + + *target_delay = delay; + } + + if empty1 { + target.entity.remove().await; + } else { + target.init_data_tracker().await; + } + + if empty2 { + source.entity.remove().await; + } else { + source.init_data_tracker().await; } } } @@ -70,15 +198,147 @@ impl ItemEntity { impl EntityBase for ItemEntity { async fn tick(&self, caller: Arc, server: &Server) { let entity = &self.entity; - entity.tick(caller, server).await; + entity.tick(caller.clone(), server).await; { let mut delay = self.pickup_delay.lock().await; *delay = delay.saturating_sub(1); }; - let age = self.item_age.fetch_add(1, Relaxed); - if age >= 6000 { - entity.remove().await; + let original_velo = entity.velocity.load(); + + let mut velo = original_velo; + + 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 -= self.get_gravity(); + } + + entity.velocity.store(velo); + + let pos = entity.pos.load(); + + let bounding_box = entity.bounding_box.load(); + + let no_clip = !self + .entity + .world + .read() + .await + .is_space_empty(bounding_box.expand(-1.0e-7, -1.0e-7, -1.0e-7)) + .await; + + entity.no_clip.store(no_clip, Ordering::Relaxed); + + if no_clip { + entity + .push_out_of_blocks(Vector3::new( + pos.x, + f64::midpoint(bounding_box.min.y, bounding_box.max.y), + pos.z, + )) + .await; + } + + let mut velo = entity.velocity.load(); // In case push_out_of_blocks modifies it + + let mut tick_move = + !entity.on_ground.load(Ordering::SeqCst) || 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().await; + + return; + }; + + tick_move = (item_age + entity.entity_id) % 4 == 0; + } + + if tick_move { + entity.move_entity(caller.clone(), velo).await; + + entity.tick_block_collisions(&caller, server).await; + + let mut friction = 0.98; + + let on_ground = entity.on_ground.load(Ordering::SeqCst); + + if on_ground { + let block_affecting_velo = entity.get_block_with_y_offset(0.999_999).await.1; + + friction *= f64::from(block_affecting_velo.slipperiness); + } + + velo = velo.multiply(friction, 0.98, friction); + + if on_ground && velo.y < 0.0 { + velo = velo.multiply(1.0, -0.5, 1.0); + } + + entity.velocity.store(velo); + } + + if !self.never_despawn.load(Ordering::Relaxed) { + let age = self.item_age.fetch_add(1, Ordering::Relaxed) + 1; + + if age >= 6000 { + entity.remove().await; + + return; + } + + let n = if entity + .last_pos + .load() + .sub(&entity.pos.load()) + .length_squared() + == 0.0 + { + 40 + } else { + 2 + }; + + if age % n == 0 && self.can_merge().await { + self.try_merge().await; + } + } + + entity.update_fluid_state(&caller).await; + + 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.01; + + + || entity.velocity.load() != original_velo; + + if velocity_dirty { + entity.send_pos_rot().await; + + entity.send_velocity().await; } } @@ -158,4 +418,12 @@ impl EntityBase for ItemEntity { fn get_living_entity(&self) -> Option<&LivingEntity> { None } + + fn get_item_entity(self: Arc) -> Option> { + Some(self) + } + + fn get_gravity(&self) -> f64 { + 0.04 + } } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 6b4cfc1a0..4504af3de 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -1,4 +1,5 @@ use pumpkin_data::potion::Effect; +use pumpkin_util::math::position::BlockPos; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::{AtomicU8, Ordering::Relaxed}; @@ -37,8 +38,6 @@ use tokio::sync::Mutex; pub struct LivingEntity { /// The underlying entity object, providing basic entity information and functionality. pub entity: Entity, - /// The last known position of the entity. - pub last_pos: AtomicCell>, /// Tracks the remaining time until the entity can regenerate health. pub hurt_cooldown: AtomicI32, /// Stores the amount of damage the entity last received. @@ -52,6 +51,20 @@ pub struct LivingEntity { pub fall_distance: AtomicCell, pub active_effects: Mutex>, pub entity_equipment: Arc>, + pub movement_input: AtomicCell>, + + pub movement_speed: AtomicCell, + + pub jumping: AtomicBool, + + pub jumping_cooldown: AtomicU8, + + pub climbing: AtomicBool, + + /// The position where the entity was last climbing, used for death messages + pub climbing_pos: AtomicCell>, + + _water_movement_speed_multiplier: f32, } #[async_trait] @@ -63,10 +76,18 @@ pub trait LivingEntityTrait: EntityBase { impl LivingEntity { pub fn new(entity: Entity) -> Self { - let pos = entity.pos.load(); + let water_movement_speed_multiplier = if entity.entity_type == &EntityType::POLAR_BEAR { + 0.98 + } else if entity.entity_type == &EntityType::SKELETON_HORSE { + 0.96 + } else { + 0.8 + }; + + // TODO: Extract default MOVEMENT_SPEED Entity Attribute + let default_movement_speed = 0.25; Self { entity, - last_pos: AtomicCell::new(pos), hurt_cooldown: AtomicI32::new(0), last_damage_taken: AtomicCell::new(0.0), health: AtomicCell::new(20.0), @@ -75,6 +96,13 @@ impl LivingEntity { dead: AtomicBool::new(false), active_effects: Mutex::new(HashMap::new()), entity_equipment: Arc::new(Mutex::new(EntityEquipment::new())), + jumping: AtomicBool::new(false), + jumping_cooldown: AtomicU8::new(0), + climbing: AtomicBool::new(false), + climbing_pos: AtomicCell::new(None), + movement_input: AtomicCell::new(Vector3::default()), + movement_speed: AtomicCell::new(default_movement_speed), + _water_movement_speed_multiplier: water_movement_speed_multiplier, } } @@ -114,11 +142,6 @@ impl LivingEntity { .await; } - pub fn set_pos(&self, position: Vector3) { - self.last_pos.store(self.entity.pos.load()); - self.entity.set_pos(position); - } - pub async fn heal(&self, additional_health: f32) { assert!(additional_health > 0.0); self.set_health(self.health.load() + additional_health) diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 7e9ba7059..7e93fdaa9 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -1,3 +1,4 @@ +use crate::entity::item::ItemEntity; use crate::world::World; use crate::{server::Server, world::portal::PortalManager}; use async_trait::async_trait; @@ -6,7 +7,10 @@ use core::f32; use crossbeam::atomic::AtomicCell; use living::LivingEntity; use player::Player; +use pumpkin_data::BlockState; use pumpkin_data::block_properties::Integer0To15; +use pumpkin_data::fluid::Fluid; +use pumpkin_data::{Block, BlockDirection}; use pumpkin_data::{ block_properties::{Facing, HorizontalFacing}, damage::DamageType, @@ -14,6 +18,7 @@ use pumpkin_data::{ 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::{ @@ -23,6 +28,7 @@ use pumpkin_protocol::{ ser::serializer::Serializer, }; use pumpkin_registry::VanillaDimensionType; +use pumpkin_util::math::vector3::Axis; use pumpkin_util::math::{ boundingbox::{BoundingBox, EntityDimensions}, get_section_cord, @@ -34,6 +40,7 @@ use pumpkin_util::math::{ use pumpkin_util::text::TextComponent; use pumpkin_util::text::hover::HoverEvent; use serde::Serialize; +use std::collections::BTreeMap; use std::sync::{ Arc, atomic::{ @@ -106,6 +113,14 @@ pub trait EntityBase: Send + Sync { .await; } + fn is_pushed_by_fluids(&self) -> bool { + true + } + + fn get_gravity(&self) -> f64 { + 0.0 + } + /// Returns if damage was successful or not async fn damage(&self, amount: f32, damage_type: DamageType) -> bool { self.damage_with_context(amount, damage_type, None, None, None) @@ -138,6 +153,10 @@ pub trait EntityBase: Send + Sync { 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 } @@ -224,8 +243,12 @@ pub struct Entity { pub world: Arc>>, /// 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 @@ -236,8 +259,18 @@ pub struct Entity { 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) @@ -276,9 +309,16 @@ pub struct 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 { @@ -303,8 +343,15 @@ impl Entity { 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), @@ -341,11 +388,20 @@ impl Entity { 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; + } + + pub async fn send_velocity(&self) { + let velocity = self.velocity.load(); self.world .read() .await @@ -417,24 +473,42 @@ impl Entity { self.pitch.store(pitch); self.yaw.store(yaw); + self.send_rotation().await; + } + + 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 .read() .await .broadcast_packet_all(&CUpdateEntityRot::new( self.entity_id.into(), - yaw as u8, + 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 .read() .await - .broadcast_packet_all(&CHeadRot::new(self.entity_id.into(), yaw as u8)) + .broadcast_packet_all(&CHeadRot::new(self.entity_id.into(), head_yaw)) .await; } @@ -446,6 +520,757 @@ impl Entity { } } + #[allow(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 + .read() + .await + .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); + } + + 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; + + + } + + + } + + + } + + + */ + } + + // Returns whether the entity's eye level is in a wall + + async fn tick_block_collisions(&self, caller: &Arc, server: &Server) -> bool { + let bounding_box = self.bounding_box.load(); + + let mut suffocating = false; + + let aabb = bounding_box.expand(-0.001, -0.001, -0.001); + + let min = aabb.min_block_pos(); + + let max = aabb.max_block_pos(); + + let world = self.world.read().await; + + let mut eye_level_box = aabb; + + let eye_height = f64::from(self.standing_eye_height); + + eye_level_box.min.y += eye_height; + + eye_level_box.max.y = eye_level_box.min.y; + + 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 (block, state) = world.get_block_and_state(&pos).await; + + let collided = World::check_outline( + &bounding_box, + pos, + state, + !suffocating && state.is_solid(), + |collision_shape: &BoundingBox| { + suffocating = collision_shape.intersects(&eye_level_box); + }, + ); + + 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 + .read() + .await + .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 + .read() + .await + .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.read().await; + + 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 self.world.read().await.dimension_type == VanillaDimensionType::TheNether { + 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 { + if 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 + .read() + .await + .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.read().await.get_block_and_state(&pos).await; + + (pos, b, s) + } + } + + // Entity.updateVelocity in yarn + + #[allow(dead_code)] + 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 + + #[allow(dead_code)] + 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 * cos - input.z * sin, + input.y, + input.z * cos + input.x * sin, + ) + } + + #[allow(clippy::float_cmp)] + async fn get_velocity_multiplier(&self) -> f32 { + let world = self.world.read().await; + + let block = world.get_block(&self.block_pos.load()).await; + + let m1 = block.velocity_multiplier; + + if m1 != 1f32 || block == &Block::WATER || block == &Block::BUBBLE_COLUMN { + m1 + } else { + let (_pos, block, _state) = self.get_block_with_y_offset(0.500_001).await; + + block.velocity_multiplier + } + } + + #[allow(clippy::float_cmp)] + async fn _get_jump_velocity_multiplier(&self) -> f32 { + let world = self.world.read().await; + + let f = world + .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) { + // TODO: Player movement checking (anticheat) + + 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 + .update_fall_distance(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 + .read() + .await + .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::() * 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); @@ -977,6 +1802,7 @@ impl EntityBase for Entity { async fn tick(&self, caller: Arc, _server: &Server) { 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 { diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 1a4706bbf..4de21231d 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -588,6 +588,7 @@ impl Player { self.get_entity().set_pose(EntityPose::Sleeping).await; self.living_entity + .entity .set_pos(bed_head_pos.to_f64().add_raw(0.5, 0.6875, 0.5)); self.get_entity() .send_meta_data(&[Metadata::new( @@ -1094,7 +1095,7 @@ impl Player { new_world.players.write().await.insert(uuid, self.clone()); self.unload_watched_chunks(¤t_world).await; - let last_pos = self.living_entity.last_pos.load(); + let last_pos = self.living_entity.entity.last_pos.load(); let death_dimension = self.world().await.dimension_type.resource_location(); let death_location = BlockPos(Vector3::new( last_pos.x.round() as i32, @@ -1118,7 +1119,7 @@ impl Player { ; self.send_permission_lvl_update().await; self.clone().request_teleport(position, yaw, pitch).await; - self.living_entity.last_pos.store(position); + self.living_entity.entity.last_pos.store(position); self.send_abilities_update().await; self.enqueue_set_held_item_packet(&CSetSelectedSlot::new( self.get_inventory().get_selected_slot() as i8, @@ -1153,7 +1154,7 @@ impl Player { .teleport_id_count .fetch_add(1, Ordering::Relaxed); let teleport_id = i + 1; - self.living_entity.set_pos(position); + self.living_entity.entity.set_pos(position); let entity = &self.living_entity.entity; entity.set_rotation(yaw, pitch); *self.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); diff --git a/pumpkin/src/entity/tnt.rs b/pumpkin/src/entity/tnt.rs index 633d6dde3..6b5f56a97 100644 --- a/pumpkin/src/entity/tnt.rs +++ b/pumpkin/src/entity/tnt.rs @@ -12,7 +12,10 @@ use std::{ f64::consts::TAU, sync::{ Arc, - atomic::{AtomicU32, Ordering::Relaxed}, + atomic::{ + AtomicU32, + Ordering::{self, Relaxed}, + }, }, }; @@ -34,7 +37,27 @@ impl TNTEntity { #[async_trait] impl EntityBase for TNTEntity { - async fn tick(&self, _caller: Arc, server: &Server) { + async fn tick(&self, caller: Arc, server: &Server) { + let entity = &self.entity; + let original_velo = entity.velocity.load(); + + let mut velo = original_velo; + velo.y -= self.get_gravity(); + + entity.move_entity(caller.clone(), velo).await; + entity.tick_block_collisions(&caller, server).await; + entity.velocity.store(velo.multiply(0.98, 0.98, 0.98)); + if entity.on_ground.load(Ordering::Relaxed) { + entity.velocity.store(velo.multiply(0.7, -0.5, 0.7)); + } + let velocity_dirty = entity.velocity_dirty.swap(false, Ordering::SeqCst); + + if velocity_dirty { + entity.send_pos_rot().await; + + entity.send_velocity().await; + } + let fuse = self.fuse.fetch_sub(1, Relaxed); if fuse == 0 { self.entity.remove().await; @@ -44,6 +67,8 @@ impl EntityBase for TNTEntity { .await .explode(server, self.entity.pos.load(), self.power) .await; + } else { + entity.update_fluid_state(&caller).await; } } @@ -89,4 +114,8 @@ impl EntityBase for TNTEntity { fn get_living_entity(&self) -> Option<&LivingEntity> { None } + + fn get_gravity(&self) -> f64 { + 0.04 + } } diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 0565f4306..e370ce192 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -43,7 +43,7 @@ impl BedrockClient { pub async fn player_pos_update(&self, player: &Arc, packet: SPlayerAuthInput) { let pos = packet.position; - player.living_entity.set_pos(pos.to_f64()); + player.living_entity.entity.set_pos(pos.to_f64()); chunker::update_position(player).await; //self.send_game_packet(&CMovePlayer { diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index c946f8c7a..59f2e41c1 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -180,7 +180,7 @@ impl JavaClient { if id == &confirm_teleport.teleport_id { // We should set the position now to what we requested in the teleport packet. // This may fix issues when the client sends the position while being teleported. - player.living_entity.set_pos(*position); + player.living_entity.entity.set_pos(*position); *awaiting_teleport = None; } else { @@ -294,7 +294,7 @@ impl JavaClient { let pos = event.to; let entity = &player.living_entity.entity; let last_pos = entity.pos.load(); - player.living_entity.set_pos(pos); + player.living_entity.entity.set_pos(pos); let height_difference = pos.y - last_pos.y; if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 { @@ -395,7 +395,7 @@ impl JavaClient { let pos = event.to; let entity = &player.living_entity.entity; let last_pos = entity.pos.load(); - player.living_entity.set_pos(pos); + player.living_entity.entity.set_pos(pos); let height_difference = pos.y - last_pos.y; if entity.on_ground.load(Ordering::Relaxed) diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index d41d9c8b6..22f7ccc2f 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -39,7 +39,7 @@ use explosion::Explosion; use pumpkin_config::BasicConfiguration; use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::entity::MobCategory; -use pumpkin_data::fluid::{Falling, FluidProperties}; +use pumpkin_data::fluid::{Falling, FluidProperties, FluidState}; use pumpkin_data::{ Block, entity::{EntityStatus, EntityType}, @@ -773,6 +773,309 @@ impl World { } } + pub async fn get_fluid_collisions(self: &Arc, bounding_box: BoundingBox) -> Vec { + let mut collisions = Vec::new(); + + let min = bounding_box.min_block_pos(); + + let max = bounding_box.max_block_pos(); + + 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) = self.get_fluid_and_fluid_state(&pos).await; + + if fluid.id != Fluid::EMPTY.id { + let height = f64::from(state.height); + + if height >= bounding_box.min.y { + collisions.push(fluid); + } + } + } + } + } + + collisions + } + + pub async fn check_fluid_collision(self: &Arc, bounding_box: BoundingBox) -> bool { + let min = bounding_box.min_block_pos(); + + let max = bounding_box.max_block_pos(); + + 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) = self.get_fluid_and_fluid_state(&pos).await; + + if fluid.id != Fluid::EMPTY.id { + let height = f64::from(state.height); + + if height >= bounding_box.min.y { + return true; + } + } + } + } + } + + false + } + + // FlowableFluid.getVelocity() + + pub async fn get_fluid_velocity( + &self, + + pos0: BlockPos, + + fluid0: &Fluid, + + state0: &FluidState, + ) -> Vector3 { + let mut velo = Vector3::default(); + + for dir in BlockDirection::horizontal() { + let mut amplitude = 0.0; + + let offset = dir.to_offset(); + + let pos = pos0.offset(offset); + + let block_state_id = self.get_block_state_id(&pos).await; + + let fluid = Fluid::from_state_id(block_state_id).unwrap_or(&Fluid::EMPTY); + + if fluid.id == Fluid::EMPTY.id { + let block = Block::from_state_id(block_state_id); + let block_state = BlockState::from_id(block_state_id); + + let blocks_movement = block_state.is_solid() + && block != &Block::COBWEB + && block != &Block::BAMBOO_SAPLING; + + if !blocks_movement { + let down_pos = pos.down(); + + let (down_fluid, down_state) = self.get_fluid_and_fluid_state(&down_pos).await; + + if down_fluid.id == fluid0.id { + amplitude = f64::from(state0.height - down_state.height) + 0.888_888_9; + } + } + } else { + if fluid.id != fluid0.id { + continue; + } + + //let state = fluid.get_state(block_state_id); + let state = fluid.states[0].clone(); + + amplitude = f64::from(state0.height - state.height); + } + + if amplitude == 0.0 { + continue; + } + + velo.x += f64::from(offset.x) * amplitude; + + velo.z += f64::from(offset.z) * amplitude; + } + + // TODO: FALLING + + if state0.falling { + for dir in BlockDirection::horizontal() { + let pos = pos0.offset(dir.to_offset()); + + if self.is_flow_blocked(fluid0.id, pos, dir).await + || self.is_flow_blocked(fluid0.id, pos.up(), dir).await + { + if velo.length_squared() != 0.0 { + velo = velo.normalize(); + } + + velo.y -= 6.0; + + break; + } + } + } + + if velo.length_squared() == 0.0 { + velo + } else { + velo.normalize() + } + } + + // FlowableFluid.isFlowBlocked() + + async fn is_flow_blocked( + &self, + fluid0_id: u16, + pos: BlockPos, + direction: BlockDirection, + ) -> bool { + let id = self.get_block_state_id(&pos).await; + + let fluid = Fluid::from_state_id(id).unwrap_or(&Fluid::EMPTY); + + if fluid.id == fluid0_id { + return false; + } + + if direction == BlockDirection::Up { + return true; + } + + let block = Block::from_state_id(id); + let state = BlockState::from_id(id); + + // Doesn't count blue ice or packed ice + + if block == &Block::ICE || block == &Block::FROSTED_ICE { + return false; + } + + state.is_side_solid(direction) + } + + pub fn check_outline( + bounding_box: &BoundingBox, + + pos: BlockPos, + + state: &BlockState, + + use_outline_shape: bool, + + mut using_outline_shape: F, + ) -> bool + where + F: FnMut(&BoundingBox), + { + if state.outline_shapes.is_empty() { + // Apparently we need this for air and moving pistons + + return true; + } + + let mut inside = false; + + 'shapes: for shape in state.get_block_outline_shapes().unwrap() { + let outline_shape = shape.at_pos(pos); + + if outline_shape.intersects(bounding_box) { + inside = true; + + if !use_outline_shape { + break 'shapes; + } + + using_outline_shape(&outline_shape.to_bounding_box()); + } + } + + inside + } + + pub fn check_collision( + bounding_box: &BoundingBox, + pos: BlockPos, + state: &BlockState, + use_collision_shape: bool, + mut using_collision_shape: F, + ) -> bool + where + F: FnMut(&BoundingBox), + { + let mut collided = false; + + if !state.is_air() && state.is_solid() && !state.collision_shapes.is_empty() { + for shape in state.get_block_collision_shapes() { + let collision_shape = shape.at_pos(pos); + + if collision_shape.intersects(bounding_box) { + collided = true; + + if !use_collision_shape { + break; + } + + using_collision_shape(&collision_shape.to_bounding_box()); + } + } + } + + collided + } + + // For adjusting movement + + pub async fn get_block_collisions( + self: &Arc, + + bounding_box: BoundingBox, + ) -> (Vec, Vec<(usize, BlockPos)>) { + let mut collisions = Vec::new(); + + let mut positions = Vec::new(); + + // Include downwards for fences + + let min = BlockPos::floored_v(bounding_box.min.add_raw(0.0, -0.50001, 0.0)); + + let max = bounding_box.max_block_pos(); + + 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 state = self.get_block_state(&pos).await; + + let collided = Self::check_collision( + &bounding_box, + pos, + state, + true, + |collision_shape: &BoundingBox| { + collisions.push(*collision_shape); + }, + ); + + if collided { + positions.push((collisions.len(), pos)); + } + } + } + } + + (collisions, positions) + } + + pub async fn is_space_empty(&self, bounding_box: BoundingBox) -> bool { + let min = bounding_box.min_block_pos(); + let max = bounding_box.max_block_pos(); + + for pos in BlockPos::iterate(min, max) { + let state = self.get_block_state(&pos).await; + let collided = Self::check_collision(&bounding_box, pos, state, false, |_| ()); + + if collided { + return false; + } + } + true + } + pub async fn tick_spawning_chunk( self: &Arc, chunk_pos: &Vector2, @@ -1149,7 +1452,7 @@ impl World { log::debug!("Sending player teleport to {}", player.gameprofile.name); player.request_teleport(position, yaw, pitch).await; - player.living_entity.last_pos.store(position); + player.living_entity.entity.last_pos.store(position); let gameprofile = &player.gameprofile; // Firstly, send an info update to our new player, so they can see their skin @@ -1447,7 +1750,7 @@ impl World { } pub async fn respawn_player(&self, player: &Arc, alive: bool) { - let last_pos = player.living_entity.last_pos.load(); + let last_pos = player.living_entity.entity.last_pos.load(); let death_dimension = player.world().await.dimension_type.resource_location(); let death_location = BlockPos(Vector3::new( last_pos.x.round() as i32, @@ -1511,7 +1814,7 @@ impl World { log::debug!("Sending player teleport to {}", player.gameprofile.name); player.request_teleport(position, yaw, pitch).await; - player.living_entity.last_pos.store(position); + player.living_entity.entity.last_pos.store(position); // TODO: difficulty, exp bar, status effect @@ -2449,6 +2752,40 @@ impl World { (block, fluid) } + pub async fn get_fluid_and_fluid_state(&self, position: &BlockPos) -> (Fluid, FluidState) { + let id = self.get_block_state_id(position).await; + + let Some(fluid) = Fluid::from_state_id(id) else { + let block = Block::from_state_id(id); + if let Some(properties) = block.properties(id) { + for (name, value) in properties.to_props() { + if name == *"waterlogged" { + if value == true.to_string() { + let fluid = Fluid::FLOWING_WATER; + + let state = fluid.states[0].clone(); + + return (fluid, state); + } + + break; + } + } + } + + let fluid = Fluid::EMPTY; + + let state = fluid.states[0].clone(); + + return (fluid, state); + }; + + //let state = fluid.get_state(id); + let state = fluid.states[0].clone(); + + (fluid.clone(), state) + } + pub async fn get_block_state_id(&self, position: &BlockPos) -> BlockStateId { self.level.get_block_state(position).await.0 }