mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add Leash
This commit is contained in:
22
pumpkin-protocol/src/bedrock/client/level_sound_event.rs
Normal file
22
pumpkin-protocol/src/bedrock/client/level_sound_event.rs
Normal 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,
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
pumpkin-protocol/src/bedrock/client/set_actor_link.rs
Normal file
13
pumpkin-protocol/src/bedrock/client/set_actor_link.rs
Normal 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,
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
15
pumpkin-protocol/src/bedrock/server/player_hotbar.rs
Normal file
15
pumpkin-protocol/src/bedrock/server/player_hotbar.rs
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
34
pumpkin-protocol/src/java/client/play/set_entity_link.rs
Normal file
34
pumpkin-protocol/src/java/client/play/set_entity_link.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
216
pumpkin/src/entity/decoration/leash_knot.rs
Normal file
216
pumpkin/src/entity/decoration/leash_knot.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod armor_stand;
|
||||
pub mod end_crystal;
|
||||
pub mod leash_knot;
|
||||
pub mod painting;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ impl Mob for ChickenEntity {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
self.mob_entity.mob_interact(player, item_stack).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ impl Mob for CowEntity {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
self.mob_entity.mob_interact(player, item_stack).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Mob for PigEntity {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
self.mob_entity.mob_interact(player, item_stack).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ impl Mob for SheepEntity {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
self.mob_entity.mob_interact(player, item_stack).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
100
pumpkin/src/item/items/lead.rs
Normal file
100
pumpkin/src/item/items/lead.rs
Normal 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, ¢er);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user