More Entity Physics work

Co-Authored-By: akashbeh <112662680+akashbeh@users.noreply.github.com>
This commit is contained in:
Alexander Medvedev 2025-08-02 16:24:06 +02:00
parent a4180e0512
commit 6e4a05d66d
8 changed files with 533 additions and 43 deletions

View File

@ -0,0 +1,7 @@
use pumpkin_data::packet::serverbound::PLAY_CUSTOM_PAYLOAD;
use pumpkin_macros::packet;
#[packet(PLAY_CUSTOM_PAYLOAD)]
pub struct SCustomPayload {
// TODO
}

View File

@ -10,6 +10,7 @@ mod close_container;
mod command_suggestion;
mod confirm_teleport;
mod cookie_response;
mod custom_payload;
mod interact;
mod keep_alive;
mod pick_item;
@ -44,6 +45,7 @@ pub use close_container::*;
pub use command_suggestion::*;
pub use confirm_teleport::*;
pub use cookie_response::*;
pub use custom_payload::*;
pub use interact::*;
pub use keep_alive::*;
pub use pick_item::*;

View File

@ -76,7 +76,31 @@ impl ExperienceOrbEntity {
#[async_trait]
impl EntityBase for ExperienceOrbEntity {
async fn tick(&self, caller: Arc<dyn EntityBase>, server: &Server) {
self.entity.tick(caller, server).await;
let entity = &self.entity;
entity.tick(caller.clone(), server).await;
let bounding_box = entity.bounding_box.load();
let original_velo = entity.velocity.load();
let mut velo = original_velo;
let no_clip = !self
.entity
.world
.read()
.await
.is_space_empty(bounding_box.expand(-1.0e-7, -1.0e-7, -1.0e-7))
.await;
// TODO: isSubmergedIn
if !no_clip {
velo.y -= self.get_gravity();
}
entity.velocity.store(velo);
entity.move_entity(caller.clone(), velo).await;
entity.tick_block_collisions(&caller, server).await;
let age = self.orb_age.fetch_add(1, Ordering::Relaxed);
if age >= 6000 {
@ -115,4 +139,8 @@ impl EntityBase for ExperienceOrbEntity {
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn get_gravity(&self) -> f64 {
0.03
}
}

View File

@ -1,8 +1,10 @@
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};
use std::sync::atomic::{
AtomicBool, AtomicU8,
Ordering::{Relaxed, SeqCst},
};
use std::{collections::HashMap, sync::atomic::AtomicI32};
use super::{Entity, NBTStorage};
@ -64,7 +66,7 @@ pub struct LivingEntity {
/// The position where the entity was last climbing, used for death messages
pub climbing_pos: AtomicCell<Option<BlockPos>>,
_water_movement_speed_multiplier: f32,
water_movement_speed_multiplier: f32,
}
#[async_trait]
@ -102,7 +104,7 @@ impl LivingEntity {
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,
water_movement_speed_multiplier,
}
}
@ -201,6 +203,445 @@ impl LivingEntity {
world.get_block(&block_pos).await == &Block::POWDER_SNOW
}
async fn get_effective_gravity(&self, caller: &Arc<dyn EntityBase>) -> f64 {
let final_gravity = caller.get_gravity();
if self.entity.velocity.load().y <= 0.0
&& self.has_effect(&StatusEffect::SLOW_FALLING).await
{
final_gravity.min(0.01)
} else {
final_gravity
}
}
async fn tick_movement(&self, server: &Server, caller: Arc<dyn EntityBase>) {
if self.jumping_cooldown.load(Relaxed) != 0 {
self.jumping_cooldown.fetch_sub(1, Relaxed);
}
let should_swim_in_fluids = if let Some(player) = caller.get_player() {
!player.is_flying().await
} else {
true
};
self.entity.check_zero_velo();
let mut movement_input = self.movement_input.load();
movement_input.x *= 0.98;
movement_input.z *= 0.98;
self.movement_input.store(movement_input);
// TODO: Tick AI
if self.jumping.load(SeqCst) && should_swim_in_fluids {
let in_lava = self.entity.touching_lava.load(SeqCst);
let in_water = self.entity.touching_water.load(SeqCst);
let fluid_height = if in_lava {
self.entity.lava_height.load()
} else {
self.entity.water_height.load()
};
let swim_height = self.get_swim_height();
let on_ground = self.entity.on_ground.load(SeqCst);
if (in_water || in_lava) && (!on_ground || fluid_height > swim_height) {
// Swim upward
let mut velo = self.entity.velocity.load();
velo.y += 0.04;
self.entity.velocity.store(velo);
} else if (on_ground || in_water && fluid_height <= swim_height)
&& self.jumping_cooldown.load(SeqCst) == 0
{
self.jump().await;
self.jumping_cooldown.store(10, SeqCst);
}
} else {
self.jumping_cooldown.store(0, SeqCst);
}
if self.has_effect(&StatusEffect::SLOW_FALLING).await
|| self.has_effect(&StatusEffect::LEVITATION).await
{
self.fall_distance.store(0.0);
}
let touching_water = self.entity.touching_water.load(SeqCst);
// Strider is the only entity that has canWalkOnFluid = false
if (touching_water || self.entity.touching_lava.load(SeqCst))
&& should_swim_in_fluids
&& self.entity.entity_type != &EntityType::STRIDER
{
self.travel_in_fluid(caller.clone(), touching_water).await;
} else {
// TODO: Gliding
self.travel_in_air(caller.clone()).await;
}
//self.entity.tick_block_underneath(&caller);
let suffocating = self.entity.tick_block_collisions(&caller, server).await;
if suffocating {
self.damage(1.0, DamageType::IN_WALL).await;
}
}
async fn travel_in_air(&self, caller: Arc<dyn EntityBase>) {
// applyMovementInput
let (speed, friction) = if self.entity.on_ground.load(SeqCst) {
// getVelocityAffectingPos
let slipperiness = f64::from(
self.entity
.get_block_with_y_offset(0.500_001)
.await
.1
.slipperiness,
);
let speed =
self.movement_speed.load() * 0.216 / (slipperiness * slipperiness * slipperiness);
(speed, slipperiness * 0.91)
} else {
let speed = if let Some(player) = caller.get_player() {
player.get_off_ground_speed().await
} else {
// TODO: If the passenger is a player, ogs = movement_speed * 0.1
0.02
};
(speed, 0.91)
};
self.entity
.update_velocity_from_input(self.movement_input.load(), speed);
self.apply_climbing_speed();
self.make_move(caller.clone()).await;
let mut velo = self.entity.velocity.load();
// TODO: Add powdered snow
if (self.entity.horizontal_collision.load(SeqCst) || self.jumping.load(SeqCst))
&& (self.climbing.load(Relaxed))
{
velo.y = 0.2;
}
let levitation = self.get_effect(&StatusEffect::LEVITATION).await;
if let Some(lev) = levitation {
velo.y += (0.05 * f64::from(lev.amplifier + 1) - velo.y) * 0.2;
} else {
velo.y -= self.get_effective_gravity(&caller).await;
// TODO: If world is not loaded: replace effective gravity with:
// if below world's bottom y then -0.1, else 0.0
}
// If entity has no drag: store velo and return
velo.x *= friction;
velo.z *= friction;
velo.y *= if caller.is_flutterer() {
friction
} else {
0.98
};
self.entity.velocity.store(velo);
}
async fn travel_in_fluid(&self, caller: Arc<dyn EntityBase>, water: bool) {
let movement_input = self.movement_input.load();
let y0 = self.entity.pos.load().y;
let falling = self.entity.velocity.load().y <= 0.0;
let gravity = self.get_effective_gravity(&caller).await;
if water {
let mut friction = if self.entity.sprinting.load(Relaxed) {
0.9
} else {
f64::from(self.water_movement_speed_multiplier)
};
let mut speed = 0.02;
let mut water_movement_efficiency = 0.0; // TODO: Entity attribute
if water_movement_efficiency > 0.0 {
if !self.entity.on_ground.load(SeqCst) {
water_movement_efficiency *= 0.5;
}
friction += (0.546_000_06 - friction) * water_movement_efficiency;
speed += (self.movement_speed.load() - speed) * water_movement_efficiency;
}
if self.has_effect(&StatusEffect::DOLPHINS_GRACE).await {
friction = 0.96;
}
self.entity
.update_velocity_from_input(movement_input, speed);
self.make_move(caller).await;
let mut velo = self.entity.velocity.load();
if self.entity.horizontal_collision.load(SeqCst) && self.climbing.load(Relaxed) {
velo.y = 0.2;
}
velo = velo.multiply(friction, 0.8, friction);
self.apply_fluid_moving_speed(&mut velo.y, gravity, falling);
self.entity.velocity.store(velo);
} else {
self.entity.update_velocity_from_input(movement_input, 0.02);
self.make_move(caller).await;
let mut velo = self.entity.velocity.load();
if self.entity.lava_height.load() <= self.get_swim_height() {
velo.x *= 0.5;
velo.z *= 0.5;
velo.y *= 0.8;
self.apply_fluid_moving_speed(&mut velo.y, gravity, falling);
} else {
velo = velo * 0.5;
}
if gravity != 0.0 {
velo.y -= gravity / 4.0; // Negative gravity = buoyancy
}
self.entity.velocity.store(velo);
}
let mut velo = self.entity.velocity.load();
velo.y += 0.6 - self.entity.pos.load().y + y0;
if self.entity.horizontal_collision.load(SeqCst)
&& !self
.entity
.world
.read()
.await
.check_fluid_collision(self.entity.bounding_box.load().shift(velo))
.await
{
velo.y = 0.3;
self.entity.velocity.store(velo);
}
}
fn apply_fluid_moving_speed(&self, dy: &mut f64, gravity: f64, falling: bool) {
if gravity != 0.0 && !self.entity.sprinting.load(Relaxed) {
if falling && (*dy - 0.005).abs() >= 0.003 && (*dy - gravity / 16.0).abs() < 0.003 {
*dy = -0.003;
} else {
*dy -= gravity / 16.0;
}
}
}
async fn make_move(&self, caller: Arc<dyn EntityBase>) {
self.entity
.move_entity(caller, self.entity.velocity.load())
.await;
self.check_climbing();
}
fn check_climbing(&self) {
// If spectator: return false
// TODO
// let mut pos = self.entity.block_pos.load();
// let world = self.entity.world.read().await;
// let (block, state) = world.get_block_and_state(&pos).await;
// let name = block.properties(state.id).map(|props| props.name());
// if let Some(name) = name {
// if name == "LadderLikeProperties"
// || name == "ScaffoldingLikeProperties"
// || name == "CaveVinesLikeProperties"
// || name == "CaveVinesPlantLikeProperties"
// {
// self.climbing.store(true, Relaxed);
// self.climbing_pos.store(Some(pos));
// return;
// }
// if name == "OakTrapdoorLikeProperties" {
// let trapdoor = OakTrapdoorLikeProperties::from_state_id(state.id, &block);
// pos.0.y -= 1;
// let (down_block, down_state) = world.get_block_and_state(&pos).await;
// let is_ladder = down_block
// .properties(down_state.id)
// .is_some_and(|down_props| down_props.name() == "LadderLikeProperties");
// if is_ladder {
// let ladder = LadderLikeProperties::from_state_id(down_state.id, &down_block);
// if trapdoor.r#facing == ladder.r#facing {
// self.climbing.store(true, Relaxed);
// self.climbing_pos.store(Some(pos));
// return;
// }
// }
// }
// }
self.climbing.store(false, Relaxed);
if self.entity.on_ground.load(SeqCst) {
self.climbing_pos.store(None);
}
}
fn apply_climbing_speed(&self) {
if self.climbing.load(Relaxed) {
self.fall_distance.store(0.0);
let mut velo = self.entity.velocity.load();
let pos = 0.15;
let neg = -0.15;
if velo.x < neg {
velo.x = neg;
} else if velo.x > pos {
velo.x = pos;
}
if velo.z < neg {
velo.z = neg;
} else if velo.z > pos {
velo.z = pos;
}
velo.y = velo.y.max(neg);
// TODO
// if velo.y < 0.0
// && self.entity.entity_type == &EntityType::PLAYER
// && self.entity.sneaking.load(Relaxed)
// {
// let block = self
// .entity
// .world
// .read()
// .await
// .get_block(&self.entity.block_pos.load())
// .await;
// if let Some(props) = block.properties(block.default_state.id) {
// if props.name() == "ScaffoldingLikeProperties" {
// velo.y = 0.0;
// }
// }
// }
self.entity.velocity.store(velo);
}
}
pub fn get_swim_height(&self) -> f64 {
let eye_height = self.entity.standing_eye_height;
if self.entity.entity_type == &EntityType::BREEZE {
f64::from(eye_height)
} else if eye_height < 0.4 {
0.0
} else {
0.4
}
}
async fn jump(&self) {
let jump = self.get_jump_velocity(1.0).await;
if jump <= 1.0e-5 {
return;
}
let mut velo = self.entity.velocity.load();
velo.y = jump.max(velo.y);
if self.entity.sprinting.load(Relaxed) {
let yaw = f64::from(self.entity.yaw.load()).to_radians();
velo.x -= yaw.sin() * 0.2;
velo.y += yaw.cos() * 0.2;
}
self.entity.velocity.store(velo);
self.entity.velocity_dirty.store(true, SeqCst);
}
async fn get_jump_velocity(&self, mut strength: f64) -> f64 {
strength *= 1.0; // TODO: Entity Attribute jump strength
strength *= f64::from(self.entity.get_jump_velocity_multiplier().await);
if let Some(effect) = self.get_effect(&StatusEffect::JUMP_BOOST).await {
strength += 0.1 * f64::from(effect.amplifier + 1);
}
strength
}
pub async fn update_fall_distance(
&self,
height_difference: f64,
@ -352,19 +793,6 @@ impl LivingEntity {
}
}
async fn tick_move(&self, entity: &dyn EntityBase, server: &Server) {
let velo = self.entity.velocity.load();
let pos = self.entity.pos.load();
self.entity
.pos
.store(Vector3::new(pos.x + velo.x, pos.y + velo.y, pos.z + velo.z));
let multiplier = f64::from(Entity::velocity_multiplier(pos));
self.entity
.velocity
.store(velo.multiply(multiplier, 1.0, multiplier));
Entity::check_block_collision(entity, server).await;
}
async fn tick_effects(&self) {
let mut effects_to_remove = Vec::new();
@ -497,9 +925,19 @@ impl EntityBase for LivingEntity {
true
}
fn get_gravity(&self) -> f64 {
const GRAVITY: f64 = 0.08;
GRAVITY
}
async fn tick(&self, caller: Arc<dyn EntityBase>, server: &Server) {
self.entity.tick(caller.clone(), server).await;
self.tick_move(caller.as_ref(), server).await;
self.tick_movement(server, caller.clone()).await;
// TODO
if caller.get_player().is_none() {
self.entity.send_pos_rot().await;
self.entity.send_velocity().await;
}
self.tick_effects().await;
if self.hurt_cooldown.load(Relaxed) > 0 {
self.hurt_cooldown.fetch_sub(1, Relaxed);

View File

@ -132,6 +132,10 @@ where
fn get_living_entity(&self) -> Option<&LivingEntity> {
Some(&self.get_mob_entity().living_entity)
}
fn get_gravity(&self) -> f64 {
self.get_mob_entity().living_entity.get_gravity()
}
}
#[allow(dead_code)]

View File

@ -139,6 +139,10 @@ pub trait EntityBase: Send + Sync {
false
}
fn is_flutterer(&self) -> bool {
false
}
async fn damage_with_context(
&self,
amount: f32,
@ -685,7 +689,8 @@ impl Entity {
self.velocity.store(motion);
}
fn _tick_block_underneath(_caller: &Arc<dyn EntityBase>) {
#[allow(dead_code)]
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;
@ -1095,8 +1100,7 @@ impl Entity {
// Entity.updateVelocity in yarn
#[allow(dead_code)]
fn _update_velocity_from_input(&self, movement_input: Vector3<f64>, speed: f64) {
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);
@ -1149,7 +1153,7 @@ impl Entity {
}
#[allow(clippy::float_cmp)]
async fn _get_jump_velocity_multiplier(&self) -> f32 {
async fn get_jump_velocity_multiplier(&self) -> f32 {
let world = self.world.read().await;
let f = world
@ -1644,25 +1648,6 @@ impl Entity {
self.invulnerable.load(Relaxed) || self.damage_immunities.contains(damage_type)
}
fn velocity_multiplier(_pos: Vector3<f64>) -> f32 {
// let world = self.world.read().await;
// TODO: handle when player is outside world
// let block = world.get_block(&self.block_pos.load()).await;
// block.velocity_multiplier
1.0
// if velo_multiplier == 1.0 {
// const VELOCITY_OFFSET: f64 = 0.500001; // Vanilla
// let pos_with_y_offset = BlockPos(Vector3::new(
// pos.x.floor() as i32,
// (pos.y - VELOCITY_OFFSET).floor() as i32,
// pos.z.floor() as i32,
// ));
// let block = world.get_block(&pos_with_y_offset).await.unwrap();
// block.velocity_multiplier
// } else {
// }
}
pub async fn check_block_collision(entity: &dyn EntityBase, server: &Server) {
let aabb = entity.get_entity().bounding_box.load();
let blockpos = BlockPos::new(

View File

@ -604,6 +604,29 @@ impl Player {
self.sleeping_since.store(Some(0));
}
pub async fn get_off_ground_speed(&self) -> f64 {
let sprinting = self.get_entity().sprinting.load(Ordering::Relaxed);
if !self.get_entity().has_vehicle().await {
let fly_speed = {
let abilities = self.abilities.lock().await;
abilities.flying.then_some(f64::from(abilities.fly_speed))
};
if let Some(flying) = fly_speed {
return if sprinting { flying * 2.0 } else { flying };
}
}
if sprinting { 0.025_999_999 } else { 0.02 }
}
pub async fn is_flying(&self) -> bool {
let abilities = self.abilities.lock().await;
abilities.flying
}
pub async fn wake_up(&self) {
let world = self.world().await;
let respawn_point = self

View File

@ -8,7 +8,7 @@ use pumpkin_config::networking::compression::CompressionInfo;
use pumpkin_protocol::java::server::play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse as SPCookieResponse, SInteract, SKeepAlive, SPickItemFromBlock,
SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SKeepAlive, SPickItemFromBlock,
SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded,
SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
@ -668,6 +668,9 @@ impl JavaClient {
self.handle_chat_session_update(player, server, SPlayerSession::read(payload)?)
.await;
}
SCustomPayload::PACKET_ID => {
// TODO: this fixes Failed to handle player packet id for now
}
_ => {
log::warn!("Failed to handle player packet id {}", packet.id);
}