From 7eb49e2e4d88782beb87f79eb487620aef0a083a Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 11 Aug 2026 18:40:48 +0200 Subject: [PATCH] feat(bedrock): add CMobEquipment --- .../src/bedrock/client/mob_equipment.rs | 45 +++++++++ .../src/bedrock/client/mod.rs | 2 + crates/pumpkin/src/entity/living.rs | 43 +++++++- crates/pumpkin/src/entity/player.rs | 24 ++--- crates/pumpkin/src/world/mod.rs | 99 +++++++++++++++---- 5 files changed, 170 insertions(+), 43 deletions(-) create mode 100644 crates/pumpkin-protocol/src/bedrock/client/mob_equipment.rs diff --git a/crates/pumpkin-protocol/src/bedrock/client/mob_equipment.rs b/crates/pumpkin-protocol/src/bedrock/client/mob_equipment.rs new file mode 100644 index 000000000..28a3d1649 --- /dev/null +++ b/crates/pumpkin-protocol/src/bedrock/client/mob_equipment.rs @@ -0,0 +1,45 @@ +use crate::{ + bedrock::network_item::NetworkItemStackDescriptor, codec::var_ulong::VarULong, + serial::PacketWrite, +}; +use pumpkin_macros::packet; + +#[derive(PacketWrite, Debug)] +#[packet(31)] +pub struct CMobEquipment { + pub entity_runtime_id: VarULong, + pub item: NetworkItemStackDescriptor, + pub inventory_slot: u8, + pub hotbar_slot: u8, + pub window_id: u8, +} + +impl CMobEquipment { + #[must_use] + pub const fn new( + entity_runtime_id: u64, + item: NetworkItemStackDescriptor, + inventory_slot: u8, + hotbar_slot: u8, + window_id: u8, + ) -> Self { + Self { + entity_runtime_id: VarULong(entity_runtime_id), + item, + inventory_slot, + hotbar_slot, + window_id, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::Packet; + + #[test] + fn mob_equipment_packet_id() { + assert_eq!(CMobEquipment::PACKET_ID, 31); + } +} diff --git a/crates/pumpkin-protocol/src/bedrock/client/mod.rs b/crates/pumpkin-protocol/src/bedrock/client/mod.rs index 5013a0d84..bcd1c569c 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/mod.rs @@ -19,6 +19,7 @@ pub mod item_registry; pub mod item_stack_response; pub mod level_chunk; pub mod level_event; +pub mod mob_equipment; pub mod modal_form_request; pub mod move_actor_absolute; pub mod move_actor_delta; @@ -73,6 +74,7 @@ pub use item_stack_response::*; pub use level_chunk::*; pub use level_event::*; pub use level_sound_event::*; +pub use mob_equipment::*; pub use modal_form_request::*; pub use move_actor_absolute::*; pub use move_actor_delta::*; diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index 6759c7da9..261684dd0 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -193,7 +193,7 @@ impl LivingEntity { if equipment.is_empty() { return; } - let equipment: Vec<(i8, ItemStackSerializer)> = equipment + let equipment_java: Vec<(i8, ItemStackSerializer)> = equipment .iter() .map(|(slot, stack)| { ( @@ -202,10 +202,43 @@ impl LivingEntity { ) }) .collect(); - self.entity.world.load().broadcast_packet_except( - &[self.entity.entity_uuid], - &CSetEquipment::new(self.entity_id().into(), equipment), - ); + let je_packet = CSetEquipment::new(self.entity_id().into(), equipment_java); + + let mut sent_editioned = false; + for (slot, stack) in equipment { + if *slot == EquipmentSlot::MAIN_HAND || *slot == EquipmentSlot::OFF_HAND { + let window_id = if *slot == EquipmentSlot::OFF_HAND { + 120 + } else { + 0 + }; + let be_packet = pumpkin_protocol::bedrock::client::CMobEquipment::new( + self.entity_id() as u64, + pumpkin_protocol::bedrock::network_item::NetworkItemStackDescriptor::from( + stack, + ), + 0, + 0, + window_id, + ); + self.entity + .world + .load() + .broadcast_packet_except_editioned_sync( + &[self.entity.entity_uuid], + &je_packet, + &be_packet, + ); + sent_editioned = true; + } + } + + if !sent_editioned { + self.entity + .world + .load() + .broadcast_packet_except(&[self.entity.entity_uuid], &je_packet); + } } /// Picks up and Item entity or XP Orb diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index f072e3022..48dd54206 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -68,11 +68,11 @@ use pumpkin_protocol::java::client::play::{ CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetCamera, CSetContainerContent, - CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment, CSetExperience, - CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, - CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, - CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags, - PlayerSpawnData, PreviousMessage, Statistic, + CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetExperience, CSetHealth, + CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, CSystemChatMessage, + CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, + MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags, PlayerSpawnData, PreviousMessage, + Statistic, }; use pumpkin_protocol::java::server::play::{ SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType, @@ -5790,18 +5790,8 @@ impl InventoryPlayer for Player { stack: &'a ItemStack, ) -> PlayerFuture<'a, ()> { Box::pin(async move { - let chunk_pos = self.living_entity.entity.chunk_pos.load(); - self.world().broadcast_to_chunk_except( - chunk_pos, - &[self.get_entity().entity_uuid], - &CSetEquipment::new( - self.entity_id().into(), - vec![( - slot.discriminant(), - ItemStackSerializer::from(stack.clone()), - )], - ), - ); + self.living_entity + .send_equipment_changes(&[(slot.clone(), stack.clone())]); if let Some(equippable) = stack.get_data_component::() { self.world().play_sound_event( diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index d283eb708..9e6181e7e 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -6,7 +6,7 @@ use pumpkin_data::item::{BedrockItem, BedrockItemVersion}; use pumpkin_protocol::bedrock::client::EntityProperties; use pumpkin_protocol::bedrock::client::item_registry::{CItemRegistry, ItemDefinition}; use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent}; -use pumpkin_protocol::bedrock::network_item::NetworkItemDescriptor; +use pumpkin_protocol::bedrock::network_item::{NetworkItemDescriptor, NetworkItemStackDescriptor}; use pumpkin_protocol::codec::data_component::data_to_proto_sound; use pumpkin_world::generation::proto_chunk::GenerationCache; use std::sync::atomic::Ordering::Relaxed; @@ -2527,6 +2527,22 @@ impl World { &bedrock_add_player, ); + let held_item = player.inventory().held_item().await; + + let be_mob_equipment = pumpkin_protocol::bedrock::client::CMobEquipment::new( + runtime_id, + NetworkItemStackDescriptor::from(&held_item), + 0, + 0, + 0, + ); + + self.broadcast_packet_except_editioned_sync( + &[gameprofile.id], + &CSetEquipment::new((runtime_id as i32).into(), vec![]), + &be_mob_equipment, + ); + // Broadcast metadata to Java players so they can correctly interact with the new player let skin_parts = player.config.load().skin_parts; @@ -2606,6 +2622,18 @@ impl World { }; client.send_game_packet(&ex_add_player).await; + + let ex_held_item = existing_player.inventory().held_item().await; + + let ex_be_mob_equipment = pumpkin_protocol::bedrock::client::CMobEquipment::new( + existing_player.entity_id() as u64, + NetworkItemStackDescriptor::from(&ex_held_item), + 0, + 0, + 0, + ); + + client.send_game_packet(&ex_be_mob_equipment).await; } // 3. Trigger Join Event and Broadcast Join Message @@ -2981,6 +3009,22 @@ impl World { &bedrock_add_player, ); + let held_item = player.inventory().held_item().await; + + let be_mob_equipment = pumpkin_protocol::bedrock::client::CMobEquipment::new( + entity_id as u64, + NetworkItemStackDescriptor::from(&held_item), + 0, + 0, + 0, + ); + + self.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &CSetEquipment::new(entity_id.into(), vec![]), + &be_mob_equipment, + ); + // Broadcast metadata to Java players so they can correctly interact with the new player let skin_parts = player.config.load().skin_parts; @@ -3153,12 +3197,9 @@ impl World { }; { - let mut equipment_list = Vec::new(); - - equipment_list.push(( - EquipmentSlot::MAIN_HAND.discriminant(), - existing_player.inventory.held_item().await, - )); + let held_item = existing_player.inventory.held_item().await; + let mut equipment_list = + vec![(EquipmentSlot::MAIN_HAND.discriminant(), held_item.clone())]; let equipment_guard = existing_player.inventory.entity_equipment.lock().await; for (slot, item_stack) in &equipment_guard.equipment { @@ -3170,11 +3211,19 @@ impl World { .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) .collect(); - client - .enqueue_packet(&CSetEquipment::new( - existing_player.entity_id().into(), - equipment, - )) + let je_packet = CSetEquipment::new(existing_player.entity_id().into(), equipment); + + let be_mob_equipment = pumpkin_protocol::bedrock::client::CMobEquipment::new( + existing_player.entity_id() as u64, + NetworkItemStackDescriptor::from(&held_item), + 0, + 0, + 0, + ); + + player + .client + .enqueue_packet_editioned(&je_packet, &be_mob_equipment) .await; } } @@ -3291,12 +3340,8 @@ impl World { } async fn send_player_equipment(&self, from: &Player) { - let mut equipment_list = Vec::new(); - - equipment_list.push(( - EquipmentSlot::MAIN_HAND.discriminant(), - from.inventory.held_item().await, - )); + let held_item = from.inventory.held_item().await; + let mut equipment_list = vec![(EquipmentSlot::MAIN_HAND.discriminant(), held_item.clone())]; let equipment_guard = from.inventory.entity_equipment.lock().await; for (slot, item_stack) in &equipment_guard.equipment { @@ -3307,12 +3352,24 @@ impl World { .iter() .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) .collect(); + let je_packet = CSetEquipment::new(from.entity_id().into(), equipment); + + let be_mob_equipment = pumpkin_protocol::bedrock::client::CMobEquipment::new( + from.entity_id() as u64, + NetworkItemStackDescriptor::from(&held_item), + 0, + 0, + 0, + ); + let chunk_pos = from.get_entity().chunk_pos.load(); - self.broadcast_to_chunk_except( + self.broadcast_to_chunk_except_editioned( chunk_pos, &[from.get_entity().entity_uuid], - &CSetEquipment::new(from.entity_id().into(), equipment), - ); + &je_packet, + &be_mob_equipment, + ) + .await; } pub async fn send_world_info(