feat: add Leash

This commit is contained in:
Alexander Medvedev
2026-07-25 10:51:55 +02:00
parent 0dfaf5d213
commit cba7578e28
25 changed files with 625 additions and 11 deletions

View File

@@ -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: <https://mojang.github.io/bedrock-protocol-docs/html/LevelSoundEventPacket.html>
#[derive(PacketWrite)]
#[packet(27)]
pub struct CLevelSoundEvent {
pub sound_id: VarUInt,
pub position: Vector3<f32>,
pub extra_data: VarInt,
pub entity_type: String,
pub is_baby_mob: bool,
pub is_global: bool,
}

View File

@@ -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::*;

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Connected_Pong>
#[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: <https://minecraft.wiki/w/RakNet#Connection_Request_Accepted>
#[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: <https://minecraft.wiki/w/RakNet#Already_Connected>
#[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: <https://minecraft.wiki/w/RakNet#No_Free_Incoming_Connections>
#[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: <https://minecraft.wiki/w/RakNet#Connection_Banned>
#[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: <https://minecraft.wiki/w/RakNet#IP_Recently_Connected>
#[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: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[derive(PacketWrite)]
#[packet(0x15)]
pub struct CDisconnect;

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Incompatible_Protocol_Version>
#[derive(PacketWrite)]
#[packet(0x19)]
pub struct CIncompatibleProtocolVersion {

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_1>
#[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: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_2>
#[derive(PacketWrite)]
#[packet(0x08)]
pub struct COpenConnectionReply2 {

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Unconnected_Pong>
#[packet(0x1c)]
pub struct CUnconnectedPong {
time: u64,

View File

@@ -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: <https://mojang.github.io/bedrock-protocol-docs/html/SetActorLinkPacket.html>
#[derive(PacketWrite)]
#[packet(41)]
pub struct CSetActorLink {
pub link: EntityLink,
}

View File

@@ -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::*;

View File

@@ -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: <https://mojang.github.io/bedrock-protocol-docs/html/PlayerHotbarPacket.html>
#[derive(PacketRead)]
#[packet(48)]
pub struct SPlayerHotbar {
pub selected_slot: u32,
pub container_id: u8,
pub select_slot: bool,
}

View File

@@ -4,6 +4,9 @@ use pumpkin_macros::packet;
use crate::serial::PacketRead;
/// Sent periodically by a connected client to measure round-trip time.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connected_Ping>
#[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: <https://minecraft.wiki/w/RakNet#Connection_Request>
#[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: <https://minecraft.wiki/w/RakNet#New_Incoming_Connection>
#[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: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[packet(0x15)]
pub struct SDisconnect;
/// Internal notification signal for a connection lost due to socket error or timeout.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[packet(0x16)]
pub struct SConnectionLost;

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_1>
#[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: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_2>
#[derive(PacketRead)]
#[packet(0x07)]
pub struct SOpenConnectionRequest2 {

View File

@@ -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: <https://minecraft.wiki/w/RakNet#Unconnected_Ping>
#[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: <https://minecraft.wiki/w/RakNet#Unconnected_Ping_Open_Connections>
#[derive(PacketRead)]
#[packet(0x02)]
/// Used to request Server information like MOTD when connection is open?
pub struct SUnconnectedPingOpenConnections {
#[serial(big_endian)]
pub time: u64,

View File

@@ -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::*;

View File

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

View File

@@ -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<World>, pos: BlockPos) -> Arc<Self> {
if let Some(existing) = Self::get_knot(world, pos) {
return existing;
}
Self::create_knot(world, pos).await
}
pub fn get_knot(world: &Arc<World>, pos: BlockPos) -> Option<Arc<Self>> {
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::<Arc<Self>>()
&& knot.pos == pos
{
return Some(knot.clone());
}
}
None
}
pub async fn create_knot(world: &Arc<World>, pos: BlockPos) -> Arc<Self> {
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<dyn EntityBase>)
.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<dyn EntityBase>,
_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<Player>,
_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<dyn EntityBase>).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<dyn EntityBase>).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
}
}

View File

@@ -1,3 +1,4 @@
pub mod armor_stand;
pub mod end_crystal;
pub mod leash_knot;
pub mod painting;

View File

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

View File

@@ -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<Player>, 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<dyn EntityBase>).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<Player>,
_item_stack: &'a mut ItemStack,
player: &'a Arc<Player>,
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<Player>) -> EntityBaseFuture<'a, ()> {
@@ -554,6 +593,7 @@ impl<T: Mob + Send + 'static> 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 {

View File

@@ -787,6 +787,8 @@ pub struct Entity {
pub passengers: Mutex<Vec<Arc<dyn EntityBase>>>,
/// The vehicle that entity is in
pub vehicle: Mutex<Option<Arc<dyn EntityBase>>>,
/// The entity this entity is attached/leashed to (if any)
pub leashed_to: Mutex<Option<Arc<dyn EntityBase>>>,
/// 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<dyn EntityBase>) {
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()
}

View File

@@ -197,7 +197,7 @@ impl Mob for ChickenEntity {
);
return true;
}
false
self.mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@@ -102,7 +102,7 @@ impl Mob for CowEntity {
);
return true;
}
false
self.mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@@ -107,7 +107,7 @@ impl Mob for PigEntity {
);
return true;
}
false
self.mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@@ -170,7 +170,7 @@ impl Mob for SheepEntity {
);
return true;
}
false
self.mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@@ -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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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<Arc<LeashKnotEntity>> = 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<dyn EntityBase>).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, &center);
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@@ -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<ItemRegistry> {
manager.register(SplashPotionItem);
manager.register(LingeringPotionItem);
manager.register(BundleItem);
manager.register(LeadItem);
Arc::new(manager)
}