From e47695d81012ec29fc40a9541f8800318be48978 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 7 Jun 2026 14:34:54 +0200 Subject: [PATCH] feat(bedrock): add item drops --- Cargo.lock | 18 ++++---- Cargo.toml | 2 +- .../src/bedrock/client/add_item_actor.rs | 34 ++++++++++++++ pumpkin-protocol/src/bedrock/client/mod.rs | 2 + pumpkin-protocol/src/bedrock/network_item.rs | 6 +++ pumpkin-util/src/math/vector3.rs | 15 ++++++ pumpkin/src/entity/item.rs | 30 ++++++++++-- pumpkin/src/entity/mod.rs | 36 +++++++++++++-- pumpkin/src/net/bedrock/mod.rs | 7 +++ pumpkin/src/net/bedrock/play.rs | 46 +++++++++++++++++-- pumpkin/src/net/mod.rs | 7 +++ pumpkin/src/world/mod.rs | 24 +++++++--- 12 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 pumpkin-protocol/src/bedrock/client/add_item_actor.rs diff --git a/Cargo.lock b/Cargo.lock index 04eb0369a..b7f609fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,9 +219,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -490,7 +490,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1170,7 +1170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2199,7 +2199,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3118,7 +3118,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3441,7 +3441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3560,7 +3560,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4614,7 +4614,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8c676cd24..7cefe8c0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ proc-macro-error2 = { version = "2", default-features = false } aes = { version = "0.9", default-features = false } async-compression = { version = "0.4.42", default-features = false } base64 = { version = "0.22.1", default-features = false, features = ["std"] } -bitflags = { version = "2.12.1", default-features = false, features = ["std"] } +bitflags = { version = "2.13.0", default-features = false, features = ["std"] } cesu8 = { version = "1.1", default-features = false } cfb8 = { version = "0.9", default-features = false } colored = { version = "3.1", default-features = false } diff --git a/pumpkin-protocol/src/bedrock/client/add_item_actor.rs b/pumpkin-protocol/src/bedrock/client/add_item_actor.rs new file mode 100644 index 000000000..853582bb8 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/add_item_actor.rs @@ -0,0 +1,34 @@ +use crate::{ + codec::{var_long::VarLong, var_ulong::VarULong}, + serial::PacketWrite, +}; +use pumpkin_macros::packet; +use pumpkin_util::math::vector3::Vector3; +use std::io::{Error, Write}; + +use super::set_actor_data::EntityMetadata; +use crate::bedrock::network_item::NetworkItemStackDescriptor; + +#[packet(15)] +pub struct CAddItemActor { + pub entity_unique_id: VarLong, + pub entity_runtime_id: VarULong, + pub item: NetworkItemStackDescriptor, + pub position: Vector3, + pub velocity: Vector3, + pub metadata: EntityMetadata, + pub from_fishing: bool, +} + +impl PacketWrite for CAddItemActor { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.entity_unique_id.write(writer)?; + self.entity_runtime_id.write(writer)?; + self.item.write(writer)?; + self.position.write(writer)?; + self.velocity.write(writer)?; + self.metadata.write(writer)?; + self.from_fishing.write(writer)?; + Ok(()) + } +} diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index d6a9e03f7..01a95ee60 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -1,4 +1,5 @@ pub mod add_actor; +pub mod add_item_actor; pub mod add_player; pub mod available_commands; pub mod boss_event; @@ -42,6 +43,7 @@ pub mod update_attributes; pub mod update_block; pub use add_actor::*; +pub use add_item_actor::*; pub use add_player::*; pub use available_commands::*; pub use boss_event::*; diff --git a/pumpkin-protocol/src/bedrock/network_item.rs b/pumpkin-protocol/src/bedrock/network_item.rs index 38d82d74f..7663226ca 100644 --- a/pumpkin-protocol/src/bedrock/network_item.rs +++ b/pumpkin-protocol/src/bedrock/network_item.rs @@ -79,6 +79,12 @@ impl NetworkItemDescriptor { } } +impl From<&ItemStack> for NetworkItemDescriptor { + fn from(stack: &ItemStack) -> Self { + NetworkItemStackDescriptor::from(stack).item + } +} + #[derive(Default, Clone)] pub struct NetworkItemStackDescriptor { // I hate mojang diff --git a/pumpkin-util/src/math/vector3.rs b/pumpkin-util/src/math/vector3.rs index 42c378254..c49bc6c59 100644 --- a/pumpkin-util/src/math/vector3.rs +++ b/pumpkin-util/src/math/vector3.rs @@ -522,6 +522,21 @@ impl From> for (T, T, T) { } } +impl Vector3 { + /// Converts this vector to a `Vector3` (lossy). + /// + /// # Returns + /// A new `Vector3` with each component cast to `f32`. + #[must_use] + pub const fn to_f32_lossy(&self) -> Vector3 { + Vector3 { + x: self.x as f32, + y: self.y as f32, + z: self.z as f32, + } + } +} + impl> Vector3 { /// Converts this vector to a `Vector3`. /// diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index 448490e62..e6343a7b2 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -4,9 +4,12 @@ use pumpkin_data::data_component_impl::DamageResistantImpl; use pumpkin_data::data_component_impl::DamageResistantType; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::{damage::DamageType, meta_data_type::MetaDataType, tracked_data::TrackedData}; -use pumpkin_protocol::{ - codec::item_stack_seralizer::ItemStackSerializer, java::client::play::Metadata, -}; +use pumpkin_protocol::bedrock::client::CAddItemActor; +use pumpkin_protocol::bedrock::network_item::NetworkItemStackDescriptor; +use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; +use pumpkin_protocol::codec::var_long::VarLong; +use pumpkin_protocol::codec::var_ulong::VarULong; +use pumpkin_protocol::java::client::play::Metadata; use pumpkin_util::math::atomic_f32::AtomicF32; use pumpkin_util::math::vector3::Vector3; use std::sync::atomic::Ordering::{AcqRel, Relaxed}; @@ -503,4 +506,25 @@ impl EntityBase for ItemEntity { fn cast_any(&self) -> &dyn std::any::Any { self } + + fn send_bedrock_spawn_packet<'a>( + &'a self, + client: &'a crate::net::bedrock::BedrockClient, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = &self.entity; + let runtime_id = entity.entity_id as u64; + let item_stack = self.item_stack.lock().await; + let packet = CAddItemActor { + entity_unique_id: VarLong(runtime_id as i64), + entity_runtime_id: VarULong(runtime_id), + item: NetworkItemStackDescriptor::from(&*item_stack), + position: entity.pos.load().to_f32_lossy(), + velocity: entity.velocity.load().to_f32_lossy(), + metadata: entity.bedrock_metadata(), + from_fishing: false, + }; + client.send_game_packet(&packet).await; + }) + } } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index a7da96ad3..6824cdb8b 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -1,6 +1,6 @@ -use crate::entity::item::ItemEntity; -use crate::net::ClientPlatform; use crate::{ + entity::item::ItemEntity, + net::{ClientPlatform, bedrock::BedrockClient}, server::Server, world::{ World, @@ -32,7 +32,8 @@ use pumpkin_data::{ sound::{Sound, SoundCategory}, }; use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; -use pumpkin_protocol::bedrock::client::CSetActorMotion; +use pumpkin_protocol::bedrock::client::{CAddActor, CSetActorMotion}; +use pumpkin_protocol::codec::var_long::VarLong; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::{ PositionFlag, @@ -248,6 +249,35 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { None } + fn send_bedrock_spawn_packet<'a>( + &'a self, + client: &'a BedrockClient, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let runtime_id = entity.entity_id as u64; + let packet = CAddActor::new( + VarLong(runtime_id as i64), + VarULong(runtime_id), + self.get_entity().entity_type.resource_name.to_string(), + entity.pos.load().to_f32_lossy(), + entity.velocity.load().to_f32_lossy(), + entity.pitch.load(), + entity.yaw.load(), + entity.head_yaw.load(), + entity.body_yaw.load(), + Vec::new(), + entity.bedrock_metadata(), + PropertySyncData { + int_properties: std::collections::HashMap::new(), + float_properties: std::collections::HashMap::new(), + }, + Vec::new(), + ); + client.send_game_packet(&packet).await; + }) + } + fn damage_with_context<'a>( &'a self, caller: &'a dyn EntityBase, diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index 8c7c47fc1..b25db0041 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -662,6 +662,13 @@ impl BedrockClient { self.close_token.is_cancelled() } + pub fn enqueue_spawn_packet(self: &Arc, entity: Arc) { + let client = self.clone(); + self.spawn_task(async move { + entity.send_bedrock_spawn_packet(&client).await; + }); + } + pub async fn send_acknowledgement(&self, ack: &Acknowledge, id: u8) -> Result<(), Error> { let mut packet_buf = Vec::new(); ack.write(&mut packet_buf, id)?; diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 0e36ca862..0dcfbf329 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -497,6 +497,7 @@ impl BedrockClient { } #[expect(clippy::match_same_arms)] + #[expect(clippy::too_many_lines)] pub async fn handle_player_action( &self, player: &Arc, @@ -509,7 +510,9 @@ impl BedrockClient { player.update_last_action_time(); match packet.action { - PlayerAction::StartBreak | PlayerAction::CreativePlayerDestroyBlock => { + PlayerAction::StartBreak + | PlayerAction::CreativePlayerDestroyBlock + | PlayerAction::ContinueDestroyBlock => { let location = packet.block_pos; if !player.can_interact_with_block_at(&location, 1.0) { return; @@ -534,9 +537,6 @@ impl BedrockClient { .await; } } else if !state.is_air() { - // Broadcast that breaking started - world.set_block_breaking(entity, location, 0).await; - let speed = crate::block::calc_block_breaking(player, state, block).await; if speed >= 1.0 { let broken_state = world.get_block_state(&location); @@ -565,11 +565,47 @@ impl BedrockClient { } } } + PlayerAction::PredictDestroyBlock | PlayerAction::StopBreak => { + let location = packet.block_pos; + if !player.can_interact_with_block_at(&location, 1.0) { + return; + } + + let entity = &player.get_entity(); + let world = entity.world.load_full(); + + player.mining.store(false, Ordering::Relaxed); + world.set_block_breaking(entity, location, -1).await; + + let (block, state) = world.get_block_and_state(&location); + if player.gamemode.load() != GameMode::Creative { + let block_drop = player.can_harvest(state, block).await; + + let new_state = world + .break_block( + &location, + Some(player.clone()), + if block_drop { + BlockFlags::NOTIFY_NEIGHBORS + } else { + BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS + }, + ) + .await; + if new_state.is_some() { + server + .block_registry + .broken(&world, block, player, &location, server, state) + .await; + player.apply_tool_damage_for_block_break(state).await; + } + } + } PlayerAction::CrackBreak => { // Don't do anything for this action. It is no longer used. Block // cracking is done fully server-side. } - PlayerAction::AbortBreak | PlayerAction::StopBreak => { + PlayerAction::AbortBreak => { let location = packet.block_pos; let entity = &player.get_entity(); let world = entity.world.load(); diff --git a/pumpkin/src/net/mod.rs b/pumpkin/src/net/mod.rs index 5cc6e5db9..b2b1dbf1c 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -242,6 +242,13 @@ impl ClientPlatform { } } + pub fn try_enqueue_spawn_packet(&self, entity: &Arc) { + match self { + Self::Java(java) => java.try_enqueue_packet(&entity.get_entity().create_spawn_packet()), + Self::Bedrock(bedrock) => bedrock.enqueue_spawn_packet(entity.clone()), + } + } + pub async fn send_chunks(&self, chunks: &[SyncChunk]) { match self { Self::Java(java) => java.send_chunks(chunks).await, diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 213411db1..14e7aeaad 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -3722,9 +3722,8 @@ impl World { } pub fn spawn_entity_non_save(&self, entity: &Arc) { - let base_entity = entity.get_entity(); - let chunk_pos = base_entity.chunk_pos.load(); - self.broadcast_to_chunk(chunk_pos, &base_entity.create_spawn_packet()); + let _base_entity = entity.get_entity(); + self.broadcast_entity_spawn(entity); self.spawn_state.load().add_entity(self, entity.as_ref()); self.entities.rcu(|current_entities| { @@ -3735,13 +3734,26 @@ impl World { } pub async fn spawn_entity(&self, entity: Arc) { - let base_entity = entity.get_entity(); - let chunk_pos = base_entity.chunk_pos.load(); - self.broadcast_to_chunk(chunk_pos, &base_entity.create_spawn_packet()); + self.broadcast_entity_spawn(&entity); entity.init_data_tracker().await; self.add_entity_silent(entity).await; } + pub fn broadcast_entity_spawn(&self, entity: &Arc) { + let base_entity = entity.get_entity(); + let chunk_pos = base_entity.chunk_pos.load(); + + let players = self.players.load(); + for player in players.iter() { + let center = player.get_entity().chunk_pos.load(); + let view_distance = get_view_distance(player).get() as i32; + + if is_within_view_distance(chunk_pos, center, view_distance) { + player.client.try_enqueue_spawn_packet(entity); + } + } + } + pub async fn add_entity_silent(&self, entity: Arc) { let base_entity = entity.get_entity();