diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 3ffe624f8..82aa069ec 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -3226,6 +3226,28 @@ impl Entity { ); } + pub(crate) async fn remove_passenger_on_disconnect(&self, passenger_id: i32) { + let mut passengers = self.passengers.lock().await; + if let Some(index) = passengers + .iter() + .position(|passenger| passenger.get_entity().entity_id == passenger_id) + { + let passenger = passengers.remove(index); + *passenger.get_entity().vehicle.lock().await = None; + } + + let passenger_ids: Vec = passengers + .iter() + .map(|passenger| VarInt(passenger.get_entity().entity_id)) + .collect(); + drop(passengers); + + self.world.load().broadcast_to_chunk( + self.chunk_pos.load(), + &CSetPassengers::new(VarInt(self.entity_id), &passenger_ids), + ); + } + #[allow(clippy::too_many_lines)] pub async fn remove_passenger(&self, passenger_id: i32) { let mut passengers = self.passengers.lock().await; diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index f5cfde7e6..4510737d0 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -121,6 +121,45 @@ use pumpkin_world::chunk_system::ChunkLoading; const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128 const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 +fn write_root_vehicle(nbt: &mut NbtCompound, uuid: Uuid) { + let value = uuid.as_u128(); + let mut root_vehicle = NbtCompound::new(); + root_vehicle.put( + "Attach", + NbtTag::IntArray(vec![ + (value >> 96) as i32, + (value >> 64) as i32, + (value >> 32) as i32, + value as i32, + ]), + ); + nbt.put("RootVehicle", NbtTag::Compound(root_vehicle)); +} + +fn read_root_vehicle(nbt: &NbtCompound) -> Option { + let root_vehicle = nbt.get_compound("RootVehicle")?; + let uuid = if let Some([most, more, less, least]) = root_vehicle.get_int_array("Attach") { + [*most, *more, *less, *least] + } else { + let [most, more, less, least] = root_vehicle.get_list("Attach")? else { + return None; + }; + [ + most.extract_int()?, + more.extract_int()?, + less.extract_int()?, + least.extract_int()?, + ] + }; + + Some(Uuid::from_u128( + (uuid[0] as u32 as u128) << 96 + | (uuid[1] as u32 as u128) << 64 + | (uuid[2] as u32 as u128) << 32 + | uuid[3] as u32 as u128, + )) +} + pub const DATA_VERSION: i32 = 4903; // 26.2 /// Food exhaustion applied for every block a player mines. @@ -532,6 +571,7 @@ pub struct Player { pub experience_pick_up_delay: Mutex, pub chunk_manager: Mutex, pub has_played_before: AtomicBool, + root_vehicle_uuid: AtomicCell>, pub chat_session: Arc>, pub signature_cache: Mutex, pub player_screen_handler: Arc>, @@ -790,6 +830,7 @@ impl Player { last_food_saturation: AtomicBool::new(true), subscribed_debug_sample: AtomicBool::new(false), has_played_before: AtomicBool::new(false), + root_vehicle_uuid: AtomicCell::new(None), chat_session: Arc::new(Mutex::new(ChatSession::default())), // Placeholder value until the player actually sets their session id signature_cache: Mutex::new(MessageCache::default()), player_screen_handler: player_screen_handler.clone(), @@ -958,6 +999,16 @@ impl Player { /// Removes the [`Player`] out of the current [`World`]. pub async fn remove(self: &Arc) { + let vehicle = self.living_entity.entity.vehicle.lock().await.clone(); + if let Some(vehicle) = vehicle { + self.root_vehicle_uuid + .store(Some(vehicle.get_entity().entity_uuid)); + vehicle + .get_entity() + .remove_passenger_on_disconnect(self.entity_id()) + .await; + } + self.stats .lock() .await @@ -1004,6 +1055,21 @@ impl Player { //self.world().level.list_cached(); } + pub(crate) async fn try_restore_vehicle(self: &Arc, vehicle: &Arc) { + let Some(expected_uuid) = self.root_vehicle_uuid.swap(None) else { + return; + }; + if vehicle.get_entity().entity_uuid != expected_uuid { + self.root_vehicle_uuid.store(Some(expected_uuid)); + return; + } + + vehicle + .get_entity() + .add_passenger(vehicle.clone(), self.clone()) + .await; + } + #[expect(clippy::too_many_lines)] pub async fn attack(&self, victim: Arc) { let world = self.world(); @@ -4755,6 +4821,18 @@ impl NBTStorage for Player { nbt.put_bool("SpawnForced", respawn.force); } nbt.put_int("XpSeed", self.enchantment_seed.load(Ordering::Relaxed)); + let vehicle_uuid = self + .living_entity + .entity + .vehicle + .lock() + .await + .as_ref() + .map(|vehicle| vehicle.get_entity().entity_uuid) + .or_else(|| self.root_vehicle_uuid.load()); + if let Some(vehicle_uuid) = vehicle_uuid { + write_root_vehicle(nbt, vehicle_uuid); + } self.stats.lock().await.write_nbt(nbt); }) } @@ -4825,6 +4903,7 @@ impl NBTStorage for Player { nbt.get_int("XpSeed").unwrap_or(rand::random()), Ordering::Relaxed, ); + self.root_vehicle_uuid.store(read_root_vehicle(nbt)); self.stats.lock().await.read_nbt(nbt); }) } @@ -5750,7 +5829,9 @@ impl InventoryPlayer for Player { #[cfg(test)] mod tests { - use super::bedrock_inventory_slot; + use super::{bedrock_inventory_slot, read_root_vehicle, write_root_vehicle}; + use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; + use uuid::Uuid; #[test] fn player_screen_slots_map_to_bedrock_inventory() { @@ -5761,4 +5842,39 @@ mod tests { assert_eq!(bedrock_inventory_slot(8), None); assert_eq!(bedrock_inventory_slot(45), None); } + + #[test] + fn root_vehicle_uuid_round_trips_with_vanilla_shape() { + let expected = Uuid::from_u128(0xFEDC_BA98_7654_3210_89AB_CDEF_0123_4567); + let mut nbt = NbtCompound::new(); + + write_root_vehicle(&mut nbt, expected); + + assert_eq!(read_root_vehicle(&nbt), Some(expected)); + assert!( + nbt.get_compound("RootVehicle") + .and_then(|root| root.get_int_array("Attach")) + .is_some() + ); + } + + #[test] + fn root_vehicle_uuid_accepts_integer_lists() { + let expected = Uuid::from_u128(0xFEDC_BA98_7654_3210_89AB_CDEF_0123_4567); + let value = expected.as_u128(); + let mut root_vehicle = NbtCompound::new(); + root_vehicle.put( + "Attach", + NbtTag::List(vec![ + NbtTag::Int((value >> 96) as i32), + NbtTag::Int((value >> 64) as i32), + NbtTag::Int((value >> 32) as i32), + NbtTag::Int(value as i32), + ]), + ); + let mut nbt = NbtCompound::new(); + nbt.put("RootVehicle", NbtTag::Compound(root_vehicle)); + + assert_eq!(read_root_vehicle(&nbt), Some(expected)); + } } diff --git a/crates/pumpkin/src/entity/projectile/arrow.rs b/crates/pumpkin/src/entity/projectile/arrow.rs index 336c65b4d..a463a773d 100644 --- a/crates/pumpkin/src/entity/projectile/arrow.rs +++ b/crates/pumpkin/src/entity/projectile/arrow.rs @@ -508,7 +508,14 @@ impl EntityBase for ArrowEntity { } let damage_succeeded = target - .damage(&*target, damage as f32, DamageType::ARROW) + .damage_with_context( + &*target, + damage as f32, + DamageType::ARROW, + Some(hit_pos), + None, + Some(self), + ) .await; if let Some(living) = target.get_living_entity() { diff --git a/crates/pumpkin/src/entity/vehicle/minecart.rs b/crates/pumpkin/src/entity/vehicle/minecart.rs index 7c0b2359f..ea4cd1356 100644 --- a/crates/pumpkin/src/entity/vehicle/minecart.rs +++ b/crates/pumpkin/src/entity/vehicle/minecart.rs @@ -1,22 +1,43 @@ +mod chest; +mod container; +mod furnace; +mod hopper; +mod rideable; +mod tnt; + use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::Ordering; use pumpkin_protocol::java::server::play::SPlayerInput; +use rand::RngExt; use crate::{ entity::{ - Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, + player::Player, }, server::Server, }; use pumpkin_data::Block; use pumpkin_data::block_properties::{BlockProperties, PoweredRailLikeProperties}; +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::GameMode; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::inventory::Inventory; use crate::entity::vehicle::vehicle::VehicleEntity; +use chest::ChestMinecart; +use container::MinecartInventory; +use furnace::FurnaceMinecart; +use hopper::HopperMinecart; +use rideable::RideableMinecart; +use tnt::TntMinecart; const fn get_exits( shape: pumpkin_data::block_properties::RailShape, @@ -41,21 +62,84 @@ const RAIL_HEIGHT_OFFSET: f64 = 0.0625; pub struct MinecartEntity { pub vehicle: VehicleEntity, - pub active: AtomicBool, - pub tnt_fuse: AtomicI32, + kind: MinecartKind, +} + +enum MinecartKind { + Rideable(RideableMinecart), + Chest(ChestMinecart), + Furnace(FurnaceMinecart), + Hopper(HopperMinecart), + Tnt(TntMinecart), + Other, } impl MinecartEntity { - pub const fn new(entity: Entity) -> Self { + pub fn new(entity: Entity) -> Self { + let kind = match entity.entity_type.id { + id if id == EntityType::MINECART.id => MinecartKind::Rideable(RideableMinecart), + id if id == EntityType::CHEST_MINECART.id => MinecartKind::Chest(ChestMinecart::new()), + id if id == EntityType::FURNACE_MINECART.id => { + MinecartKind::Furnace(FurnaceMinecart::new()) + } + id if id == EntityType::HOPPER_MINECART.id => { + MinecartKind::Hopper(HopperMinecart::new()) + } + id if id == EntityType::TNT_MINECART.id => MinecartKind::Tnt(TntMinecart::new()), + _ => MinecartKind::Other, + }; Self { vehicle: VehicleEntity::new(entity), - active: AtomicBool::new(false), - tnt_fuse: AtomicI32::new(-1), + kind, + } + } + + const fn container(&self) -> Option<&Arc> { + match &self.kind { + MinecartKind::Chest(minecart) => Some(minecart.inventory()), + MinecartKind::Hopper(minecart) => Some(minecart.inventory()), + _ => None, + } + } + + const fn drop_item(&self) -> Option<&'static Item> { + match &self.kind { + MinecartKind::Chest(_) => Some(&Item::CHEST_MINECART), + MinecartKind::Furnace(_) => Some(&Item::FURNACE_MINECART), + MinecartKind::Hopper(_) => Some(&Item::HOPPER_MINECART), + MinecartKind::Tnt(_) => Some(&Item::TNT_MINECART), + _ => None, } } } -impl NBTStorage for MinecartEntity {} +impl NBTStorage for MinecartEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.vehicle.entity.write_nbt(nbt).await; + match &self.kind { + MinecartKind::Chest(minecart) => minecart.write_nbt(nbt).await, + MinecartKind::Furnace(minecart) => minecart.write_nbt(nbt), + MinecartKind::Hopper(minecart) => minecart.write_nbt(nbt).await, + MinecartKind::Tnt(minecart) => minecart.write_nbt(nbt), + MinecartKind::Rideable(_) | MinecartKind::Other => {} + } + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.vehicle.entity.read_nbt_non_mut(nbt).await; + match &self.kind { + MinecartKind::Chest(minecart) => minecart.read_nbt(nbt).await, + MinecartKind::Furnace(minecart) => minecart.read_nbt(nbt), + MinecartKind::Hopper(minecart) => minecart.read_nbt(nbt).await, + MinecartKind::Tnt(minecart) => minecart.read_nbt(nbt), + MinecartKind::Rideable(_) | MinecartKind::Other => {} + } + }) + } +} impl EntityBase for MinecartEntity { #[allow(clippy::too_many_lines)] @@ -66,6 +150,9 @@ impl EntityBase for MinecartEntity { ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.vehicle.tick(); + if let MinecartKind::Furnace(minecart) = &self.kind { + minecart.tick(&self.vehicle.entity); + } let world = self.vehicle.entity.world.load(); let pos = self.vehicle.entity.pos.load(); @@ -111,7 +198,9 @@ impl EntityBase for MinecartEntity { let props = PoweredRailLikeProperties::from_state_id(state_id, block); let powered = props.powered; - self.active.store(powered, Ordering::Relaxed); + if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind { + minecart.set_enabled(!powered); + } if powered { if is_powered_rail { @@ -137,27 +226,27 @@ impl EntityBase for MinecartEntity { } 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, - ); + match &self.kind { + MinecartKind::Tnt(minecart) => { + minecart.prime(&self.vehicle.entity, 80); } - } 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 + MinecartKind::Rideable(_) => { + let passengers = + self.vehicle.entity.passengers.lock().await.clone(); + for passenger in passengers { + self.vehicle + .entity + .remove_passenger(passenger.get_entity().entity_id) + .await; + } + 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); + self.vehicle.send_wobble_metadata(); + } + } + _ => {} } } } else if is_powered_rail { @@ -171,27 +260,10 @@ impl EntityBase for MinecartEntity { } } - 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; - } - } + if let MinecartKind::Tnt(minecart) = &self.kind + && minecart.tick(&self.vehicle.entity).await + { + return; } let mut velocity = self.vehicle.entity.velocity.load(); @@ -352,6 +424,24 @@ impl EntityBase for MinecartEntity { if velocity.length() > 0.001 { self.move_entity(caller, velocity).await; + + if let MinecartKind::Tnt(minecart) = &self.kind + && self + .vehicle + .entity + .horizontal_collision + .load(Ordering::Relaxed) + && velocity.x.mul_add(velocity.x, velocity.z * velocity.z) >= 0.01 + { + minecart + .explode( + &self.vehicle.entity, + velocity.x.mul_add(velocity.x, velocity.z * velocity.z), + ) + .await; + return; + } + let new_pos = self.vehicle.entity.pos.load(); let passengers = self.vehicle.entity.passengers.lock().await; @@ -363,7 +453,7 @@ impl EntityBase for MinecartEntity { self.vehicle.entity.send_pos_rot(); #[allow(clippy::useless_let_if_seq)] - let mut friction = 0.98; // Default off-rail air drag + let mut friction = 0.95; // Vanilla minecart air drag if is_on_rails { let passengers = self.vehicle.entity.passengers.lock().await; @@ -392,7 +482,14 @@ impl EntityBase for MinecartEntity { } } - let mut next_vel = velocity.multiply(friction, friction, friction); + let mut next_vel = + if is_on_rails && let MinecartKind::Furnace(minecart) = &self.kind { + minecart.velocity(&self.vehicle.entity, velocity) + } else if is_on_rails && let Some(inventory) = self.container() { + container::velocity(&self.vehicle.entity, inventory, velocity).await + } else { + velocity.multiply(friction, friction, friction) + }; if next_vel.length() < 0.005 { next_vel = Vector3::new(0.0, 0.0, 0.0); } @@ -401,6 +498,10 @@ impl EntityBase for MinecartEntity { self.vehicle.entity.send_velocity(); } } + + if let MinecartKind::Hopper(minecart) = &self.kind { + minecart.tick(&self.vehicle.entity).await; + } }) } @@ -564,6 +665,9 @@ impl EntityBase for MinecartEntity { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { self.vehicle.send_wobble_metadata(); + if let MinecartKind::Furnace(minecart) = &self.kind { + minecart.init_data_tracker(&self.vehicle.entity); + } }) } @@ -575,47 +679,108 @@ impl EntityBase for MinecartEntity { &'a self, _caller: &'a dyn EntityBase, amount: f32, - _damage_type: pumpkin_data::damage::DamageType, + damage_type: DamageType, _position: Option>, source: Option<&'a dyn EntityBase>, - _cause: Option<&'a dyn EntityBase>, + cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { - Box::pin(async move { self.vehicle.damage_with_context(amount, source).await }) + Box::pin(async move { + let creative = source + .and_then(EntityBase::get_player) + .is_some_and(|player| player.gamemode.load() == GameMode::Creative); + + if let MinecartKind::Tnt(minecart) = &self.kind + && damage_type == DamageType::ARROW + && self.vehicle.entity.fire_ticks.load(Ordering::Relaxed) > 0 + { + let projectile_speed_squared = cause + .map(|entity| entity.get_entity().velocity.load().length_squared()) + .unwrap_or_default(); + minecart + .explode(&self.vehicle.entity, projectile_speed_squared) + .await; + if self.vehicle.entity.is_removed() { + return true; + } + } + + let will_break = self.vehicle.entity.is_alive() + && (creative || self.vehicle.get_damage() + amount * 10.0 > 40.0); + + if let MinecartKind::Tnt(minecart) = &self.kind + && will_break + && !creative + { + let velocity = self.vehicle.entity.velocity.load(); + let speed_squared = velocity.x.mul_add(velocity.x, velocity.z * velocity.z); + let ignites = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE) + || damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_EXPLOSION) + || self.vehicle.entity.fire_ticks.load(Ordering::Relaxed) > 0; + if ignites || speed_squared >= 0.01 { + self.vehicle.apply_damage_wobble(amount); + let fuse = rand::rng().random_range(0..20) + rand::rng().random_range(0..20); + if self + .vehicle + .entity + .world + .load() + .level_info + .load() + .game_rules + .tnt_explodes + { + minecart.prime(&self.vehicle.entity, fuse); + } else { + minecart.set_fuse(fuse); + } + return true; + } + } + + let damaged = self.vehicle.damage_with_context(amount, source).await; + + if will_break && !creative && self.vehicle.entity.is_removed() { + let world = self.vehicle.entity.world.load(); + if world.level_info.load().game_rules.entity_drops { + let position = self.vehicle.entity.block_pos.load(); + if let Some(container) = self.container() + && container.claim_drops() + { + container.unpack_loot().await; + let inventory: Arc = container.clone(); + world.scatter_inventory(&position, &inventory).await; + } + if let Some(item) = self.drop_item() { + world.drop_stack(&position, ItemStack::new(1, item)).await; + } + } + } + + damaged + }) } fn interact<'a>( &'a self, player: &'a Arc, - _item_stack: &'a mut ItemStack, + item_stack: &'a mut ItemStack, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { - if player.get_entity().is_sneaking() { - return false; + match &self.kind { + MinecartKind::Chest(minecart) => { + minecart.interact(&self.vehicle.entity, player).await + } + MinecartKind::Furnace(minecart) => { + minecart.interact(&self.vehicle.entity, player, item_stack) + } + MinecartKind::Hopper(minecart) => { + minecart.interact(&self.vehicle.entity, player).await + } + MinecartKind::Rideable(minecart) => { + minecart.interact(&self.vehicle.entity, player).await + } + MinecartKind::Tnt(_) | MinecartKind::Other => 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 }) } diff --git a/crates/pumpkin/src/entity/vehicle/minecart/chest.rs b/crates/pumpkin/src/entity/vehicle/minecart/chest.rs new file mode 100644 index 000000000..e7cacf9ab --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/chest.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use pumpkin_data::translation; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::text::TextComponent; + +use crate::entity::{Entity, player::Player}; + +use super::container::{self, MinecartInventory}; + +pub(super) struct ChestMinecart { + inventory: Arc, +} + +impl ChestMinecart { + pub(super) fn new() -> Self { + Self { + inventory: Arc::new(MinecartInventory::new(27)), + } + } + + pub(super) const fn inventory(&self) -> &Arc { + &self.inventory + } + + pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { + container::open( + entity, + player, + &self.inventory, + TextComponent::translate_cross( + translation::java::ENTITY_MINECRAFT_CHEST_MINECART, + translation::bedrock::ENTITY_CHEST_MINECART_NAME, + [], + ), + false, + ) + .await + } + + pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { + self.inventory.write_nbt(nbt).await; + } + + pub(super) async fn read_nbt(&self, nbt: &NbtCompound) { + self.inventory.read_nbt(nbt).await; + } +} diff --git a/crates/pumpkin/src/entity/vehicle/minecart/container.rs b/crates/pumpkin/src/entity/vehicle/minecart/container.rs new file mode 100644 index 000000000..69192383f --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/container.rs @@ -0,0 +1,276 @@ +use std::any::Any; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use pumpkin_data::item_stack::ItemStack; +use pumpkin_inventory::generic_container_screen_handler::{create_generic_9x3, create_hopper}; +use pumpkin_inventory::player::player_inventory::PlayerInventory; +use pumpkin_inventory::screen_handler::{ + BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, +}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::text::TextComponent; +use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture}; +use tokio::sync::{Mutex, RwLock}; + +use crate::entity::{Entity, EntityBase, player::Player}; +use crate::world::loot::fill_chest_inventory; +use pumpkin_data::chest_loot_table::get_chest_loot_table; + +pub(super) struct MinecartInventory { + items: RwLock>, + size: usize, + loot_table: Mutex>, + drops_claimed: AtomicBool, +} + +impl MinecartInventory { + pub(super) fn new(size: usize) -> Self { + Self { + items: RwLock::new(vec![ItemStack::EMPTY.clone(); size]), + size, + loot_table: Mutex::new(None), + drops_claimed: AtomicBool::new(false), + } + } + + pub(super) fn claim_drops(&self) -> bool { + !self.drops_claimed.swap(true, Ordering::AcqRel) + } + + pub(super) async fn read_nbt(&self, nbt: &NbtCompound) { + let loot_table = nbt.get_string("LootTable").map(|loot_table| { + ( + loot_table.to_owned(), + nbt.get_long("LootTableSeed").unwrap_or(0), + ) + }); + let has_loot_table = loot_table.is_some(); + *self.loot_table.lock().await = loot_table; + + if !has_loot_table { + let mut items = self.items.write().await; + items.fill_with(|| ItemStack::EMPTY.clone()); + self.read_data(nbt, &mut items); + } + } + + pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { + let loot_table = self.loot_table.lock().await.clone(); + if let Some((loot_table, seed)) = loot_table { + nbt.put_string("LootTable", loot_table); + if seed != 0 { + nbt.put_long("LootTableSeed", seed); + } + } else { + self.write_inventory_nbt(nbt, true).await; + } + } + + pub(super) async fn has_loot_table(&self) -> bool { + self.loot_table.lock().await.is_some() + } + + pub(super) async fn unpack_loot(self: &Arc) { + let loot_table = self.loot_table.lock().await.take(); + let Some((loot_table, seed)) = loot_table else { + return; + }; + let Some(table) = get_chest_loot_table(&loot_table) else { + *self.loot_table.lock().await = Some((loot_table, seed)); + return; + }; + + let inventory: Arc = self.clone(); + fill_chest_inventory(&inventory, table, seed).await; + } +} + +impl Inventory for MinecartInventory { + fn size(&self) -> usize { + self.size + } + + fn is_empty(&self) -> InventoryFuture<'_, bool> { + Box::pin(async move { + let items = self.items.read().await; + items.iter().all(ItemStack::is_empty) + }) + } + + fn get_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { self.items.read().await[slot].clone() }) + } + + fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut items = self.items.write().await; + std::mem::replace(&mut items[slot], ItemStack::EMPTY.clone()) + }) + } + + fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut items = self.items.write().await; + if !items[slot].is_empty() && amount > 0 { + items[slot].split(amount) + } else { + ItemStack::EMPTY.clone() + } + }) + } + + fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> { + Box::pin(async move { + self.items.write().await[slot] = stack; + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl Clearable for MinecartInventory { + fn clear(&self) -> Pin + Send + '_>> { + Box::pin(async move { + self.items + .write() + .await + .fill_with(|| ItemStack::EMPTY.clone()); + }) + } +} + +struct MinecartScreenFactory { + inventory: Arc, + title: TextComponent, + hopper: bool, +} + +impl ScreenHandlerFactory for MinecartScreenFactory { + fn create_screen_handler<'a>( + &'a self, + sync_id: u8, + player_inventory: &'a Arc, + _player: &'a dyn InventoryPlayer, + ) -> BoxFuture<'a, Option> { + Box::pin(async move { + let inventory: Arc = self.inventory.clone(); + let handler = if self.hopper { + create_hopper(sync_id, player_inventory, inventory).await + } else { + create_generic_9x3(sync_id, player_inventory, inventory).await + }; + Some(Arc::new(Mutex::new(handler)) as SharedScreenHandler) + }) + } + + fn get_display_name(&self) -> TextComponent { + self.title.clone() + } +} + +pub(super) async fn open( + entity: &Entity, + player: &Arc, + inventory: &Arc, + title: TextComponent, + hopper: bool, +) -> bool { + if player.is_spectator() && inventory.has_loot_table().await { + return false; + } + if !player.is_spectator() { + inventory.unpack_loot().await; + } + + player + .open_handled_screen( + &MinecartScreenFactory { + inventory: inventory.clone(), + title: entity.custom_name.load().as_ref().clone().unwrap_or(title), + hopper, + }, + None, + ) + .await + .is_some() +} + +pub(super) async fn velocity( + entity: &Entity, + inventory: &MinecartInventory, + velocity: Vector3, +) -> Vector3 { + let signal = crate::block::calculate_comparator_output(inventory).await; + let mut friction = if inventory.has_loot_table().await { + 0.98 + } else { + 0.98 + f64::from(15 - signal) * 0.001 + }; + if entity + .touching_water + .load(std::sync::atomic::Ordering::Relaxed) + { + friction *= 0.95; + } + velocity.multiply(friction, 0.0, friction) +} + +#[cfg(test)] +mod tests { + use super::MinecartInventory; + use pumpkin_data::item::Item; + use pumpkin_data::item_stack::ItemStack; + use pumpkin_nbt::compound::NbtCompound; + use pumpkin_world::inventory::Inventory; + + #[tokio::test] + async fn deferred_mineshaft_loot_is_preserved_until_unpacked() { + let inventory = std::sync::Arc::new(MinecartInventory::new(27)); + let mut source = NbtCompound::new(); + source.put_string( + "LootTable", + "minecraft:chests/abandoned_mineshaft".to_string(), + ); + source.put_long("LootTableSeed", 1234); + inventory.read_nbt(&source).await; + + let mut deferred = NbtCompound::new(); + inventory.write_nbt(&mut deferred).await; + assert_eq!( + deferred.get_string("LootTable"), + Some("minecraft:chests/abandoned_mineshaft") + ); + assert_eq!(deferred.get_long("LootTableSeed"), Some(1234)); + assert!(deferred.get_list("Items").is_none()); + + inventory.unpack_loot().await; + assert!(!inventory.is_empty().await); + + let mut unpacked = NbtCompound::new(); + inventory.write_nbt(&mut unpacked).await; + assert!(unpacked.get_string("LootTable").is_none()); + assert!(unpacked.get_list("Items").is_some()); + } + + #[tokio::test] + async fn chest_minecart_items_round_trip_through_nbt() { + let inventory = MinecartInventory::new(27); + inventory + .set_stack(8, ItemStack::new(3, &Item::POWERED_RAIL)) + .await; + + let mut nbt = NbtCompound::new(); + inventory.write_nbt(&mut nbt).await; + + let restored = MinecartInventory::new(27); + restored.read_nbt(&nbt).await; + let stack = restored.get_stack(8).await; + assert_eq!(stack.get_item().id, Item::POWERED_RAIL.id); + assert_eq!(stack.item_count, 3); + } +} diff --git a/crates/pumpkin/src/entity/vehicle/minecart/furnace.rs b/crates/pumpkin/src/entity/vehicle/minecart/furnace.rs new file mode 100644 index 000000000..b8e13ba7b --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/furnace.rs @@ -0,0 +1,160 @@ +use std::sync::atomic::{AtomicI32, Ordering}; + +use crossbeam::atomic::AtomicCell; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::particle::Particle; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +use crate::entity::{Entity, EntityBase, player::Player}; + +pub(super) struct FurnaceMinecart { + fuel: AtomicI32, + push: AtomicCell>, +} + +impl FurnaceMinecart { + const FUEL_PER_ITEM: i32 = 3_600; + const MAX_FUEL: i32 = 32_000; + + pub(super) const fn new() -> Self { + Self { + fuel: AtomicI32::new(0), + push: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), + } + } + + fn set_fueled(entity: &Entity, fueled: bool) { + entity.send_meta_data( + &[Metadata::new( + TrackedData::ID_FUEL, + MetaDataType::BOOLEAN, + fueled, + )], + None, + ); + } + + pub(super) fn tick(&self, entity: &Entity) { + let fuel = self.fuel.load(Ordering::Relaxed); + if fuel <= 0 { + self.push.store(Vector3::new(0.0, 0.0, 0.0)); + return; + } + + let remaining = fuel - 1; + self.fuel.store(remaining, Ordering::Relaxed); + if remaining == 0 { + self.push.store(Vector3::new(0.0, 0.0, 0.0)); + Self::set_fueled(entity, false); + } else if rand::rng().random_range(0..4) == 0 { + let mut pos = entity.pos.load(); + pos.y += 0.8; + entity.world.load().spawn_particle( + pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + Particle::LargeSmoke, + ); + } + } + + pub(super) fn velocity(&self, entity: &Entity, velocity: Vector3) -> Vector3 { + let mut push = self.push.load(); + let push_length_squared = push.x.mul_add(push.x, push.z * push.z); + let velocity_length_squared = velocity.x.mul_add(velocity.x, velocity.z * velocity.z); + + let mut next = if push_length_squared > 1.0e-7 { + if push_length_squared > 1.0e-4 && velocity_length_squared > 0.001 { + let velocity_direction = Vector3::new(velocity.x, 0.0, velocity.z).normalize(); + let push_length = push.length(); + let push_scale = if velocity_direction.dot(&push) < 0.0 { + -push_length + } else { + push_length + }; + push = velocity_direction.multiply(push_scale, 0.0, push_scale); + self.push.store(push); + } + velocity.multiply(0.8, 0.0, 0.8).add(&push) + } else { + velocity.multiply(0.98, 0.0, 0.98) + }; + + let in_water = entity.touching_water.load(Ordering::Relaxed); + if in_water { + next = next.multiply(0.1, 0.0, 0.1); + } + let slowdown = 0.96 * if in_water { 0.95 } else { 1.0 }; + next = next.multiply(slowdown, 0.0, slowdown); + + let max_speed = if in_water { 0.3 } else { 0.2 }; + let speed = next.x.hypot(next.z); + if speed > max_speed { + next.x = next.x / speed * max_speed; + next.z = next.z / speed * max_speed; + } + next + } + + pub(super) fn interact( + &self, + entity: &Entity, + player: &Player, + item_stack: &mut ItemStack, + ) -> bool { + let fuel = self.fuel.load(Ordering::Relaxed); + if item_stack + .get_item() + .has_tag(&tag::Item::MINECRAFT_FURNACE_MINECART_FUEL) + && fuel <= Self::MAX_FUEL - Self::FUEL_PER_ITEM + { + self.fuel + .store(fuel + Self::FUEL_PER_ITEM, Ordering::Relaxed); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + if fuel <= 0 { + Self::set_fueled(entity, true); + } + } + + if self.fuel.load(Ordering::Relaxed) > 0 { + let cart_pos = entity.pos.load(); + let player_pos = player.get_entity().pos.load(); + self.push.store(Vector3::new( + cart_pos.x - player_pos.x, + 0.0, + cart_pos.z - player_pos.z, + )); + } + true + } + + pub(super) fn init_data_tracker(&self, entity: &Entity) { + Self::set_fueled(entity, self.fuel.load(Ordering::Relaxed) > 0); + } + + pub(super) fn write_nbt(&self, nbt: &mut NbtCompound) { + let push = self.push.load(); + nbt.put_double("PushX", push.x); + nbt.put_double("PushZ", push.z); + nbt.put_short("Fuel", self.fuel.load(Ordering::Relaxed) as i16); + } + + pub(super) fn read_nbt(&self, nbt: &NbtCompound) { + self.push.store(Vector3::new( + nbt.get_double("PushX").unwrap_or(0.0), + 0.0, + nbt.get_double("PushZ").unwrap_or(0.0), + )); + self.fuel.store( + i32::from(nbt.get_short("Fuel").unwrap_or(0)), + Ordering::Relaxed, + ); + } +} diff --git a/crates/pumpkin/src/entity/vehicle/minecart/hopper.rs b/crates/pumpkin/src/entity/vehicle/minecart/hopper.rs new file mode 100644 index 000000000..564524303 --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/hopper.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::block::entities::hopper::HopperBlockEntity; +use crate::entity::{Entity, player::Player}; +use pumpkin_data::translation; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::boundingbox::BoundingBox; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::text::TextComponent; + +use super::container::{self, MinecartInventory}; + +pub(super) struct HopperMinecart { + enabled: AtomicBool, + inventory: Arc, +} + +impl HopperMinecart { + pub(super) fn new() -> Self { + Self { + enabled: AtomicBool::new(true), + inventory: Arc::new(MinecartInventory::new(5)), + } + } + + pub(super) const fn inventory(&self) -> &Arc { + &self.inventory + } + + pub(super) fn set_enabled(&self, enabled: bool) { + self.enabled.store(enabled, Ordering::Relaxed); + } + + pub(super) async fn tick(&self, entity: &Entity) { + if !self.enabled.load(Ordering::Relaxed) { + return; + } + + let world = entity.world.load(); + let pos = entity.pos.load(); + let source_pos = BlockPos::floored(pos.x, pos.y + 1.5, pos.z); + if let Some(block_entity) = world.get_block_entity(&source_pos) + && let Some(source) = block_entity.get_inventory() + { + for slot in 0..source.size() { + let stack = source.get_stack(slot).await; + if stack.is_empty() + || !source.can_transfer_to(self.inventory.as_ref(), slot, &stack) + { + continue; + } + let backup = stack.clone(); + let one = source.remove_stack_specific(slot, 1).await; + if HopperBlockEntity::add_one_item(source.as_ref(), self.inventory.as_ref(), one) + .await + { + return; + } + source.set_stack(slot, backup).await; + } + return; + } + + let suction_box = BoundingBox::new( + Vector3::new(pos.x - 0.5, pos.y + 0.6875, pos.z - 0.5), + Vector3::new(pos.x + 0.5, pos.y + 2.0, pos.z + 0.5), + ); + if self.pick_up_item(entity, &suction_box).await { + return; + } + let cart_box = entity.bounding_box.load().expand(0.25, 0.0, 0.25); + self.pick_up_item(entity, &cart_box).await; + } + + async fn pick_up_item(&self, entity: &Entity, search_box: &BoundingBox) -> bool { + let world = entity.world.load(); + for entity in world.get_entities_at_box(search_box) { + let Some(item) = entity.get_item_entity() else { + continue; + }; + let mut stack = item.get_item_stack().lock().await; + if stack.is_empty() { + continue; + } + let backup = stack.clone(); + let one = stack.split(1); + if HopperBlockEntity::add_one_item( + self.inventory.as_ref(), + self.inventory.as_ref(), + one, + ) + .await + { + if stack.is_empty() { + item.get_entity().remove().await; + } + return true; + } + *stack = backup; + } + false + } + + pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { + container::open( + entity, + player, + &self.inventory, + TextComponent::translate_cross( + translation::java::ENTITY_MINECRAFT_HOPPER_MINECART, + translation::bedrock::ENTITY_HOPPER_MINECART_NAME, + [], + ), + true, + ) + .await + } + + pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { + self.inventory.write_nbt(nbt).await; + nbt.put_bool("Enabled", self.enabled.load(Ordering::Relaxed)); + } + + pub(super) async fn read_nbt(&self, nbt: &NbtCompound) { + self.inventory.read_nbt(nbt).await; + self.enabled + .store(nbt.get_bool("Enabled").unwrap_or(true), Ordering::Relaxed); + } +} + +#[cfg(test)] +mod tests { + use super::HopperMinecart; + use pumpkin_world::inventory::Inventory; + + #[test] + fn hopper_minecart_inventory_has_five_slots() { + assert_eq!(HopperMinecart::new().inventory.size(), 5); + } +} diff --git a/crates/pumpkin/src/entity/vehicle/minecart/rideable.rs b/crates/pumpkin/src/entity/vehicle/minecart/rideable.rs new file mode 100644 index 000000000..d1a78a544 --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/rideable.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use crate::entity::{Entity, EntityBase, player::Player}; + +pub(super) struct RideableMinecart; + +impl RideableMinecart { + pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { + if player.get_entity().is_sneaking() + || !entity.passengers.lock().await.is_empty() + || player.get_entity().has_vehicle().await + { + return false; + } + + let world = entity.world.load(); + let Some(vehicle) = world.get_entity_by_id(entity.entity_id) else { + return false; + }; + let Some(passenger) = world.get_player_by_id(player.entity_id()) else { + return false; + }; + + entity + .add_passenger(vehicle, passenger as Arc) + .await; + true + } +} diff --git a/crates/pumpkin/src/entity/vehicle/minecart/tnt.rs b/crates/pumpkin/src/entity/vehicle/minecart/tnt.rs new file mode 100644 index 000000000..2d8150bc2 --- /dev/null +++ b/crates/pumpkin/src/entity/vehicle/minecart/tnt.rs @@ -0,0 +1,157 @@ +use std::sync::atomic::{AtomicI32, Ordering}; + +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityStatus; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::bedrock::server::actor_event::ActorEventType; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +use crate::entity::Entity; + +pub(super) struct TntMinecart { + fuse: AtomicI32, + explosion_power: AtomicCell, + explosion_speed_factor: AtomicCell, +} + +impl TntMinecart { + pub(super) const fn new() -> Self { + Self { + fuse: AtomicI32::new(-1), + explosion_power: AtomicCell::new(4.0), + explosion_speed_factor: AtomicCell::new(1.0), + } + } + + pub(super) fn prime(&self, entity: &Entity, fuse: i32) { + if self.fuse.load(Ordering::Relaxed) >= 0 + || !entity + .world + .load() + .level_info + .load() + .game_rules + .tnt_explodes + { + return; + } + + self.fuse.store(fuse, Ordering::Relaxed); + let world = entity.world.load(); + world.send_entity_status( + entity, + EntityStatus::TntPrime, + Some(ActorEventType::CartWithPrimeTNT), + ); + world.play_sound( + Sound::EntityTntPrimed, + SoundCategory::Blocks, + &entity.pos.load(), + ); + } + + pub(super) async fn tick(&self, entity: &Entity) -> bool { + let fuse = self.fuse.load(Ordering::Relaxed); + if fuse > 0 { + self.fuse.store(fuse - 1, Ordering::Relaxed); + let mut smoke_pos = entity.pos.load(); + smoke_pos.y += 0.5; + entity.world.load().spawn_particle( + smoke_pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + Particle::Smoke, + ); + } else if fuse == 0 { + let velocity = entity.velocity.load(); + self.explode( + entity, + velocity.x.mul_add(velocity.x, velocity.z * velocity.z), + ) + .await; + return true; + } + false + } + + pub(super) async fn explode(&self, entity: &Entity, horizontal_speed_squared: f64) { + let world = entity.world.load(); + if !world.level_info.load().game_rules.tnt_explodes { + if self.fuse.load(Ordering::Relaxed) > -1 { + entity.remove().await; + } + return; + } + + let power = Self::explosion_strength( + self.explosion_power.load(), + self.explosion_speed_factor.load(), + horizontal_speed_squared, + rand::rng().random_range(0.0..1.0), + ); + let pos = entity.pos.load(); + let primed = self.fuse.load(Ordering::Relaxed) > -1; + entity.remove().await; + if primed { + world.explode_tnt_minecart(pos, power).await; + } else { + world.explode(pos, power).await; + } + } + + pub(super) fn set_fuse(&self, fuse: i32) { + self.fuse.store(fuse, Ordering::Relaxed); + } + + pub(super) fn write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_int("fuse", self.fuse.load(Ordering::Relaxed)); + let power = self.explosion_power.load(); + if power != 4.0 { + nbt.put_float("explosion_power", power); + } + let speed_factor = self.explosion_speed_factor.load(); + if speed_factor != 1.0 { + nbt.put_float("explosion_speed_factor", speed_factor); + } + } + + pub(super) fn read_nbt(&self, nbt: &NbtCompound) { + self.fuse + .store(nbt.get_int("fuse").unwrap_or(-1), Ordering::Relaxed); + self.explosion_power.store( + nbt.get_float("explosion_power") + .unwrap_or(4.0) + .clamp(0.0, 128.0), + ); + self.explosion_speed_factor.store( + nbt.get_float("explosion_speed_factor") + .unwrap_or(1.0) + .clamp(0.0, 128.0), + ); + } + + fn explosion_strength( + base: f32, + speed_factor: f32, + horizontal_speed_squared: f64, + random: f32, + ) -> f32 { + let speed = horizontal_speed_squared.sqrt().min(5.0) as f32; + base + speed_factor * random * 1.5 * speed + } +} + +#[cfg(test)] +mod tests { + use super::TntMinecart; + + #[test] + fn tnt_minecart_explosion_bonus_is_speed_capped() { + assert_eq!(TntMinecart::explosion_strength(4.0, 1.0, 100.0, 1.0), 11.5); + assert_eq!(TntMinecart::explosion_strength(4.0, 1.0, 100.0, 0.0), 4.0); + } +} diff --git a/crates/pumpkin/src/entity/vehicle/vehicle.rs b/crates/pumpkin/src/entity/vehicle/vehicle.rs index 142306587..09c4ff93e 100644 --- a/crates/pumpkin/src/entity/vehicle/vehicle.rs +++ b/crates/pumpkin/src/entity/vehicle/vehicle.rs @@ -8,6 +8,7 @@ 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_protocol::codec::var_int::VarInt; use pumpkin_util::GameMode; pub struct VehicleEntity { @@ -67,24 +68,41 @@ impl VehicleEntity { self.entity.send_meta_data( &[ Metadata::new( - TrackedData::ID_HURT, + TrackedData::DAMAGE_WOBBLE_TICKS, MetaDataType::INTEGER, - self.get_hurt_time(), + VarInt(self.get_hurt_time()), + ), + Metadata::new( + TrackedData::DAMAGE_WOBBLE_SIDE, + MetaDataType::INTEGER, + VarInt(self.get_hurt_dir()), + ), + Metadata::new( + TrackedData::ID_HURT, + MetaDataType::INT, + VarInt(self.get_hurt_time()), ), Metadata::new( TrackedData::ID_HURTDIR, - MetaDataType::INTEGER, - self.get_hurt_dir(), + MetaDataType::INT, + VarInt(self.get_hurt_dir()), ), ], None, ); self.entity.send_meta_data( - &[Metadata::new( - TrackedData::ID_DAMAGE, - MetaDataType::FLOAT, - self.get_damage(), - )], + &[ + Metadata::new( + TrackedData::DAMAGE_WOBBLE_STRENGTH, + MetaDataType::FLOAT, + self.get_damage(), + ), + Metadata::new( + TrackedData::ID_DAMAGE, + MetaDataType::FLOAT, + self.get_damage(), + ), + ], None, ); } @@ -116,16 +134,7 @@ impl VehicleEntity { 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 new_strength = self.apply_damage_wobble(amount); let is_creative = source .and_then(|s| s.get_player()) @@ -141,4 +150,64 @@ impl VehicleEntity { true } + + /// Applies the standard minecart damage wobble without destroying the vehicle. + /// TNT minecarts use this before deciding whether damage primes or breaks them. + pub fn apply_damage_wobble(&self, amount: f32) -> f32 { + 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(); + new_strength + } +} + +#[cfg(test)] +mod tests { + 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::version::JavaMinecraftVersion; + + fn wobble_integer_metadata(version: JavaMinecraftVersion) -> Vec { + let mut bytes = Vec::new(); + for metadata in [ + Metadata::new( + TrackedData::DAMAGE_WOBBLE_TICKS, + MetaDataType::INTEGER, + VarInt(10), + ), + Metadata::new( + TrackedData::DAMAGE_WOBBLE_SIDE, + MetaDataType::INTEGER, + VarInt(-1), + ), + Metadata::new(TrackedData::ID_HURT, MetaDataType::INT, VarInt(10)), + Metadata::new(TrackedData::ID_HURTDIR, MetaDataType::INT, VarInt(-1)), + ] { + metadata.write(&mut bytes, &version).unwrap(); + } + bytes + } + + #[test] + fn wobble_integers_serialize_for_legacy_and_current_clients() { + let expected = vec![8, 1, 10, 9, 1, 0xff, 0xff, 0xff, 0xff, 0x0f]; + + assert_eq!( + wobble_integer_metadata(JavaMinecraftVersion::V_1_21_11), + expected + ); + assert_eq!( + wobble_integer_metadata(JavaMinecraftVersion::V_26_2), + expected + ); + } } diff --git a/crates/pumpkin/src/item/items/minecart.rs b/crates/pumpkin/src/item/items/minecart.rs index 20ab9589f..c1b12df89 100644 --- a/crates/pumpkin/src/item/items/minecart.rs +++ b/crates/pumpkin/src/item/items/minecart.rs @@ -88,6 +88,7 @@ impl ItemBehaviour for MinecartItem { ); let minecart_entity = Arc::new(MinecartEntity::new(entity)); world.spawn_entity(minecart_entity).await; + item.decrement_unless_creative(player.gamemode.load(), 1); }) } diff --git a/crates/pumpkin/src/world/explosion.rs b/crates/pumpkin/src/world/explosion.rs index a5e68fae0..19317b671 100644 --- a/crates/pumpkin/src/world/explosion.rs +++ b/crates/pumpkin/src/world/explosion.rs @@ -18,12 +18,34 @@ use super::{BlockFlags, World}; pub struct Explosion { power: f32, pos: Vector3, + preserve_rails: bool, } impl Explosion { #[must_use] pub const fn new(power: f32, pos: Vector3) -> Self { - Self { power, pos } + Self { + power, + pos, + preserve_rails: false, + } + } + + #[must_use] + pub const fn preserving_rails(mut self) -> Self { + self.preserve_rails = true; + self + } + + fn protects_rail(&self, world: &World, pos: &BlockPos, block: &Block) -> bool { + self.preserve_rails && (Self::is_rail(block) || Self::is_rail(world.get_block(&pos.up()))) + } + + fn is_rail(block: &Block) -> bool { + block.id == Block::RAIL.id + || block.id == Block::POWERED_RAIL.id + || block.id == Block::DETECTOR_RAIL.id + || block.id == Block::ACTIVATOR_RAIL.id } fn get_blocks_to_destroy( @@ -113,12 +135,16 @@ impl Explosion { ); if !state.is_air() || !fluid_state.is_empty { - let resistance = - fluid_state.blast_resistance.max(block.blast_resistance); + let protects_rail = self.protects_rail(world, &block_pos, block); + let resistance = if protects_rail { + 0.0 + } else { + fluid_state.blast_resistance.max(block.blast_resistance) + }; h -= (resistance + 0.3) * 0.3; - if h > 0.0 { + if h > 0.0 && !protects_rail { map.insert(block_pos, (block, state)); } } @@ -307,3 +333,22 @@ impl Explosion { blocks.len() as u32 } } + +#[cfg(test)] +mod tests { + use super::Explosion; + use pumpkin_data::Block; + + #[test] + fn tnt_minecart_rail_protection_covers_every_rail_type() { + for rail in [ + &Block::RAIL, + &Block::POWERED_RAIL, + &Block::DETECTOR_RAIL, + &Block::ACTIVATOR_RAIL, + ] { + assert!(Explosion::is_rail(rail)); + } + assert!(!Explosion::is_rail(&Block::STONE)); + } +} diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index 55c0db89e..af65863f5 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -35,7 +35,7 @@ use crate::{ {OnNeighborUpdateArgs, OnScheduledTickArgs}, }, command::client_suggestions, - entity::{Entity, EntityBase, NBTStorage, player::Player, r#type::from_type}, + entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type}, error::PumpkinError, net::{ClientPlatform, java::JavaClient}, plugin::{ @@ -400,7 +400,11 @@ impl World { /// currently in; the chunk is rewritten from scratch every unload cycle, so /// there is nothing stale to deduplicate. async fn save_entity(&self, entity: &Arc) { - let current_chunk = entity.get_entity().block_pos.load().chunk_position(); + let base_entity = entity.get_entity(); + if base_entity.is_removed() { + return; + } + let current_chunk = base_entity.block_pos.load().chunk_position(); let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; let chunk = self.level.get_entity_chunk(current_chunk).await; @@ -3357,6 +3361,20 @@ impl World { pub async fn explode(self: &Arc, position: Vector3, power: f32) { let explosion = Explosion::new(power, position); + self.run_explosion(explosion, position, power).await; + } + + pub async fn explode_tnt_minecart(self: &Arc, position: Vector3, power: f32) { + let explosion = Explosion::new(power, position).preserving_rails(); + self.run_explosion(explosion, position, power).await; + } + + async fn run_explosion( + self: &Arc, + explosion: Explosion, + position: Vector3, + power: f32, + ) { let block_count = explosion.explode(self).await; let particle = if power < 2.0 { Particle::Explosion @@ -3785,6 +3803,7 @@ impl World { base_entity.velocity.store(Vector3::default()); player.client.enqueue_spawn_packet(&entity).await; + player.try_restore_vehicle(&entity).await; entities_to_add.push(entity); } @@ -3803,6 +3822,7 @@ impl World { let base_entity = entity.get_entity(); if base_entity.chunk_pos.load() == position { player.client.enqueue_spawn_packet(entity).await; + player.try_restore_vehicle(entity).await; } } } @@ -4284,6 +4304,15 @@ impl World { #[allow(clippy::unused_async)] pub async fn remove_entity(&self, entity: &dyn EntityBase) { let base_entity = entity.get_entity(); + if base_entity + .removal_reason + .swap(Some(RemovalReason::Discarded)) + .is_some() + { + return; + } + base_entity.removed.store(true, Ordering::Release); + self.spawn_state.load().remove_entity(self, entity); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone();