diff --git a/pumpkin-protocol/src/bedrock/client/level_sound_event.rs b/pumpkin-protocol/src/bedrock/client/level_sound_event.rs new file mode 100644 index 000000000..fc3158967 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/level_sound_event.rs @@ -0,0 +1,22 @@ +use pumpkin_macros::packet; +use pumpkin_util::math::vector3::Vector3; + +use crate::{ + codec::{var_int::VarInt, var_uint::VarUInt}, + serial::PacketWrite, +}; + +/// Sent by the server to spawn a visual particle effect at a specific 3D location in the world. +/// +/// Packet ID: `27` +/// Ref: +#[derive(PacketWrite)] +#[packet(27)] +pub struct CLevelSoundEvent { + pub sound_id: VarUInt, + pub position: Vector3, + pub extra_data: VarInt, + pub entity_type: String, + pub is_baby_mob: bool, + pub is_global: bool, +} diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index f96f287b2..a82548b44 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -49,6 +49,9 @@ pub mod update_abilities; pub mod update_attributes; pub mod update_block; +pub mod level_sound_event; +pub mod set_actor_link; + pub use add_actor::*; pub use add_item_actor::*; pub use add_player::*; @@ -70,6 +73,7 @@ pub use item_registry::*; pub use item_stack_response::*; pub use level_chunk::*; pub use level_event::*; +pub use level_sound_event::*; pub use modal_form_request::*; pub use move_actor_absolute::*; pub use move_actor_delta::*; @@ -85,6 +89,7 @@ pub use resource_pack_stack::*; pub use resource_packs_info::*; pub use scoreboard::*; pub use set_actor_data::*; +pub use set_actor_link::*; pub use set_actor_motion::*; pub use set_difficulty::*; pub use set_health::*; diff --git a/pumpkin-protocol/src/bedrock/client/raknet/connection.rs b/pumpkin-protocol/src/bedrock/client/raknet/connection.rs index 2ff276a61..cb85cf598 100644 --- a/pumpkin-protocol/src/bedrock/client/raknet/connection.rs +++ b/pumpkin-protocol/src/bedrock/client/raknet/connection.rs @@ -3,6 +3,9 @@ use std::net::SocketAddr; use pumpkin_macros::packet; use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite}; +/// Sent in response to a `ConnectedPing` (`0x00`) to calculate round-trip latency and synchronize time across an established connection. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x03)] pub struct CConnectedPong { @@ -18,6 +21,9 @@ impl CConnectedPong { } } +/// Sent by the server to accept an incoming `ConnectionRequest` (`0x09`), confirming connection parameters and system addresses. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x10)] pub struct CConnectionRequestAccepted { @@ -47,6 +53,9 @@ impl CConnectionRequestAccepted { } } +/// Sent by the server when a client attempts to connect while already being connected. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x12)] pub struct CAlreadyConnected { @@ -64,6 +73,9 @@ impl CAlreadyConnected { } } +/// Sent by the server when it has reached its maximum connection capacity. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x14)] pub struct CNoFreeIncomingConnections { @@ -81,6 +93,9 @@ impl CNoFreeIncomingConnections { } } +/// Sent by the server when a client attempts to connect from a banned IP address or identifier. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x17)] pub struct CConnectionBanned { @@ -98,6 +113,9 @@ impl CConnectionBanned { } } +/// Sent by the server when a client attempts to connect again too quickly after disconnecting. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x1A)] pub struct CIpRecentlyConnected { @@ -115,6 +133,9 @@ impl CIpRecentlyConnected { } } +/// Sent by the server to initiate graceful termination of the connection session. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x15)] pub struct CDisconnect; diff --git a/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs b/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs index 6c1f3503c..8af478cb9 100644 --- a/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs +++ b/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs @@ -1,6 +1,9 @@ use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite}; use pumpkin_macros::packet; +/// Sent by the server when the client's `RakNet` protocol version does not match the server's expected protocol version (`11`). +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x19)] pub struct CIncompatibleProtocolVersion { diff --git a/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs b/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs index cd4c6ee36..7a22d5fcd 100644 --- a/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs +++ b/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs @@ -4,6 +4,9 @@ use pumpkin_macros::packet; use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite}; +/// Sent by the server in response to `OpenConnectionRequest1` (`0x05`), negotiating security options, server GUID, and MTU. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x06)] pub struct COpenConnectionReply1 { @@ -28,6 +31,9 @@ impl COpenConnectionReply1 { } } +/// Sent by the server in response to `OpenConnectionRequest2` (`0x07`), confirming the connection setup and client address before establishing session state. +/// +/// Ref: #[derive(PacketWrite)] #[packet(0x08)] pub struct COpenConnectionReply2 { diff --git a/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs b/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs index a8ad14f5e..937d58f17 100644 --- a/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs +++ b/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs @@ -5,6 +5,9 @@ use pumpkin_macros::packet; use crate::serial::PacketWrite; +/// Sent by the server in response to an `UnconnectedPing` (`0x01` / `0x02`), containing server metadata (MOTD, protocol version, player counts, edition). +/// +/// Ref: #[packet(0x1c)] pub struct CUnconnectedPong { time: u64, diff --git a/pumpkin-protocol/src/bedrock/client/set_actor_link.rs b/pumpkin-protocol/src/bedrock/client/set_actor_link.rs new file mode 100644 index 000000000..49a79da64 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/set_actor_link.rs @@ -0,0 +1,13 @@ +use pumpkin_macros::packet; + +use crate::{bedrock::client::common::EntityLink, serial::PacketWrite}; + +/// Sent by the server to set the entity an actor is riding or to unmount an actor. +/// +/// Packet ID: `41` +/// Ref: +#[derive(PacketWrite)] +#[packet(41)] +pub struct CSetActorLink { + pub link: EntityLink, +} diff --git a/pumpkin-protocol/src/bedrock/server/mod.rs b/pumpkin-protocol/src/bedrock/server/mod.rs index be10b6f8d..f109434de 100644 --- a/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/pumpkin-protocol/src/bedrock/server/mod.rs @@ -16,6 +16,7 @@ pub mod mob_equipment; pub mod modal_form_response; pub mod player_action; pub mod player_auth_input; +pub mod player_hotbar; pub mod raknet; pub mod request_ability; pub mod request_chunk_radius; @@ -43,6 +44,7 @@ pub use mob_equipment::*; pub use modal_form_response::*; pub use player_action::{Action as PlayerActionType, SPlayerAction}; pub use player_auth_input::*; +pub use player_hotbar::*; pub use raknet::*; pub use request_ability::*; pub use request_chunk_radius::*; diff --git a/pumpkin-protocol/src/bedrock/server/player_hotbar.rs b/pumpkin-protocol/src/bedrock/server/player_hotbar.rs new file mode 100644 index 000000000..bc30ae036 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/server/player_hotbar.rs @@ -0,0 +1,15 @@ +use pumpkin_macros::packet; + +use crate::serial::PacketRead; + +/// Sent by the Bedrock client when the player changes their active hotbar slot. +/// +/// Packet ID: `48` +/// Ref: +#[derive(PacketRead)] +#[packet(48)] +pub struct SPlayerHotbar { + pub selected_slot: u32, + pub container_id: u8, + pub select_slot: bool, +} diff --git a/pumpkin-protocol/src/bedrock/server/raknet/connection.rs b/pumpkin-protocol/src/bedrock/server/raknet/connection.rs index 52b212a4c..1b1ee2cbd 100644 --- a/pumpkin-protocol/src/bedrock/server/raknet/connection.rs +++ b/pumpkin-protocol/src/bedrock/server/raknet/connection.rs @@ -4,6 +4,9 @@ use pumpkin_macros::packet; use crate::serial::PacketRead; +/// Sent periodically by a connected client to measure round-trip time. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x00)] pub struct SConnectedPing { @@ -12,6 +15,9 @@ pub struct SConnectedPing { pub time: u64, } +/// Sent by the client after receiving `OpenConnectionReply2` to request formal session establishment. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x09)] pub struct SConnectionRequest { @@ -22,6 +28,9 @@ pub struct SConnectionRequest { pub security: bool, } +/// Sent by the client to confirm local network address and finish connection establishment. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x13)] pub struct SNewIncomingConnection { @@ -33,8 +42,14 @@ pub struct SNewIncomingConnection { pub pong_time: u64, } +/// Sent by the client to notify the server of graceful disconnection. +/// +/// Ref: #[packet(0x15)] pub struct SDisconnect; +/// Internal notification signal for a connection lost due to socket error or timeout. +/// +/// Ref: #[packet(0x16)] pub struct SConnectionLost; diff --git a/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs b/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs index 400a63417..6927d6c66 100644 --- a/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs +++ b/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs @@ -4,9 +4,11 @@ use pumpkin_macros::packet; use crate::serial::PacketRead; +/// Sent by a connecting client to initiate `RakNet` handshake and check server MTU size. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x05)] -/// The client sends this when attempting to join the server pub struct SOpenConnectionRequest1 { pub magic: [u8; 16], pub protocol_version: u8, @@ -14,6 +16,9 @@ pub struct SOpenConnectionRequest1 { pub mtu: u16, } +/// Sent by a connecting client following `OpenConnectionReply1` to verify server address, client GUID, and MTU. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x07)] pub struct SOpenConnectionRequest2 { diff --git a/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs b/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs index 9da8ccce0..3429bb6e3 100644 --- a/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs +++ b/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs @@ -2,9 +2,11 @@ use pumpkin_macros::packet; use crate::serial::PacketRead; +/// Sent by an unconnected client to request server information, status, and MOTD. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x01)] -/// Used to request Server information like MOTD pub struct SUnconnectedPing { #[serial(big_endian)] pub time: u64, @@ -13,9 +15,11 @@ pub struct SUnconnectedPing { pub client_guid: u64, } +/// Sent by a client to query server information when connections are open. +/// +/// Ref: #[derive(PacketRead)] #[packet(0x02)] -/// Used to request Server information like MOTD when connection is open? pub struct SUnconnectedPingOpenConnections { #[serial(big_endian)] pub time: u64, diff --git a/pumpkin-protocol/src/java/client/play/mod.rs b/pumpkin-protocol/src/java/client/play/mod.rs index 781f876e0..560ba68e1 100644 --- a/pumpkin-protocol/src/java/client/play/mod.rs +++ b/pumpkin-protocol/src/java/client/play/mod.rs @@ -182,7 +182,10 @@ pub use set_container_content::*; pub use set_container_property::*; pub use set_container_slot::*; pub use set_cursor_slot::*; +mod set_entity_link; +pub use set_entity_link::*; pub use set_equipment::*; + pub use set_experience::*; pub use set_health::*; pub use set_held_item::*; diff --git a/pumpkin-protocol/src/java/client/play/set_entity_link.rs b/pumpkin-protocol/src/java/client/play/set_entity_link.rs new file mode 100644 index 000000000..8f9305222 --- /dev/null +++ b/pumpkin-protocol/src/java/client/play/set_entity_link.rs @@ -0,0 +1,34 @@ +use pumpkin_data::packet::clientbound::PLAY_SET_ENTITY_LINK; +use pumpkin_macros::java_packet; +use pumpkin_util::version::JavaMinecraftVersion; + +use crate::{ClientPacket, ser::NetworkWriteExt}; + +/// Sent by the server to attach (leash) or detach an entity to another entity (e.g. leash knot, player, mob). +#[java_packet(PLAY_SET_ENTITY_LINK)] +pub struct CSetEntityLink { + pub attached_entity_id: i32, + pub holding_entity_id: i32, +} + +impl CSetEntityLink { + #[must_use] + pub const fn new(attached_entity_id: i32, holding_entity_id: i32) -> Self { + Self { + attached_entity_id, + holding_entity_id, + } + } +} + +impl ClientPacket for CSetEntityLink { + fn write_packet_data( + &self, + mut write: impl std::io::Write, + _version: &JavaMinecraftVersion, + ) -> Result<(), crate::ser::WritingError> { + write.write_i32_be(self.attached_entity_id)?; + write.write_i32_be(self.holding_entity_id)?; + Ok(()) + } +} diff --git a/pumpkin/src/entity/decoration/leash_knot.rs b/pumpkin/src/entity/decoration/leash_knot.rs new file mode 100644 index 000000000..242dfd21b --- /dev/null +++ b/pumpkin/src/entity/decoration/leash_knot.rs @@ -0,0 +1,216 @@ +use crate::entity::player::Player; +use crate::entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity}; +use crate::world::World; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::Taggable; +use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use std::sync::Arc; + +pub struct LeashKnotEntity { + entity: Entity, + pos: BlockPos, +} + +impl LeashKnotEntity { + pub const OFFSET_Y: f64 = 0.375; + + pub const fn new(entity: Entity, pos: BlockPos) -> Self { + Self { entity, pos } + } + + pub const fn block_pos(&self) -> BlockPos { + self.pos + } + + pub async fn get_or_create(world: &Arc, pos: BlockPos) -> Arc { + if let Some(existing) = Self::get_knot(world, pos) { + return existing; + } + Self::create_knot(world, pos).await + } + + pub fn get_knot(world: &Arc, pos: BlockPos) -> Option> { + let center = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + Self::OFFSET_Y, + f64::from(pos.0.z) + 0.5, + ); + + let search_dim = EntityDimensions { + width: 2.0, + height: 2.0, + eye_height: 1.0, + }; + + let search_box = BoundingBox::new_from_pos(center.x, center.y, center.z, &search_dim); + let entities = world.get_entities_at_box(&search_box); + + for entity_base in entities { + if entity_base.get_entity().entity_type == &EntityType::LEASH_KNOT + && let Some(knot) = entity_base.cast_any().downcast_ref::>() + && knot.pos == pos + { + return Some(knot.clone()); + } + } + None + } + + pub async fn create_knot(world: &Arc, pos: BlockPos) -> Arc { + let raw_pos = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + Self::OFFSET_Y, + f64::from(pos.0.z) + 0.5, + ); + + let entity = Entity::new(world.clone(), raw_pos, &EntityType::LEASH_KNOT); + let knot = Arc::new(Self::new(entity, pos)); + world + .spawn_entity(knot.clone() as Arc) + .await; + + world.play_sound(Sound::ItemLeadTied, SoundCategory::Neutral, &raw_pos); + + knot + } + + pub fn play_placement_sound(&self, world: &World) { + let pos = self.entity.pos.load(); + world.play_sound(Sound::ItemLeadTied, SoundCategory::Neutral, &pos); + } +} + +impl NBTStorage for LeashKnotEntity {} + +impl EntityBase for LeashKnotEntity { + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a crate::server::Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let world = self.entity.world.load(); + let block = world.get_block(&self.pos); + if !block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_FENCES) { + let knot_id = self.entity.entity_id; + let search_dim = EntityDimensions { + width: 32.0, + height: 32.0, + eye_height: 16.0, + }; + let pos = self.entity.pos.load(); + let search_box = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &search_dim); + let entities = world.get_entities_at_box(&search_box); + + for entity_base in entities { + let ent = entity_base.get_entity(); + let is_attached_to_knot = ent + .leashed_to + .try_lock() + .ok() + .and_then(|guard| { + guard + .as_ref() + .map(|holder| holder.get_entity().entity_id == knot_id) + }) + .unwrap_or(false); + + if is_attached_to_knot { + ent.unleash().await; + let lead_item = pumpkin_data::item_stack::ItemStack::new( + 1, + &pumpkin_data::item::Item::LEAD, + ); + world.drop_stack(&ent.block_pos.load(), lead_item).await; + } + } + + world.play_sound(Sound::ItemLeadUntied, SoundCategory::Neutral, &pos); + + self.entity.remove().await; + } + }) + } + + fn interact<'a>( + &'a self, + player: &'a Arc, + _item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let world = player.world(); + let knot_id = self.entity.entity_id; + let player_id = player.entity_id(); + + let search_dim = EntityDimensions { + width: 32.0, + height: 32.0, + eye_height: 16.0, + }; + let pos = self.entity.pos.load(); + let search_box = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &search_dim); + let entities = world.get_entities_at_box(&search_box); + + let mut attached_mob = false; + let mut player_leashed_mobs = Vec::new(); + + for entity_base in &entities { + let ent = entity_base.get_entity(); + if let Ok(guard) = ent.leashed_to.try_lock() + && let Some(holder) = guard.as_ref() + && holder.get_entity().entity_id == player_id + { + player_leashed_mobs.push(ent); + } + } + + if let Some(self_knot) = Self::get_knot(&world, self.pos) { + for mob in player_leashed_mobs { + mob.leash_to(self_knot.clone() as Arc).await; + attached_mob = true; + } + } + + let mut any_dropped = false; + if !attached_mob { + for entity_base in &entities { + let ent = entity_base.get_entity(); + if let Ok(guard) = ent.leashed_to.try_lock() + && let Some(holder) = guard.as_ref() + && holder.get_entity().entity_id == knot_id + { + ent.leash_to(player.clone() as Arc).await; + any_dropped = true; + } + } + } + + if attached_mob || any_dropped { + self.play_placement_sound(&world); + true + } else { + false + } + }) + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/entity/decoration/mod.rs b/pumpkin/src/entity/decoration/mod.rs index 7ef7e02ae..debec4e84 100644 --- a/pumpkin/src/entity/decoration/mod.rs +++ b/pumpkin/src/entity/decoration/mod.rs @@ -1,3 +1,4 @@ pub mod armor_stand; pub mod end_crystal; +pub mod leash_knot; pub mod painting; diff --git a/pumpkin/src/entity/mob/creeper.rs b/pumpkin/src/entity/mob/creeper.rs index 4d2673152..470fd1211 100644 --- a/pumpkin/src/entity/mob/creeper.rs +++ b/pumpkin/src/entity/mob/creeper.rs @@ -203,7 +203,7 @@ impl Mob for CreeperEntity { ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { if item_stack.item.id != Item::FLINT_AND_STEEL.id { - return false; + return self.mob_entity.mob_interact(player, item_stack).await; } let entity = &self.mob_entity.living_entity.entity; diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index ae84377fb..2960a6616 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -389,6 +389,45 @@ impl MobEntity { let entity = &self.living_entity.entity; entity.set_on_fire_for(8.0); } + + pub async fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let entity = &self.living_entity.entity; + + // If already leashed to player, right-clicking unleashes the mob + let currently_leashed = { + let guard = entity.leashed_to.lock().await; + guard.is_some() + }; + + if currently_leashed { + entity.unleash().await; + let lead_item = + pumpkin_data::item_stack::ItemStack::new(1, &pumpkin_data::item::Item::LEAD); + entity + .world + .load() + .drop_stack(&entity.block_pos.load(), lead_item) + .await; + return true; + } + + // If holding a lead, leash the mob to the player + if item_stack.item.registry_key == "lead" + || item_stack.item.registry_key == "minecraft:lead" + { + let diff = entity.pos.load() - player.get_entity().pos.load(); + let dist_sq = diff.length_squared(); + if dist_sq <= Entity::LEASH_SNAP_DISTANCE * Entity::LEASH_SNAP_DISTANCE { + entity.leash_to(player.clone() as Arc).await; + if player.gamemode.load() != pumpkin_util::GameMode::Creative { + item_stack.decrement(1); + } + return true; + } + } + + false + } } pub trait Mob: EntityBase + Send + Sync { @@ -479,10 +518,10 @@ pub trait Mob: EntityBase + Send + Sync { fn mob_interact<'a>( &'a self, - _player: &'a Arc, - _item_stack: &'a mut ItemStack, + player: &'a Arc, + item_stack: &'a mut ItemStack, ) -> EntityBaseFuture<'a, bool> { - Box::pin(async { false }) + Box::pin(async move { self.get_mob_entity().mob_interact(player, item_stack).await }) } fn mob_player_collision<'a>(&'a self, _player: &'a Arc) -> EntityBaseFuture<'a, ()> { @@ -554,6 +593,7 @@ impl EntityBase for T { ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { let mob_entity = self.get_mob_entity(); + mob_entity.living_entity.entity.tick_leash().await; mob_entity.tick_sun_burn().await; if mob_entity.breeding_cooldown.load(Relaxed) > 0 { diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 6f38fcc4a..c457611ab 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -787,6 +787,8 @@ pub struct Entity { pub passengers: Mutex>>, /// The vehicle that entity is in pub vehicle: Mutex>>, + /// The entity this entity is attached/leashed to (if any) + pub leashed_to: Mutex>>, /// Cooldown before entity can mount again after dismounting pub riding_cooldown: AtomicI32, /// The age of the entity in ticks. Negative values indicate a baby. @@ -930,6 +932,8 @@ impl Entity { removal_reason: AtomicCell::new(None), passengers: Mutex::new(Vec::new()), vehicle: Mutex::new(None), + leashed_to: Mutex::new(None), + riding_cooldown: AtomicI32::new(0), age: AtomicI32::new(0), current_biome: ArcSwap::new(Arc::new(&Biome::PLAINS)), @@ -3013,6 +3017,103 @@ impl Entity { !self.is_removed() } + pub const LEASH_SNAP_DISTANCE: f64 = 12.0; + pub const LEASH_ELASTIC_DISTANCE: f64 = 6.0; + + pub async fn leash_to(&self, holder: Arc) { + let holder_entity = holder.get_entity(); + *self.leashed_to.lock().await = Some(holder.clone()); + + let je_packet = pumpkin_protocol::java::client::play::CSetEntityLink::new( + self.entity_id, + holder_entity.entity_id, + ); + let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink { + link: pumpkin_protocol::bedrock::client::common::EntityLink { + ridden_unique_id: pumpkin_protocol::codec::var_long::VarLong(self.entity_id as i64), + rider_unique_id: pumpkin_protocol::codec::var_long::VarLong( + holder_entity.entity_id as i64, + ), + link_type: 1, // Leash link + immediate: true, + rider_initiated: false, + vehicle_angular_velocity: 0.0, + }, + }; + + self.world.load().broadcast_to_chunk_editioned_sync( + self.chunk_pos.load(), + &je_packet, + &be_packet, + ); + } + + pub async fn unleash(&self) { + let old_holder = self.leashed_to.lock().await.take(); + if old_holder.is_none() { + return; + } + + let je_packet = + pumpkin_protocol::java::client::play::CSetEntityLink::new(self.entity_id, -1); + let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink { + link: pumpkin_protocol::bedrock::client::common::EntityLink { + ridden_unique_id: pumpkin_protocol::codec::var_long::VarLong(self.entity_id as i64), + rider_unique_id: pumpkin_protocol::codec::var_long::VarLong(-1), + link_type: 0, // Unlink + immediate: true, + rider_initiated: false, + vehicle_angular_velocity: 0.0, + }, + }; + + self.world.load().broadcast_to_chunk_editioned_sync( + self.chunk_pos.load(), + &je_packet, + &be_packet, + ); + } + + pub async fn tick_leash(&self) { + let holder = { + let guard = self.leashed_to.lock().await; + guard.clone() + }; + + if let Some(holder) = holder { + let holder_entity = holder.get_entity(); + + // Drop leash if entity or holder is removed or dead + if !self.is_alive() || !holder_entity.is_alive() { + self.unleash().await; + return; + } + + let self_pos = self.pos.load(); + let holder_pos = holder_entity.pos.load(); + let diff = self_pos - holder_pos; + let distance = diff.length(); + + if distance > Self::LEASH_SNAP_DISTANCE { + // Too far: snap/break leash and drop lead item + self.unleash().await; + let lead_item = + pumpkin_data::item_stack::ItemStack::new(1, &pumpkin_data::item::Item::LEAD); + self.world + .load() + .drop_stack(&self.block_pos.load(), lead_item) + .await; + } else if distance > Self::LEASH_ELASTIC_DISTANCE { + // Elastic pull force towards leash holder + let dir = (holder_pos - self_pos).normalize(); + let pull_strength = (distance - Self::LEASH_ELASTIC_DISTANCE) * 0.11; + let current_vel = self.velocity.load(); + self.velocity.store(current_vel + dir * pull_strength); + self.velocity_dirty.store(true, Relaxed); + } + } + } + pub async fn has_passengers(&self) -> bool { !self.passengers.lock().await.is_empty() } diff --git a/pumpkin/src/entity/passive/chicken.rs b/pumpkin/src/entity/passive/chicken.rs index a0b18727e..b042643f8 100644 --- a/pumpkin/src/entity/passive/chicken.rs +++ b/pumpkin/src/entity/passive/chicken.rs @@ -197,7 +197,7 @@ impl Mob for ChickenEntity { ); return true; } - false + self.mob_entity.mob_interact(player, item_stack).await }) } } diff --git a/pumpkin/src/entity/passive/cow.rs b/pumpkin/src/entity/passive/cow.rs index dd537a641..848fb2c73 100644 --- a/pumpkin/src/entity/passive/cow.rs +++ b/pumpkin/src/entity/passive/cow.rs @@ -102,7 +102,7 @@ impl Mob for CowEntity { ); return true; } - false + self.mob_entity.mob_interact(player, item_stack).await }) } } diff --git a/pumpkin/src/entity/passive/pig.rs b/pumpkin/src/entity/passive/pig.rs index 4fbaf337c..a3008d649 100644 --- a/pumpkin/src/entity/passive/pig.rs +++ b/pumpkin/src/entity/passive/pig.rs @@ -107,7 +107,7 @@ impl Mob for PigEntity { ); return true; } - false + self.mob_entity.mob_interact(player, item_stack).await }) } } diff --git a/pumpkin/src/entity/passive/sheep.rs b/pumpkin/src/entity/passive/sheep.rs index 1751a7af5..624183469 100644 --- a/pumpkin/src/entity/passive/sheep.rs +++ b/pumpkin/src/entity/passive/sheep.rs @@ -170,7 +170,7 @@ impl Mob for SheepEntity { ); return true; } - false + self.mob_entity.mob_interact(player, item_stack).await }) } } diff --git a/pumpkin/src/item/items/lead.rs b/pumpkin/src/item/items/lead.rs new file mode 100644 index 000000000..e07da57f8 --- /dev/null +++ b/pumpkin/src/item/items/lead.rs @@ -0,0 +1,100 @@ +use std::pin::Pin; +use std::sync::Arc; + +use crate::entity::EntityBase; +use crate::entity::decoration::leash_knot::LeashKnotEntity; +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::Block; +use pumpkin_data::BlockDirection; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::Taggable; +use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +pub struct LeadItem; + +impl ItemMetadata for LeadItem { + fn ids() -> Box<[u16]> { + [Item::LEAD.id].into() + } +} + +impl ItemBehaviour for LeadItem { + fn use_on_block<'a>( + &'a self, + item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + _face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if !block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_FENCES) { + return; + } + + let world = player.world(); + let center = Vector3::new( + f64::from(location.0.x) + 0.5, + f64::from(location.0.y) + 0.5, + f64::from(location.0.z) + 0.5, + ); + + let search_dim = EntityDimensions { + width: 32.0, + height: 32.0, + eye_height: 16.0, + }; + + let search_box = BoundingBox::new_from_pos(center.x, center.y, center.z, &search_dim); + + let player_id = player.entity_id(); + let entities = world.get_entities_at_box(&search_box); + + let mut any_leashed = false; + let mut knot: Option> = None; + + for entity_base in entities { + let ent = entity_base.get_entity(); + let is_leashed_to_player = ent + .leashed_to + .try_lock() + .ok() + .and_then(|guard| { + guard + .as_ref() + .map(|holder| holder.get_entity().entity_id == player_id) + }) + .unwrap_or(false); + + if is_leashed_to_player { + if knot.is_none() { + knot = Some(LeashKnotEntity::get_or_create(&world, location).await); + } + if let Some(k) = &knot { + ent.leash_to(k.clone() as Arc).await; + any_leashed = true; + } + } + } + + if any_leashed { + if player.gamemode.load() != pumpkin_util::GameMode::Creative { + item.decrement(1); + } + world.play_sound(Sound::ItemLeadTied, SoundCategory::Neutral, ¢er); + } + }) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index 42cdbdc6b..7696133b1 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -30,11 +30,15 @@ pub mod swords; pub mod trident; pub mod wind_charge; +pub mod lead; + use crate::item::items::armor_stand::ArmorStandItem; use crate::item::items::boat::BoatItem; use crate::item::items::bundle::BundleItem; use crate::item::items::end_crystal::EndCrystalItem; +use crate::item::items::lead::LeadItem; use crate::item::items::map::MapItem; + use crate::item::items::minecart::MinecartItem; use crate::item::items::name_tag::NameTagItem; use crate::item::items::spawn_egg::SpawnEggItem; @@ -108,6 +112,7 @@ pub fn default_registry() -> Arc { manager.register(SplashPotionItem); manager.register(LingeringPotionItem); manager.register(BundleItem); + manager.register(LeadItem); Arc::new(manager) }