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>
This commit is contained in:
Alexander Medvedev 2025-08-01 15:25:18 +02:00
parent e75e52cd37
commit 3f2484672c
14 changed files with 1728 additions and 53 deletions

View File

@ -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<MathAxis> for Axis {
fn from(a: MathAxis) -> Self {
match a {
MathAxis::X => Self::X,
MathAxis::Y => Self::Y,
MathAxis::Z => Self::Z,
}
}
}
impl From<Axis> 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<i32> 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,

View File

@ -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<f64>, max: Vector3<f64>) -> Self {
Self { min, max }
}

View File

@ -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<f64>,
}
#[derive(Clone, Copy, Debug)]
struct BoundingPlane {
pub min: Vector2<f64>,
pub max: Vector2<f64>,
}
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<f64> {
if max { self.max } else { self.min }
}
pub fn calculate_collision_time(
&self,
other: &Self,
movement: Vector3<f64>,
axis: Axis,
max_time: f64, // Start with 1.0
) -> Option<f64> {
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<f64>) -> Self {
Self {
min: self.min + delta,
max: self.max + delta,
}
}
pub fn stretch(&self, other: Vector3<f64>) -> 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 {

View File

@ -270,6 +270,22 @@ impl BlockPos {
))
}
pub fn floored_v(pos: Vector3<f64>) -> 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<f64>) -> 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<f64> {
Vector3::new(
self.0.x as f64 + 0.5,

View File

@ -13,6 +13,54 @@ pub struct Vector3<T> {
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<T: Copy> Vector3<T> {
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<T: Math + PartialOrd + Copy> Vector3<T> {
pub const fn new(x: T, y: T, z: T) -> Self {
Vector3 { x, y, z }

View File

@ -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;
}
}

View File

@ -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<ItemStack>,
pickup_delay: Mutex<u8>,
health: AtomicCell<f32>,
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<dyn EntityBase>| {
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<dyn EntityBase>, 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<Self>) -> Option<Arc<ItemEntity>> {
Some(self)
}
fn get_gravity(&self) -> f64 {
0.04
}
}

View File

@ -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<Vector3<f64>>,
/// 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<f32>,
pub active_effects: Mutex<HashMap<&'static StatusEffect, Effect>>,
pub entity_equipment: Arc<Mutex<EntityEquipment>>,
pub movement_input: AtomicCell<Vector3<f64>>,
pub movement_speed: AtomicCell<f64>,
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<Option<BlockPos>>,
_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<f64>) {
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)

View File

@ -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<Self>) -> Option<Arc<ItemEntity>> {
None
}
fn get_player(&self) -> Option<&Player> {
None
}
@ -224,8 +243,12 @@ pub struct Entity {
pub world: Arc<RwLock<Arc<World>>>,
/// The entity's current position in the world
pub pos: AtomicCell<Vector3<f64>>,
/// The last known position of the entity.
pub last_pos: AtomicCell<Vector3<f64>>,
/// The entity's position rounded to the nearest block coordinates
pub block_pos: AtomicCell<BlockPos>,
/// The block supporting the entity
pub supporting_block_pos: AtomicCell<Option<BlockPos>>,
/// The chunk coordinates of the entity's current position
pub chunk_pos: AtomicCell<Vector2<i32>>,
/// 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<Vector3<f64>>,
/// 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<f64>,
/// Indicates whether the entity is touching lava
pub touching_lava: AtomicBool,
/// Indicates the fluid height
pub lava_height: AtomicCell<f64>,
/// The entity's yaw rotation (horizontal rotation) ← →
pub yaw: AtomicCell<f32>,
/// The entity's head yaw rotation (horizontal rotation of the head)
@ -276,9 +309,16 @@ pub struct Entity {
pub custom_name: Option<TextComponent>,
/// 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<Vector3<f64>>,
/// 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<f64>) {
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<f64>) -> Vector3<f64> {
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::<f64>() - rand::random::<f64>()) * 0.01;
z = (rand::random::<f64>() - rand::random::<f64>()) * 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<dyn EntityBase>) {
// 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<dyn EntityBase>, 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<f64> {
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<dyn EntityBase>) {
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<f64>, 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<f64>, 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<f64>, speed: f64) -> Vector3<f64> {
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<f64>) {
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<dyn EntityBase>, mut motion: Vector3<f64>) {
// 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<f64>) {
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::<f64>() * 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<dyn EntityBase>) {
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<dyn EntityBase>, _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 {

View File

@ -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(&current_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));

View File

@ -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<dyn EntityBase>, server: &Server) {
async fn tick(&self, caller: Arc<dyn EntityBase>, 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
}
}

View File

@ -43,7 +43,7 @@ impl BedrockClient {
pub async fn player_pos_update(&self, player: &Arc<Player>, 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 {

View File

@ -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)

View File

@ -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<Self>, bounding_box: BoundingBox) -> Vec<Fluid> {
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<Self>, 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<f64> {
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<F>(
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<F>(
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<Self>,
bounding_box: BoundingBox,
) -> (Vec<BoundingBox>, 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<Self>,
chunk_pos: &Vector2<i32>,
@ -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<Player>, 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
}