feat: add minecarts

This commit is contained in:
Alexander Medvedev 2026-06-21 15:01:06 +02:00
parent c52531b37a
commit daa0111009
14 changed files with 1259 additions and 164 deletions

View File

@ -28,7 +28,7 @@ struct Rail {
impl Rail {
fn find_with_elevation(world: &World, position: BlockPos) -> Option<Self> {
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,

View File

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

View File

@ -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<dyn EntityBase>) -> 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<dyn EntityBase>,
motion: Vector3<f64>,
) -> 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<dyn EntityBase>) -> 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<dyn EntityBase>,
) -> 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<dyn EntityBase> = 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<dyn EntityBase> = 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<dyn EntityBase>, mut motion: Vector3<f64>) {
pub async fn move_entity<'a>(
&'a self,
caller: &'a Arc<dyn EntityBase>,
mut motion: Vector3<f64>,
) {
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
}
}
};

View File

@ -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<BlockPos>,
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())

View File

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

View File

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

View File

@ -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 => {

View File

@ -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<f32>,
pub vehicle: VehicleEntity,
ticks_underwater: AtomicCell<f32>,
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<Box<dyn EntityBase>>) -> 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<dyn EntityBase>)
.await;
@ -229,4 +157,8 @@ impl EntityBase for BoatEntity {
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
true
}
}

View File

@ -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<f64>, Vector3<f64>) {
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<dyn EntityBase>,
_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<dyn EntityBase>) -> 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<Box<dyn EntityBase>>) -> 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<Vector3<f64>>,
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<Player>,
_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<dyn EntityBase>)
.await;
true
})
}
fn on_player_collision<'a>(&'a self, player: &'a Arc<Player>) -> 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<dyn EntityBase>,
motion: Vector3<f64>,
) -> 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(&current_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
}
}

View File

@ -1 +1,4 @@
pub mod boat;
pub mod minecart;
#[allow(clippy::module_inception)]
pub mod vehicle;

View File

@ -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<f32>,
}
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
}
}

View File

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

View File

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

View File

@ -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! {{