diff --git a/pumpkin/src/block/blocks/redstone/rails/mod.rs b/pumpkin/src/block/blocks/redstone/rails/mod.rs index 606b781c3..6f1c8cb18 100644 --- a/pumpkin/src/block/blocks/redstone/rails/mod.rs +++ b/pumpkin/src/block/blocks/redstone/rails/mod.rs @@ -28,7 +28,7 @@ struct Rail { impl Rail { fn find_with_elevation(world: &World, position: BlockPos) -> Option { let (block, block_state) = world.get_block_and_state_id(&position); - if block.has_tag(&tag::Item::MINECRAFT_RAILS) { + if block.has_tag(&tag::Block::MINECRAFT_RAILS) { let properties = RailProperties::new(block_state, block); return Some(Self { block, @@ -40,7 +40,7 @@ impl Rail { let pos = position.up(); let (block, block_state) = world.get_block_and_state_id(&pos); - if block.has_tag(&tag::Item::MINECRAFT_RAILS) { + if block.has_tag(&tag::Block::MINECRAFT_RAILS) { let properties = RailProperties::new(block_state, block); return Some(Self { block, @@ -52,7 +52,7 @@ impl Rail { let pos = position.down(); let (block, block_state) = world.get_block_and_state_id(&pos); - if block.has_tag(&tag::Item::MINECRAFT_RAILS) { + if block.has_tag(&tag::Block::MINECRAFT_RAILS) { let properties = RailProperties::new(block_state, block); return Some(Self { block, diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index ddfc8250c..7422a738e 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -2574,6 +2574,10 @@ impl EntityBase for LivingEntity { Some(self) } + fn is_pushable(&self) -> bool { + self.health.load() > 0.0 && !self.dead.load(Relaxed) + } + fn cast_any(&self) -> &dyn std::any::Any { self } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 7d97eede2..8644c0bb0 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -312,6 +312,233 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { Box::pin(async {}) } + fn is_passenger(&self) -> EntityBaseFuture<'_, bool> { + Box::pin(async move { self.get_entity().has_vehicle().await }) + } + + fn is_vehicle(&self) -> EntityBaseFuture<'_, bool> { + Box::pin(async move { self.get_entity().has_passengers().await }) + } + + fn has_passenger<'a>(&'a self, other: &'a Arc) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + self.get_entity() + .passengers + .lock() + .await + .iter() + .any(|p| p.get_entity().entity_id == other.get_entity().entity_id) + }) + } + + fn move_entity<'a>( + &'a self, + caller: &'a Arc, + motion: Vector3, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.get_entity().move_entity(caller, motion).await; + }) + } + + fn is_pushable(&self) -> bool { + false + } + + fn push<'a>(&'a self, entity: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let self_entity = self.get_entity(); + let other_entity = entity.get_entity(); + + if self_entity.no_clip.load(Ordering::Relaxed) + || other_entity.no_clip.load(Ordering::Relaxed) + { + return; + } + + { + let passengers = self_entity.passengers.lock().await; + if passengers + .iter() + .any(|p| p.get_entity().entity_id == other_entity.entity_id) + { + return; + } + } + { + let passengers = other_entity.passengers.lock().await; + if passengers + .iter() + .any(|p| p.get_entity().entity_id == self_entity.entity_id) + { + return; + } + } + + let mut dx = other_entity.pos.load().x - self_entity.pos.load().x; + let mut dz = other_entity.pos.load().z - self_entity.pos.load().z; + let mut d = dx.abs().max(dz.abs()); + if d >= 0.01 { + d = d.sqrt(); + dx /= d; + dz /= d; + let mut d2 = 1.0 / d; + if d2 > 1.0 { + d2 = 1.0; + } + dx *= d2; + dz *= d2; + dx *= 0.05; + dz *= 0.05; + + if !self_entity.has_passengers().await && self.is_pushable() { + let mut vel = self_entity.velocity.load(); + vel.x -= dx; + vel.z -= dz; + self_entity.velocity.store(vel); + self_entity.send_velocity(); + } + + if !other_entity.has_passengers().await && entity.is_pushable() { + let mut vel = other_entity.velocity.load(); + vel.x += dx; + vel.z += dz; + other_entity.velocity.store(vel); + other_entity.send_velocity(); + } + } + }) + } + + #[allow(clippy::too_many_lines)] + fn push_entities<'a>( + &'a self, + dyn_self: &'a Arc, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let mut picked_up = false; + let mut pushed = false; + let self_entity = self.get_entity(); + let entity_bb = self_entity.bounding_box.load(); + + if !self.is_pushable() { + return false; + } + + let world = self_entity.world.load(); + + let is_rideable_minecart = self_entity.entity_type.id == EntityType::MINECART.id; + let is_abstract_minecart = is_rideable_minecart + || self_entity.entity_type.id == EntityType::CHEST_MINECART.id + || self_entity.entity_type.id == EntityType::COMMAND_BLOCK_MINECART.id + || self_entity.entity_type.id == EntityType::FURNACE_MINECART.id + || self_entity.entity_type.id == EntityType::HOPPER_MINECART.id + || self_entity.entity_type.id == EntityType::SPAWNER_MINECART.id + || self_entity.entity_type.id == EntityType::TNT_MINECART.id; + + let is_minecart_fn = |id| -> bool { + id == EntityType::MINECART.id + || id == EntityType::CHEST_MINECART.id + || id == EntityType::COMMAND_BLOCK_MINECART.id + || id == EntityType::FURNACE_MINECART.id + || id == EntityType::HOPPER_MINECART.id + || id == EntityType::SPAWNER_MINECART.id + || id == EntityType::TNT_MINECART.id + }; + + if is_abstract_minecart { + let is_vehicle = self.is_vehicle().await; + + if is_rideable_minecart && !is_vehicle { + let pickup_bb = entity_bb.expand(0.2, 0.0, 0.2); + let other_entities = world.get_entities_at_box(&pickup_bb); + + for other in other_entities { + if other.get_entity().entity_id != self_entity.entity_id { + let other_type = other.get_entity().entity_type.id; + let is_iron_golem = other_type == EntityType::IRON_GOLEM.id; + let is_other_minecart = is_minecart_fn(other_type); + + if !is_iron_golem + && !is_other_minecart + && !other.is_passenger().await + && other.is_pushable() + && other.get_entity().riding_cooldown.load(Relaxed) == 0 + { + dyn_self + .get_entity() + .add_passenger(dyn_self.clone(), other.clone()) + .await; + picked_up = true; + break; + } + } + } + } + + let push_bb = entity_bb.expand(1.0e-7, 1.0e-7, 1.0e-7); + + let other_entities = world.get_entities_at_box(&push_bb); + for other in other_entities { + if other.get_entity().entity_id != self_entity.entity_id { + let other_type = other.get_entity().entity_type.id; + let is_other_minecart = is_minecart_fn(other_type); + let is_iron_golem = other_type == EntityType::IRON_GOLEM.id; + + if is_rideable_minecart { + if (is_iron_golem + || is_other_minecart + || is_vehicle + || !other.get_entity().has_vehicle().await) + && other.is_pushable() + { + dyn_self.push(&other).await; + pushed = true; + } + } else if !self.has_passenger(&other).await + && other.is_pushable() + && is_other_minecart + { + dyn_self.push(&other).await; + pushed = true; + } + } + } + + let players = world.get_players_at_box(&push_bb); + for player in players { + if player.get_entity().entity_id != self_entity.entity_id + && is_rideable_minecart + { + let player_base: Arc = player.clone(); + dyn_self.push(&player_base).await; + pushed = true; + // Non-rideable minecarts (hoppers, chests) do not push players in vanilla. + } + } + } else { + let other_entities = world.get_entities_at_box(&entity_bb); + for other in other_entities { + if other.get_entity().entity_id != self_entity.entity_id { + dyn_self.push(&other).await; + pushed = true; + } + } + + let players = world.get_players_at_box(&entity_bb); + for player in players { + if player.get_entity().entity_id != self_entity.entity_id { + let player_base: Arc = player.clone(); + dyn_self.push(&player_base).await; + pushed = true; + } + } + } + + picked_up && !pushed + }) + } + fn on_hit(&self, _hit: crate::entity::projectile::ProjectileHit) -> EntityBaseFuture<'_, ()> { Box::pin(async {}) } @@ -1766,7 +1993,11 @@ impl Entity { // Move by a delta, adjust for collisions, and send // Does not send movement. That must be done separately - async fn move_entity<'a>(&'a self, caller: &'a Arc, mut motion: Vector3) { + pub async fn move_entity<'a>( + &'a self, + caller: &'a Arc, + mut motion: Vector3, + ) { if caller.get_player().is_some() { return; } @@ -2764,9 +2995,6 @@ impl Entity { if let Some(passenger) = removed_passenger { let vehicle_box = self.bounding_box.load(); let passenger_entity = passenger.get_entity(); - let passenger_yaw = passenger_entity.yaw.load(); - let passenger_width = passenger_entity.entity_dimension.load().width as f64; - let vehicle_width = self.entity_dimension.load().width as f64; // Pre-allocate teleport ID and block movement packets BEFORE sending // CSetPassengers. This prevents a race condition where the client receives @@ -2806,28 +3034,75 @@ impl Entity { world.broadcast_to_chunk(chunk_pos, &passengers_packet); } - // Calculate dismount offset (vanilla getPassengerDismountOffset) - let offset_dist = - (vehicle_width * std::f64::consts::SQRT_2 + passenger_width + 0.00001) / 2.0; - let yaw_rad = (-passenger_yaw).to_radians(); - let sin_yaw = f64::from(yaw_rad.sin()); - let cos_yaw = f64::from(yaw_rad.cos()); - let max_component = sin_yaw.abs().max(cos_yaw.abs()); - let offset_x = sin_yaw * offset_dist / max_component; - let offset_z = cos_yaw * offset_dist / max_component; + // Calculate dismount directions and offsets (vanilla DismountHelper) + let vehicle_yaw = self.yaw.load(); + // Wrap yaw to 0..360 range + let wrapped_yaw = (vehicle_yaw % 360.0 + 360.0) % 360.0; + let forward_dir = if !(45.0..315.0).contains(&wrapped_yaw) { + BlockDirection::South + } else if (45.0..135.0).contains(&wrapped_yaw) { + BlockDirection::West + } else if (135.0..225.0).contains(&wrapped_yaw) { + BlockDirection::North + } else { + BlockDirection::East + }; + + let get_step = |dir: BlockDirection| -> (i32, i32) { + match dir { + BlockDirection::North => (0, -1), + BlockDirection::South => (0, 1), + BlockDirection::East => (1, 0), + BlockDirection::West => (-1, 0), + _ => (0, 0), + } + }; + + let get_clockwise = |dir: BlockDirection| -> BlockDirection { + match dir { + BlockDirection::North => BlockDirection::East, + BlockDirection::East => BlockDirection::South, + BlockDirection::South => BlockDirection::West, + BlockDirection::West => BlockDirection::North, + other => other, + } + }; + + let get_opposite = |dir: BlockDirection| -> BlockDirection { + match dir { + BlockDirection::North => BlockDirection::South, + BlockDirection::South => BlockDirection::North, + BlockDirection::East => BlockDirection::West, + BlockDirection::West => BlockDirection::East, + other => other, + } + }; + + let right_dir = get_clockwise(forward_dir); + let left_dir = get_opposite(right_dir); + let back_dir = get_opposite(forward_dir); + + let (fx, fz) = get_step(forward_dir); + let (rx, rz) = get_step(right_dir); + let (lx, lz) = get_step(left_dir); + let (bx, bz) = get_step(back_dir); + + let offsets = [ + (rx, rz), + (lx, lz), + (bx + rx, bz + rz), + (bx + lx, bz + lz), + (fx + rx, fz + rz), + (fx + lx, fz + lz), + (bx, bz), + (fx, fz), + ]; - let target_x = self.pos.load().x + offset_x; - let target_z = self.pos.load().z + offset_z; let target_block_y = vehicle_box.max.y.floor() as i32; - let block_pos = BlockPos(Vector3::new( - target_x.floor() as i32, - target_block_y, - target_z.floor() as i32, - )); let below_pos = BlockPos(Vector3::new( - target_x.floor() as i32, + self.pos.load().x.floor() as i32, target_block_y - 1, - target_z.floor() as i32, + self.pos.load().z.floor() as i32, )); let below_state_id = world.get_block_state_id(&below_pos); @@ -2841,35 +3116,47 @@ impl Entity { let dismount_pos = if is_water { fallback_pos } else { - // Vanilla tries two Y levels: at vehicle top and one block below - let mut candidates = Vec::new(); - for pos in [&block_pos, &below_pos] { - let height = world.get_dismount_height(pos); - // Vanilla: canDismountInBlock = !height.is_infinite() && height < 1.0 - if height.is_finite() && height < 1.0 { - candidates.push(Vector3::new( - target_x, - f64::from(pos.0.y) + height, - target_z, - )); - } - } - - // Try poses: Standing, Crouching, Swimming (vanilla order) - let poses = [ - EntityPose::Standing, - EntityPose::Crouching, - EntityPose::Swimming, + // Vanilla checks Standing, Crouching, Swimming poses and their respective height checks + let poses_and_heights = [ + (EntityPose::Standing, vec![0, 1, -1]), + (EntityPose::Crouching, vec![0, 1, -1]), + (EntityPose::Swimming, vec![0, 1]), ]; + + let vehicle_block_pos = self.block_pos.load(); let mut found = None; - 'outer: for pose in poses { + + 'search: for (pose, y_offsets) in poses_and_heights { let dims = Self::get_entity_dimensions(pose); - for candidate in &candidates { - let bbox = - BoundingBox::new_from_pos(candidate.x, candidate.y, candidate.z, &dims); - if world.is_space_empty(bbox) { - found = Some((*candidate, pose)); - break 'outer; + + for y_offset in y_offsets { + for &(ox, oz) in &offsets { + let target_block_x = vehicle_block_pos.0.x + ox; + let target_block_y = vehicle_block_pos.0.y + y_offset; + let target_block_z = vehicle_block_pos.0.z + oz; + + let target_pos = BlockPos(Vector3::new( + target_block_x, + target_block_y, + target_block_z, + )); + let height = world.get_dismount_height(&target_pos); + + if height.is_finite() && height < 1.0 { + let location = Vector3::new( + f64::from(target_block_x) + 0.5, + f64::from(target_block_y) + height, + f64::from(target_block_z) + 0.5, + ); + + let bbox = BoundingBox::new_from_pos( + location.x, location.y, location.z, &dims, + ); + if world.is_space_empty(bbox) { + found = Some((location, pose)); + break 'search; + } + } } } } @@ -2880,7 +3167,41 @@ impl Entity { } pos } else { - fallback_pos + // Try dismounting directly on top of the vehicle as fallback + let mut found_fallback = None; + let vehicle_top = vehicle_box.max.y; + + let poses = [ + EntityPose::Standing, + EntityPose::Crouching, + EntityPose::Swimming, + ]; + + for pose in poses { + let dims = Self::get_entity_dimensions(pose); + let bbox = BoundingBox::new_from_pos( + self.pos.load().x, + vehicle_top, + self.pos.load().z, + &dims, + ); + if world.is_space_empty(bbox) { + found_fallback = Some(( + Vector3::new(self.pos.load().x, vehicle_top, self.pos.load().z), + pose, + )); + break; + } + } + + if let Some((pos, pose)) = found_fallback { + if pose != EntityPose::Standing { + passenger_entity.set_pose(pose); + } + pos + } else { + fallback_pos + } } }; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 20604c5dd..3cee015a9 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -7,7 +7,7 @@ use std::f64::consts::TAU; use std::mem; use std::num::NonZeroU8; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI8, AtomicI32, AtomicU8, AtomicU32, Ordering}; use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; @@ -452,6 +452,7 @@ pub struct Player { pub tick_counter: AtomicI32, pub packet_sequence: AtomicI32, pub mining_pos: Mutex, + pub last_input: AtomicI8, /// A counter for teleport IDs used to track pending teleports. pub teleport_id_count: AtomicI32, /// The pending teleport information, including the teleport ID and target location. @@ -645,6 +646,7 @@ impl Player { tick_counter: AtomicI32::new(0), packet_sequence: AtomicI32::new(-1), start_mining_time: AtomicI32::new(0), + last_input: AtomicI8::new(0), carried_item: Mutex::new(None), experience_pick_up_delay: Mutex::new(0), teleport_id_count: AtomicI32::new(0), @@ -4471,6 +4473,10 @@ impl EntityBase for Player { self.gamemode.load() == GameMode::Spectator } + fn is_pushable(&self) -> bool { + self.gamemode.load() != GameMode::Spectator && self.gamemode.load() != GameMode::Creative + } + fn get_name(&self) -> TextComponent { //TODO: team color TextComponent::text(self.gameprofile.name.clone()) diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 3d57dcf32..9a00f4ead 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -406,16 +406,15 @@ impl EntityBase for ArrowEntity { if target.get_living_entity().is_some() { let punch = self.punch_level.load(Ordering::Relaxed); - if punch > 0 { - if let Some(owner_id) = self.owner_id { - if let Some(owner_entity) = world.get_entity_by_id(owner_id) { - crate::entity::combat::handle_knockback( - owner_entity.get_entity(), - target.get_entity(), - f64::from(punch) * 0.6, - ); - } - } + if punch > 0 + && let Some(owner_id) = self.owner_id + && let Some(owner_entity) = world.get_entity_by_id(owner_id) + { + crate::entity::combat::handle_knockback( + owner_entity.get_entity(), + target.get_entity(), + f64::from(punch) * 0.6, + ); } // Play hit sound diff --git a/pumpkin/src/entity/projectile/trident.rs b/pumpkin/src/entity/projectile/trident.rs index 78ca46219..32271395c 100644 --- a/pumpkin/src/entity/projectile/trident.rs +++ b/pumpkin/src/entity/projectile/trident.rs @@ -284,10 +284,10 @@ impl EntityBase for TridentEntity { } // Handle hit - if let Some(h) = hit { - if !self.has_hit.swap(true, Ordering::SeqCst) { - caller.on_hit(h).await; - } + if let Some(h) = hit + && !self.has_hit.swap(true, Ordering::SeqCst) + { + caller.on_hit(h).await; } }) } diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 2fef6c4f5..d2cd294ba 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -109,6 +109,7 @@ use crate::entity::projectile::trident::TridentEntity; use crate::entity::projectile::wind_charge::{WIND_CHARGE_GRAVITY, WindChargeEntity}; use crate::entity::tnt::TNTEntity; use crate::entity::vehicle::boat::BoatEntity; +use crate::entity::vehicle::minecart::MinecartEntity; use crate::entity::{Entity, EntityBase, mob}; use crate::world::World; use pumpkin_data::Block; @@ -240,6 +241,16 @@ pub fn from_type( id if id == EntityType::ARROW.id => Arc::new(ArrowEntity::new(entity, None)), id if id == EntityType::SPECTRAL_ARROW.id => Arc::new(ArrowEntity::new(entity, None)), id if id == EntityType::TRIDENT.id => Arc::new(TridentEntity::new(entity, None)), + id if id == EntityType::MINECART.id + || id == EntityType::CHEST_MINECART.id + || id == EntityType::FURNACE_MINECART.id + || id == EntityType::TNT_MINECART.id + || id == EntityType::HOPPER_MINECART.id + || id == EntityType::COMMAND_BLOCK_MINECART.id + || id == EntityType::SPAWNER_MINECART.id => + { + Arc::new(MinecartEntity::new(entity)) + } id if id == EntityType::FIREBALL.id => Arc::new(FireballEntity::new(entity)), id if id == EntityType::SMALL_FIREBALL.id => Arc::new(SmallFireballEntity::new(entity)), id if id == EntityType::WIND_CHARGE.id => { diff --git a/pumpkin/src/entity/vehicle/boat.rs b/pumpkin/src/entity/vehicle/boat.rs index b182fe1c5..c28e6dc40 100644 --- a/pumpkin/src/entity/vehicle/boat.rs +++ b/pumpkin/src/entity/vehicle/boat.rs @@ -1,26 +1,25 @@ use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use crossbeam::atomic::AtomicCell; use crate::entity::player::Player; use crate::entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity}; use crate::server::Server; -use crate::world::loot::{LootContextParameters, LootTableExt}; + use pumpkin_data::damage::DamageType; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tracked_data::TrackedData; -use pumpkin_protocol::codec::var_int::VarInt; + use pumpkin_protocol::java::client::play::Metadata; -use pumpkin_util::GameMode; + use pumpkin_util::math::vector3::Vector3; +use crate::entity::vehicle::vehicle::VehicleEntity; + pub struct BoatEntity { - entity: Entity, - damage_wobble_ticks: AtomicI32, - damage_wobble_side: AtomicI32, - damage_wobble_strength: AtomicCell, + pub vehicle: VehicleEntity, ticks_underwater: AtomicCell, left_paddle_moving: AtomicBool, right_paddle_moving: AtomicBool, @@ -29,10 +28,7 @@ pub struct BoatEntity { impl BoatEntity { pub const fn new(entity: Entity) -> Self { Self { - entity, - damage_wobble_ticks: AtomicI32::new(0), - damage_wobble_side: AtomicI32::new(1), - damage_wobble_strength: AtomicCell::new(0.0), + vehicle: VehicleEntity::new(entity), ticks_underwater: AtomicCell::new(0.0), left_paddle_moving: AtomicBool::new(false), right_paddle_moving: AtomicBool::new(false), @@ -43,45 +39,14 @@ impl BoatEntity { self.left_paddle_moving.store(left, Ordering::Relaxed); self.right_paddle_moving.store(right, Ordering::Relaxed); - self.entity.send_meta_data(&[ + self.vehicle.entity.send_meta_data(&[ Metadata::new(TrackedData::ID_PADDLE_LEFT, MetaDataType::BOOLEAN, left), Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right), ]); } fn send_wobble_metadata(&self) { - self.entity.send_meta_data(&[ - Metadata::new( - TrackedData::ID_HURT, - MetaDataType::INTEGER, - VarInt(self.damage_wobble_ticks.load(Ordering::Relaxed)), - ), - Metadata::new( - TrackedData::ID_HURTDIR, - MetaDataType::INTEGER, - VarInt(self.damage_wobble_side.load(Ordering::Relaxed)), - ), - ]); - self.entity.send_meta_data(&[Metadata::new( - TrackedData::ID_DAMAGE, - MetaDataType::FLOAT, - self.damage_wobble_strength.load(), - )]); - } - - async fn kill_and_drop_self(&self) { - let world = self.entity.world.load(); - let entity_drops = world.level_info.load().game_rules.entity_drops; - - if entity_drops && let Some(loot_table) = &self.entity.entity_type.loot_table { - let pos = self.entity.block_pos.load(); - let params = LootContextParameters::default(); - for stack in loot_table.get_loot(params) { - world.drop_stack(&pos, stack).await; - } - } - - self.entity.remove().await; + self.vehicle.send_wobble_metadata(); } } @@ -89,7 +54,7 @@ impl NBTStorage for BoatEntity {} impl EntityBase for BoatEntity { fn get_entity(&self) -> &Entity { - &self.entity + &self.vehicle.entity } fn get_living_entity(&self) -> Option<&LivingEntity> { @@ -102,18 +67,10 @@ impl EntityBase for BoatEntity { _server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { - let ticks = self.damage_wobble_ticks.load(Ordering::Relaxed); - if ticks > 0 { - self.damage_wobble_ticks.store(ticks - 1, Ordering::Relaxed); - } - - let strength = self.damage_wobble_strength.load(); - if strength > 0.0 { - self.damage_wobble_strength.store(strength - 1.0); - } + self.vehicle.tick(); let underwater = self.ticks_underwater.load(); - if self.entity.touching_water.load(Ordering::Relaxed) { + if self.vehicle.entity.touching_water.load(Ordering::Relaxed) { self.ticks_underwater.store((underwater + 1.0).min(60.0)); } else if underwater > 0.0 { self.ticks_underwater.store((underwater - 1.0).max(0.0)); @@ -128,7 +85,7 @@ impl EntityBase for BoatEntity { } fn can_hit(&self) -> bool { - self.entity.is_alive() + self.vehicle.entity.is_alive() } fn is_collidable(&self, _entity: Option>) -> bool { @@ -144,37 +101,7 @@ impl EntityBase for BoatEntity { source: Option<&'a dyn EntityBase>, _cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { - Box::pin(async move { - if !self.entity.is_alive() { - return true; - } - - let current_side = self.damage_wobble_side.load(Ordering::Relaxed); - self.damage_wobble_side - .store(-current_side, Ordering::Relaxed); - self.damage_wobble_ticks.store(10, Ordering::Relaxed); - self.entity.velocity_dirty.store(true, Ordering::SeqCst); - - let current_strength = self.damage_wobble_strength.load(); - let new_strength = current_strength + amount * 10.0; - self.damage_wobble_strength.store(new_strength); - - self.send_wobble_metadata(); - - let is_creative = source - .and_then(|s| s.get_player()) - .is_some_and(|p| p.gamemode.load() == GameMode::Creative); - - if is_creative || new_strength > 40.0 { - if is_creative { - self.entity.remove().await; - } else { - self.kill_and_drop_self().await; - } - } - - true - }) + Box::pin(async move { self.vehicle.damage_with_context(amount, source).await }) } fn interact<'a>( @@ -191,7 +118,7 @@ impl EntityBase for BoatEntity { return false; } - if self.entity.passengers.lock().await.len() >= 2 { + if self.vehicle.entity.passengers.lock().await.len() >= 2 { return false; } @@ -199,8 +126,8 @@ impl EntityBase for BoatEntity { return false; } - let world = self.entity.world.load(); - let Some(vehicle) = world.get_entity_by_id(self.entity.entity_id) else { + let world = self.vehicle.entity.world.load(); + let Some(vehicle) = world.get_entity_by_id(self.vehicle.entity.entity_id) else { return false; }; @@ -208,7 +135,8 @@ impl EntityBase for BoatEntity { return false; }; - self.entity + self.vehicle + .entity .add_passenger(vehicle, passenger as Arc) .await; @@ -229,4 +157,8 @@ impl EntityBase for BoatEntity { fn cast_any(&self) -> &dyn std::any::Any { self } + + fn is_pushable(&self) -> bool { + true + } } diff --git a/pumpkin/src/entity/vehicle/minecart.rs b/pumpkin/src/entity/vehicle/minecart.rs new file mode 100644 index 000000000..946413ad2 --- /dev/null +++ b/pumpkin/src/entity/vehicle/minecart.rs @@ -0,0 +1,679 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +use pumpkin_protocol::java::server::play::SPlayerInput; + +use crate::{ + entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, + }, + server::Server, +}; +use pumpkin_data::Block; +use pumpkin_data::block_properties::{BlockProperties, PoweredRailLikeProperties}; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +use crate::entity::vehicle::vehicle::VehicleEntity; + +const fn get_exits( + shape: pumpkin_data::block_properties::RailShape, +) -> (Vector3, Vector3) { + use pumpkin_data::block_properties::RailShape; + match shape { + RailShape::NorthSouth => (Vector3::new(0.0, 0.0, -1.0), Vector3::new(0.0, 0.0, 1.0)), + RailShape::EastWest => (Vector3::new(-1.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0)), + RailShape::AscendingEast => (Vector3::new(-1.0, 0.0, 0.0), Vector3::new(1.0, 1.0, 0.0)), + RailShape::AscendingWest => (Vector3::new(1.0, 0.0, 0.0), Vector3::new(-1.0, 1.0, 0.0)), + RailShape::AscendingNorth => (Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.0, 1.0, -1.0)), + RailShape::AscendingSouth => (Vector3::new(0.0, 0.0, -1.0), Vector3::new(0.0, 1.0, 1.0)), + RailShape::SouthEast => (Vector3::new(0.0, 0.0, 1.0), Vector3::new(1.0, 0.0, 0.0)), + RailShape::SouthWest => (Vector3::new(0.0, 0.0, 1.0), Vector3::new(-1.0, 0.0, 0.0)), + RailShape::NorthWest => (Vector3::new(0.0, 0.0, -1.0), Vector3::new(-1.0, 0.0, 0.0)), + RailShape::NorthEast => (Vector3::new(0.0, 0.0, -1.0), Vector3::new(1.0, 0.0, 0.0)), + } +} + +pub struct MinecartEntity { + pub vehicle: VehicleEntity, + pub active: AtomicBool, + pub tnt_fuse: AtomicI32, +} + +impl MinecartEntity { + pub const fn new(entity: Entity) -> Self { + Self { + vehicle: VehicleEntity::new(entity), + active: AtomicBool::new(false), + tnt_fuse: AtomicI32::new(-1), + } + } +} + +impl NBTStorage for MinecartEntity {} + +impl EntityBase for MinecartEntity { + #[allow(clippy::too_many_lines)] + fn tick<'a>( + &'a self, + caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.vehicle.tick(); + + let world = self.vehicle.entity.world.load(); + let pos = self.vehicle.entity.pos.load(); + let mut block_pos = BlockPos(Vector3::new( + pos.x.floor() as i32, + pos.y.floor() as i32, + pos.z.floor() as i32, + )); + + let mut block = world.get_block(&block_pos); + let mut state_id = world.get_block_state_id(&block_pos); + + let mut is_powered_rail = block.id == Block::POWERED_RAIL.id; + let mut is_activator_rail = block.id == Block::ACTIVATOR_RAIL.id; + let mut is_on_rails = is_powered_rail + || is_activator_rail + || block.id == Block::RAIL.id + || block.id == Block::DETECTOR_RAIL.id; + + // If not on rails at current Y level, check the block directly below + if !is_on_rails { + let below_block_pos = BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y - 1, + block_pos.0.z, + )); + let below_block = world.get_block(&below_block_pos); + if below_block.id == Block::RAIL.id + || below_block.id == Block::POWERED_RAIL.id + || below_block.id == Block::DETECTOR_RAIL.id + || below_block.id == Block::ACTIVATOR_RAIL.id + { + block_pos = below_block_pos; + block = below_block; + state_id = world.get_block_state_id(&block_pos); + is_powered_rail = block.id == Block::POWERED_RAIL.id; + is_activator_rail = block.id == Block::ACTIVATOR_RAIL.id; + is_on_rails = true; + } + } + + if is_powered_rail || is_activator_rail { + let props = PoweredRailLikeProperties::from_state_id(state_id, block); + let powered = props.powered; + + self.active.store(powered, Ordering::Relaxed); + + if powered { + if is_powered_rail { + let mut velocity = self.vehicle.entity.velocity.load(); + let speed = velocity.length(); + if speed > 0.01 { + let new_speed = (speed + 0.06).min(0.4); + velocity = velocity + .normalize() + .multiply(new_speed, new_speed, new_speed); + self.vehicle.entity.velocity.store(velocity); + } else { + let yaw = self.vehicle.entity.yaw.load(); + let push_dir = Vector3::new( + -f64::from((yaw.to_radians()).sin()), + 0.0, + f64::from((yaw.to_radians()).cos()), + ); + self.vehicle + .entity + .velocity + .store(push_dir.multiply(0.1, 0.1, 0.1)); + } + self.vehicle.entity.send_velocity(); + } else if is_activator_rail { + let passengers = self.vehicle.entity.passengers.lock().await.clone(); + for passenger in passengers { + let passenger_id = passenger.get_entity().entity_id; + self.vehicle.entity.remove_passenger(passenger_id).await; + } + + if self.vehicle.entity.entity_type.id == EntityType::TNT_MINECART.id { + let fuse = self.tnt_fuse.load(Ordering::Relaxed); + if fuse == -1 { + self.tnt_fuse.store(80, Ordering::Relaxed); + world.play_sound( + pumpkin_data::sound::Sound::EntityTntPrimed, + pumpkin_data::sound::SoundCategory::Blocks, + &pos, + ); + } + } else { + if self.vehicle.get_hurt_time() == 0 { + self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); + self.vehicle.set_hurt_time(10); + self.vehicle.set_damage(50.0); + // TODO: Send entity status + } + } + } + } else if is_powered_rail { + let mut velocity = self.vehicle.entity.velocity.load(); + velocity = velocity.multiply(0.5, 0.5, 0.5); + if velocity.length() < 0.01 { + velocity = Vector3::new(0.0, 0.0, 0.0); + } + self.vehicle.entity.velocity.store(velocity); + self.vehicle.entity.send_velocity(); + } + } + + if self.vehicle.entity.entity_type.id == EntityType::TNT_MINECART.id { + let fuse = self.tnt_fuse.load(Ordering::Relaxed); + if fuse > 0 { + let new_fuse = fuse - 1; + self.tnt_fuse.store(new_fuse, Ordering::Relaxed); + + if new_fuse % 2 == 0 { + world.spawn_particle( + pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + pumpkin_data::particle::Particle::Smoke, + ); + } + + if new_fuse == 0 { + self.vehicle.entity.remove().await; + world.explode(pos, 4.0).await; + } + } + } + + let mut velocity = self.vehicle.entity.velocity.load(); + + let mut has_driver = false; + let mut driver_input = 0; + let mut driver_yaw = 0.0f32; + + { + let passengers = self.vehicle.entity.passengers.lock().await; + if let Some(passenger) = passengers.first() + && let Some(player) = passenger.get_player() + { + driver_input = player.last_input.load(Ordering::Relaxed); + driver_yaw = player.get_entity().yaw.load(); + has_driver = true; + } + } + + if has_driver && is_on_rails { + let forward = driver_input & SPlayerInput::FORWARD != 0; + let backward = driver_input & SPlayerInput::BACKWARD != 0; + + let mut force_dir = Vector3::new(0.0, 0.0, 0.0); + if forward { + let yaw_rad = f64::from(driver_yaw).to_radians(); + force_dir.x = -yaw_rad.sin(); + force_dir.z = yaw_rad.cos(); + } else if backward { + let yaw_rad = f64::from(driver_yaw).to_radians(); + force_dir.x = yaw_rad.sin(); + force_dir.z = -yaw_rad.cos(); + } + + if forward || backward { + velocity.x += force_dir.x * 0.02; + velocity.z += force_dir.z * 0.02; + + let speed = velocity.x.hypot(velocity.z); + if speed > 0.15 { + #[allow(clippy::suboptimal_flops)] + let old_speed = self + .vehicle + .entity + .velocity + .load() + .x + .hypot(self.vehicle.entity.velocity.load().z); + + let max_speed = old_speed.clamp(0.15, 0.4); + if speed > max_speed { + velocity.x = (velocity.x / speed) * max_speed; + velocity.z = (velocity.z / speed) * max_speed; + } + } + self.vehicle.entity.velocity.store(velocity); + self.vehicle.entity.send_velocity(); + } + } + + let mut velocity = self.vehicle.entity.velocity.load(); + + if is_on_rails { + use pumpkin_data::block_properties::RailLikeProperties; + use pumpkin_data::block_properties::{RailShape, RailShapeStraight}; + + let shape = if block.id == Block::RAIL.id { + let props = RailLikeProperties::from_state_id(state_id, block); + props.shape + } else { + let props = PoweredRailLikeProperties::from_state_id(state_id, block); + match props.shape { + RailShapeStraight::NorthSouth => RailShape::NorthSouth, + RailShapeStraight::EastWest => RailShape::EastWest, + RailShapeStraight::AscendingEast => RailShape::AscendingEast, + RailShapeStraight::AscendingWest => RailShape::AscendingWest, + RailShapeStraight::AscendingNorth => RailShape::AscendingNorth, + RailShapeStraight::AscendingSouth => RailShape::AscendingSouth, + } + }; + + let pos = self.vehicle.entity.pos.load(); + let block_center_bottom = Vector3::new( + f64::from(block_pos.0.x) + 0.5, + f64::from(block_pos.0.y), + f64::from(block_pos.0.z) + 0.5, + ); + + let (exit0, exit1) = get_exits(shape); + let exit0 = exit0.multiply(0.5, 0.5, 0.5); + let exit1 = exit1.multiply(0.5, 0.5, 0.5); + + let in_corner = exit0.x != exit1.x && exit0.z != exit1.z; + let mut target_position = pos; + + if in_corner { + let from0to1 = exit1 - exit0; + let from0topos = pos - block_center_bottom - exit0; + let dot_num = from0to1.dot(&from0topos); + let dot_den = from0to1.dot(&from0to1); + if dot_den != 0.0 { + let travel_vector_from0 = from0to1.multiply( + dot_num / dot_den, + dot_num / dot_den, + dot_num / dot_den, + ); + target_position = block_center_bottom.add(&exit0).add(&travel_vector_from0); + } + } else { + let z_snap = (exit0.x - exit1.x).abs() > 1e-5; + let x_snap = (exit0.z - exit1.z).abs() > 1e-5; + if x_snap { + target_position.x = block_center_bottom.x; + } + if z_snap { + target_position.z = block_center_bottom.z; + } + } + + target_position.y = pos.y; + self.vehicle.entity.pos.store(target_position); + + let horizontal_in_direction = Vector3::new(exit1.x, 0.0, exit1.z); + let mut horizontal_out_direction = Vector3::new(exit0.x, 0.0, exit0.z); + + if velocity.dot(&horizontal_out_direction) < velocity.dot(&horizontal_in_direction) + { + horizontal_out_direction = horizontal_in_direction; + } + + let out_position = block_center_bottom.add(&horizontal_out_direction).add( + &horizontal_out_direction + .normalize() + .multiply(1e-5, 1e-5, 1e-5), + ); + + let mut towards_out = out_position - target_position; + towards_out.y = 0.0; + let towards_length = towards_out.length(); + if towards_length > 1e-5 { + towards_out = towards_out.normalize(); + let speed = velocity.length(); + velocity = towards_out.multiply(speed, speed, speed); + } + + self.vehicle.entity.velocity.store(velocity); + } + + if velocity.length() > 0.001 { + self.move_entity(caller, velocity).await; + let new_pos = self.vehicle.entity.pos.load(); + + let passengers = self.vehicle.entity.passengers.lock().await; + for passenger in passengers.iter() { + passenger.get_entity().set_pos(new_pos); + } + drop(passengers); + + self.vehicle.entity.send_pos_rot(); + + #[allow(clippy::useless_let_if_seq)] + let mut friction = 0.98; // Default off-rail air drag + + if is_on_rails { + let passengers = self.vehicle.entity.passengers.lock().await; + let has_passengers = !passengers.is_empty(); + drop(passengers); + friction = if has_passengers { 0.99 } else { 0.96 }; + } else { + let below_block_pos = BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y - 1, + block_pos.0.z, + )); + let below_block = world.get_block(&below_block_pos); + + let is_on_ground = self.vehicle.entity.on_ground.load(Ordering::Relaxed) + || (below_block.id != Block::AIR.id + && below_block.id != Block::WATER.id + && below_block.id != Block::LAVA.id); + let is_in_water = self.vehicle.entity.touching_water.load(Ordering::Relaxed) + || below_block.id == Block::WATER.id; + + if is_on_ground { + friction = 0.5; + } else if is_in_water { + friction = 0.95; + } + } + + let mut next_vel = velocity.multiply(friction, friction, friction); + if next_vel.length() < 0.005 { + next_vel = Vector3::new(0.0, 0.0, 0.0); + } + self.vehicle.entity.velocity.store(next_vel); + } + }) + } + + fn get_entity(&self) -> &Entity { + &self.vehicle.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn is_pushable(&self) -> bool { + true + } + + #[allow(clippy::too_many_lines)] + fn push<'a>(&'a self, entity: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let self_entity = self.get_entity(); + let other_entity = entity.get_entity(); + + if self_entity.no_clip.load(Ordering::Relaxed) + || other_entity.no_clip.load(Ordering::Relaxed) + { + return; + } + + { + let passengers = self_entity.passengers.lock().await; + if passengers + .iter() + .any(|p| p.get_entity().entity_id == other_entity.entity_id) + { + return; + } + } + { + let passengers = other_entity.passengers.lock().await; + if passengers + .iter() + .any(|p| p.get_entity().entity_id == self_entity.entity_id) + { + return; + } + } + + let mut xa = other_entity.pos.load().x - self_entity.pos.load().x; + let mut za = other_entity.pos.load().z - self_entity.pos.load().z; + let mut dd = xa * xa + za * za; + if dd >= 1.0E-4 { + dd = dd.sqrt(); + xa /= dd; + za /= dd; + let mut pow = 1.0 / dd; + if pow > 1.0 { + pow = 1.0; + } + xa *= pow; + za *= pow; + xa *= 0.1; + za *= 0.1; + xa *= 0.5; + za *= 0.5; + + let is_other_minecart = other_entity.entity_type.id == EntityType::MINECART.id + || other_entity.entity_type.id == EntityType::CHEST_MINECART.id + || other_entity.entity_type.id == EntityType::COMMAND_BLOCK_MINECART.id + || other_entity.entity_type.id == EntityType::FURNACE_MINECART.id + || other_entity.entity_type.id == EntityType::HOPPER_MINECART.id + || other_entity.entity_type.id == EntityType::SPAWNER_MINECART.id + || other_entity.entity_type.id == EntityType::TNT_MINECART.id; + + if is_other_minecart { + let xo = self_entity.velocity.load().x; + let zo = self_entity.velocity.load().z; + + let dir = Vector3::new(xo, 0.0, zo).normalize(); + let facing = Vector3::new( + f64::from(self_entity.yaw.load().to_radians().cos()), + 0.0, + f64::from(self_entity.yaw.load().to_radians().sin()), + ) + .normalize(); + + let dot = dir.dot(&facing).abs(); + if dot >= 0.8 { + let vel = self_entity.velocity.load(); + let ovel = other_entity.velocity.load(); + + let is_self_furnace = + self_entity.entity_type.id == EntityType::FURNACE_MINECART.id; + let is_other_furnace = + other_entity.entity_type.id == EntityType::FURNACE_MINECART.id; + + if is_other_furnace && !is_self_furnace { + self_entity.velocity.store(vel.multiply(0.2, 1.0, 0.2)); + let mut new_self_vel = self_entity.velocity.load(); + new_self_vel.x += ovel.x - xa; + new_self_vel.z += ovel.z - za; + self_entity.velocity.store(new_self_vel); + self_entity.send_velocity(); + + other_entity.velocity.store(ovel.multiply(0.95, 1.0, 0.95)); + other_entity.send_velocity(); + } else if !is_other_furnace && is_self_furnace { + other_entity.velocity.store(ovel.multiply(0.2, 1.0, 0.2)); + let mut new_other_vel = other_entity.velocity.load(); + new_other_vel.x += vel.x + xa; + new_other_vel.z += vel.z + za; + other_entity.velocity.store(new_other_vel); + other_entity.send_velocity(); + + self_entity.velocity.store(vel.multiply(0.95, 1.0, 0.95)); + self_entity.send_velocity(); + } else { + #[allow(clippy::manual_midpoint)] + let xdd = (ovel.x + vel.x) / 2.0; + #[allow(clippy::manual_midpoint)] + let zdd = (ovel.z + vel.z) / 2.0; + + self_entity.velocity.store(vel.multiply(0.2, 1.0, 0.2)); + let mut new_self_vel = self_entity.velocity.load(); + new_self_vel.x += xdd - xa; + new_self_vel.z += zdd - za; + self_entity.velocity.store(new_self_vel); + self_entity.send_velocity(); + + other_entity.velocity.store(ovel.multiply(0.2, 1.0, 0.2)); + let mut new_other_vel = other_entity.velocity.load(); + new_other_vel.x += xdd + xa; + new_other_vel.z += zdd + za; + other_entity.velocity.store(new_other_vel); + other_entity.send_velocity(); + } + } + } else { + if !self_entity.has_passengers().await && self.is_pushable() { + let mut vel = self_entity.velocity.load(); + vel.x -= xa; + vel.z -= za; + self_entity.velocity.store(vel); + self_entity.send_velocity(); + } + + if !other_entity.has_passengers().await && entity.is_pushable() { + let mut vel = other_entity.velocity.load(); + vel.x += xa / 4.0; + vel.z += za / 4.0; + other_entity.velocity.store(vel); + other_entity.send_velocity(); + } + } + } + }) + } + + fn is_collidable(&self, _entity: Option>) -> bool { + true + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.vehicle.send_wobble_metadata(); + }) + } + + fn can_hit(&self) -> bool { + self.vehicle.entity.is_alive() + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + amount: f32, + _damage_type: pumpkin_data::damage::DamageType, + _position: Option>, + source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { self.vehicle.damage_with_context(amount, source).await }) + } + + fn interact<'a>( + &'a self, + player: &'a Arc, + _item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if player.get_entity().is_sneaking() { + return false; + } + + if !self.vehicle.entity.passengers.lock().await.is_empty() { + return false; + } + + if player.get_entity().has_vehicle().await { + return false; + } + + let world = self.vehicle.entity.world.load(); + let Some(vehicle) = world.get_entity_by_id(self.vehicle.entity.entity_id) else { + return false; + }; + + let Some(passenger) = world.get_player_by_id(player.entity_id()) else { + return false; + }; + + self.vehicle + .entity + .add_passenger(vehicle, passenger as Arc) + .await; + + true + }) + } + + fn on_player_collision<'a>(&'a self, player: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + if self + .vehicle + .entity + .passengers + .lock() + .await + .iter() + .any(|passenger| passenger.get_entity().entity_id == player.entity_id()) + { + return; + } + + if player.is_spectator() { + return; + } + + let player_pos = player.get_entity().pos.load(); + let minecart_pos = self.vehicle.entity.pos.load(); + + let mut diff_x = minecart_pos.x - player_pos.x; + let mut diff_z = minecart_pos.z - player_pos.z; + + let dist_sq = diff_x * diff_x + diff_z * diff_z; + if dist_sq > 0.0001 { + let dist = dist_sq.sqrt(); + diff_x /= dist; + diff_z /= dist; + + let push_force = 0.1; + let mut vel = self.vehicle.entity.velocity.load(); + vel.x += diff_x * push_force; + vel.z += diff_z * push_force; + + let horizontal_speed = vel.x.hypot(vel.z); + if horizontal_speed > 0.4 { + vel.x = (vel.x / horizontal_speed) * 0.4; + vel.z = (vel.z / horizontal_speed) * 0.4; + } + + self.vehicle.entity.velocity.store(vel); + self.vehicle.entity.send_velocity(); + } + }) + } + + fn move_entity<'a>( + &'a self, + caller: &'a Arc, + motion: Vector3, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let to_position = self.vehicle.entity.pos.load().add(&motion); + self.vehicle.entity.move_entity(caller, motion).await; + let should_continue = self.push_entities(caller).await; + if should_continue { + let current_pos = self.vehicle.entity.pos.load(); + let back_motion = to_position.sub(¤t_pos); + self.vehicle.entity.move_entity(caller, back_motion).await; + } + }) + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/entity/vehicle/mod.rs b/pumpkin/src/entity/vehicle/mod.rs index 68d7f1ab2..239701ba0 100644 --- a/pumpkin/src/entity/vehicle/mod.rs +++ b/pumpkin/src/entity/vehicle/mod.rs @@ -1 +1,4 @@ pub mod boat; +pub mod minecart; +#[allow(clippy::module_inception)] +pub mod vehicle; diff --git a/pumpkin/src/entity/vehicle/vehicle.rs b/pumpkin/src/entity/vehicle/vehicle.rs new file mode 100644 index 000000000..d8d872eb3 --- /dev/null +++ b/pumpkin/src/entity/vehicle/vehicle.rs @@ -0,0 +1,131 @@ +use crossbeam::atomic::AtomicCell; +use std::sync::atomic::{AtomicI32, Ordering}; + +use crate::entity::Entity; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::EntityBase; +use crate::world::loot::{LootContextParameters, LootTableExt}; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_util::GameMode; + +pub struct VehicleEntity { + pub entity: Entity, + pub hurt_time: AtomicI32, + pub hurt_dir: AtomicI32, + pub damage: AtomicCell, +} + +impl VehicleEntity { + pub const fn new(entity: Entity) -> Self { + Self { + entity, + hurt_time: AtomicI32::new(0), + hurt_dir: AtomicI32::new(1), + damage: AtomicCell::new(0.0), + } + } + + pub fn tick(&self) { + let current_hurt = self.hurt_time.load(Ordering::Relaxed); + if current_hurt > 0 { + self.hurt_time.store(current_hurt - 1, Ordering::Relaxed); + } + + let current_damage = self.damage.load(); + if current_damage > 0.0 { + self.damage.store(current_damage - 1.0); + } + } + + pub fn set_damage(&self, damage: f32) { + self.damage.store(damage); + } + + pub fn get_damage(&self) -> f32 { + self.damage.load() + } + + pub fn set_hurt_time(&self, hurt_time: i32) { + self.hurt_time.store(hurt_time, Ordering::Relaxed); + } + + pub fn get_hurt_time(&self) -> i32 { + self.hurt_time.load(Ordering::Relaxed) + } + + pub fn set_hurt_dir(&self, hurt_dir: i32) { + self.hurt_dir.store(hurt_dir, Ordering::Relaxed); + } + + pub fn get_hurt_dir(&self) -> i32 { + self.hurt_dir.load(Ordering::Relaxed) + } + + pub fn send_wobble_metadata(&self) { + self.entity.send_meta_data(&[ + Metadata::new( + TrackedData::ID_HURT, + MetaDataType::INTEGER, + self.get_hurt_time(), + ), + Metadata::new( + TrackedData::ID_HURTDIR, + MetaDataType::INTEGER, + self.get_hurt_dir(), + ), + ]); + self.entity.send_meta_data(&[Metadata::new( + TrackedData::ID_DAMAGE, + MetaDataType::FLOAT, + self.get_damage(), + )]); + } + + pub async fn kill_and_drop_self(&self) { + let world = self.entity.world.load(); + let entity_drops = world.level_info.load().game_rules.entity_drops; + + if entity_drops && let Some(loot_table) = &self.entity.entity_type.loot_table { + let pos = self.entity.block_pos.load(); + let params = LootContextParameters::default(); + for stack in loot_table.get_loot(params) { + world.drop_stack(&pos, stack).await; + } + } + + self.entity.remove().await; + } + + pub async fn damage_with_context(&self, amount: f32, source: Option<&dyn EntityBase>) -> bool { + if !self.entity.is_alive() { + return true; + } + + let current_side = self.get_hurt_dir(); + self.set_hurt_dir(-current_side); + self.set_hurt_time(10); + self.entity.velocity_dirty.store(true, Ordering::SeqCst); + + let current_strength = self.get_damage(); + let new_strength = current_strength + amount * 10.0; + self.set_damage(new_strength); + + self.send_wobble_metadata(); + + let is_creative = source + .and_then(|s| s.get_player()) + .is_some_and(|p| p.gamemode.load() == GameMode::Creative); + + if is_creative || new_strength > 40.0 { + if is_creative { + self.entity.remove().await; + } else { + self.kill_and_drop_self().await; + } + } + + true + } +} diff --git a/pumpkin/src/item/items/bow.rs b/pumpkin/src/item/items/bow.rs index ff2bd1864..7fa7f17d6 100644 --- a/pumpkin/src/item/items/bow.rs +++ b/pumpkin/src/item/items/bow.rs @@ -173,8 +173,7 @@ impl BowItem { { for (enchantment, level) in enchantments.enchantment.iter() { if **enchantment == pumpkin_data::Enchantment::POWER { - arrow.base_damage = - arrow.base_damage * (1.0 + 0.25 * (f64::from(*level) + 1.0)); + arrow.base_damage *= 1.0 + 0.25 * (f64::from(*level) + 1.0); } else if **enchantment == pumpkin_data::Enchantment::PUNCH { arrow.punch_level.store(*level as u8, Ordering::Relaxed); } else if **enchantment == pumpkin_data::Enchantment::FLAME { diff --git a/pumpkin/src/item/items/minecart.rs b/pumpkin/src/item/items/minecart.rs index 9f78305b6..20ab9589f 100644 --- a/pumpkin/src/item/items/minecart.rs +++ b/pumpkin/src/item/items/minecart.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::entity::Entity; use crate::entity::player::Player; +use crate::entity::vehicle::minecart::MinecartEntity; use crate::item::{ItemBehaviour, ItemMetadata}; use crate::server::Server; use pumpkin_data::BlockDirection; @@ -80,12 +81,13 @@ impl ItemBehaviour for MinecartItem { let height = if is_ascending { 0.5 } else { 0.0 }; let entity_type = Self::item_to_entity(item.item); let pos = location.to_f64(); - let entity = Arc::new(Entity::new( + let entity = Entity::new( world.clone(), Vector3::new(pos.x, pos.y + 0.0625 + height, pos.z), entity_type, - )); - world.spawn_entity(entity).await; + ); + let minecart_entity = Arc::new(MinecartEntity::new(entity)); + world.spawn_entity(minecart_entity).await; }) } diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 609b2be5f..d5b2e664b 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -325,6 +325,9 @@ impl JavaClient { if !player.has_client_loaded() { return; } + if player.get_entity().has_vehicle().await { + return; + } // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) if player.awaiting_teleport.lock().await.is_some() { return; @@ -455,6 +458,9 @@ impl JavaClient { if !player.has_client_loaded() { return; } + if player.get_entity().has_vehicle().await { + return; + } // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) if player.awaiting_teleport.lock().await.is_some() { return; @@ -978,6 +984,8 @@ impl JavaClient { input: SPlayerInput, server: &Server, ) { + player.last_input.store(input.input, Ordering::Relaxed); + let sneak = input.input & SPlayerInput::SNEAK != 0; if player.get_entity().is_sneaking() != sneak { send_cancellable! {{