diff --git a/pumpkin-protocol/src/codec/data_component.rs b/pumpkin-protocol/src/codec/data_component.rs index dc2dc9e0f..21cf2c50b 100644 --- a/pumpkin-protocol/src/codec/data_component.rs +++ b/pumpkin-protocol/src/codec/data_component.rs @@ -4,11 +4,11 @@ use crate::codec::var_int::VarInt; use pumpkin_data::Enchantment; use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component_impl::{ - ConsumableImpl, ConsumeAnimation, ConsumeEffect, CustomNameImpl, DamageImpl, DataComponentImpl, - EnchantmentsImpl, EquipmentSlot, EquippableImpl, FireworkExplosionImpl, FireworkExplosionShape, - FireworksImpl, IDSet, IDSetContent, IdOr, ItemModelImpl, MapIdImpl, MaxStackSizeImpl, - PotionContentsImpl, SoundEvent, StatusEffectInstance, StoredEnchantmentsImpl, UnbreakableImpl, - UseCooldownImpl, get, + BundleContentsImpl, ConsumableImpl, ConsumeAnimation, ConsumeEffect, CustomNameImpl, + DamageImpl, DataComponentImpl, EnchantmentsImpl, EquipmentSlot, EquippableImpl, + FireworkExplosionImpl, FireworkExplosionShape, FireworksImpl, IDSet, IDSetContent, IdOr, + ItemModelImpl, MapIdImpl, MaxStackSizeImpl, PotionContentsImpl, SoundEvent, + StatusEffectInstance, StoredEnchantmentsImpl, UnbreakableImpl, UseCooldownImpl, get, }; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::EntityType; @@ -845,6 +845,7 @@ pub fn deserialize<'a, A: SeqAccess<'a>>( DataComponent::StoredEnchantments => Ok(StoredEnchantmentsImpl::deserialize(seq)?.to_dyn()), DataComponent::UseCooldown => Ok(UseCooldownImpl::deserialize(seq)?.to_dyn()), DataComponent::MapId => Ok(MapIdImpl::deserialize(seq)?.to_dyn()), + DataComponent::BundleContents => Ok(BundleContentsImpl::deserialize(seq)?.to_dyn()), _ => Err(serde::de::Error::custom(format!("{id:?} (TODO)"))), } } @@ -868,6 +869,7 @@ pub fn serialize( DataComponent::StoredEnchantments => get::(value).serialize(seq), DataComponent::UseCooldown => get::(value).serialize(seq), DataComponent::MapId => get::(value).serialize(seq), + DataComponent::BundleContents => get::(value).serialize(seq), _ => Err(serde::ser::Error::custom(format!( "{} not yet implemented", id.to_name() @@ -910,3 +912,134 @@ impl DataComponentCodec for UseCooldownImpl { }) } } + +fn deserialize_item_stack_template<'a, A: SeqAccess<'a>>( + seq: &mut A, +) -> Result { + let item_id = seq + .next_element::()? + .ok_or_else(|| de::Error::custom("Missing item_id in ItemStackTemplate"))? + .0 as u16; + + let count = seq + .next_element::()? + .ok_or_else(|| de::Error::custom("Missing count in ItemStackTemplate"))? + .0 as u8; + + let num_to_add = seq.next_element::()?.map_or(0, |v| v.0); + let num_to_remove = seq.next_element::()?.map_or(0, |v| v.0); + + if num_to_add < 0 || num_to_remove < 0 { + return Err(de::Error::custom("Negative component count")); + } + + const MAX_COMPONENTS: i32 = 256; + let total_components = num_to_add + .checked_add(num_to_remove) + .ok_or_else(|| de::Error::custom("Component count overflow"))?; + + if total_components > MAX_COMPONENTS { + return Err(de::Error::custom( + "Too many components in ItemStackTemplate patch", + )); + } + + let mut patch = Vec::with_capacity((num_to_add + num_to_remove) as usize); + + for _ in 0..num_to_add { + let id_val = seq + .next_element::()? + .ok_or_else(|| de::Error::custom("Missing component ID"))? + .0; + let id = DataComponent::try_from_id(id_val as u8) + .ok_or_else(|| de::Error::custom(format!("Unknown component ID: {id_val}")))?; + + let _byte_len = seq + .next_element::()? + .ok_or_else(|| de::Error::custom("No data len VarInt!"))?; + + let component_impl = deserialize(id, seq)?; + patch.push((id, Some(component_impl))); + } + + for _ in 0..num_to_remove { + let id_val = seq + .next_element::()? + .ok_or_else(|| de::Error::custom("Missing remove component ID"))? + .0; + let id = DataComponent::try_from_id(id_val as u8) + .ok_or_else(|| de::Error::custom("Unknown component ID"))?; + patch.push((id, None)); + } + + Ok(pumpkin_data::item_stack::ItemStack::new_with_component( + count, + pumpkin_data::item::Item::from_id(item_id).unwrap_or(&pumpkin_data::item::Item::AIR), + patch, + )) +} + +fn serialize_item_stack_template( + stack: &pumpkin_data::item_stack::ItemStack, + seq: &mut T, +) -> Result<(), T::Error> { + seq.serialize_field::("", &VarInt::from(stack.item.id))?; + seq.serialize_field::("", &VarInt::from(stack.item_count))?; + + let mut to_add = 0u8; + let mut to_remove = 0u8; + for (_id, data) in &stack.patch { + if data.is_none() { + to_remove += 1; + } else { + to_add += 1; + } + } + + seq.serialize_field::("", &VarInt::from(to_add))?; + seq.serialize_field::("", &VarInt::from(to_remove))?; + + for (id, data) in &stack.patch { + if let Some(data) = data { + seq.serialize_field::("", &VarInt::from(id.to_id()))?; + serialize(*id, data.as_ref(), seq)?; + } + } + + for (id, data) in &stack.patch { + if data.is_none() { + seq.serialize_field::("", &VarInt::from(id.to_id()))?; + } + } + + Ok(()) +} + +impl DataComponentCodec for BundleContentsImpl { + fn serialize(&self, seq: &mut T) -> Result<(), T::Error> { + seq.serialize_field::("", &VarInt::from(self.items.len() as i32))?; + for item in &self.items { + serialize_item_stack_template(item, seq)?; + } + Ok(()) + } + + fn deserialize<'a, A: SeqAccess<'a>>(seq: &mut A) -> Result { + const MAX_BUNDLE_ITEMS: usize = 64; + + let len = seq + .next_element::()? + .ok_or(de::Error::custom("No BundleContentsImpl len VarInt!"))? + .0 as usize; + + if len > MAX_BUNDLE_ITEMS { + return Err(de::Error::custom("Too many items in BundleContents")); + } + + let mut items = Vec::with_capacity(len); + for _ in 0..len { + items.push(deserialize_item_stack_template(seq)?); + } + Ok(Self { items }) + } +} diff --git a/pumpkin-protocol/src/java/server/play/bundle_item_selected.rs b/pumpkin-protocol/src/java/server/play/bundle_item_selected.rs new file mode 100644 index 000000000..891bf5a06 --- /dev/null +++ b/pumpkin-protocol/src/java/server/play/bundle_item_selected.rs @@ -0,0 +1,12 @@ +use pumpkin_data::packet::serverbound::PLAY_BUNDLE_ITEM_SELECTED; +use pumpkin_macros::java_packet; +use serde::{Deserialize, Serialize}; + +use crate::VarInt; + +#[derive(Deserialize, Serialize)] +#[java_packet(PLAY_BUNDLE_ITEM_SELECTED)] +pub struct SBundleItemSelected { + pub slot_id: VarInt, + pub selected_item_index: VarInt, +} diff --git a/pumpkin-protocol/src/java/server/play/mod.rs b/pumpkin-protocol/src/java/server/play/mod.rs index 300fbf3a5..d788df59d 100644 --- a/pumpkin-protocol/src/java/server/play/mod.rs +++ b/pumpkin-protocol/src/java/server/play/mod.rs @@ -1,4 +1,5 @@ mod attack; +mod bundle_item_selected; mod change_game_mode; mod chat_command; mod chat_message; @@ -47,6 +48,7 @@ mod use_item; mod use_item_on; pub use attack::*; +pub use bundle_item_selected::*; pub use change_game_mode::*; pub use chat_command::*; pub use chat_message::*; diff --git a/pumpkin/src/item/items/bundle.rs b/pumpkin/src/item/items/bundle.rs new file mode 100644 index 000000000..34de1bbc5 --- /dev/null +++ b/pumpkin/src/item/items/bundle.rs @@ -0,0 +1,81 @@ +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::data_component_impl::BundleContentsImpl; +use pumpkin_data::item::Item; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag; + +pub struct BundleItem; + +impl ItemMetadata for BundleItem { + fn ids() -> Box<[u16]> { + tag::Item::MINECRAFT_BUNDLES.1.into() + } +} + +impl ItemBehaviour for BundleItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let held_item_ref = player.inventory.held_item(); + let mut held_item = held_item_ref.lock().await; + let mut matched = false; + let mut used_slot_index = player.inventory.get_selected_slot() as usize; + + if !held_item.is_empty() && Self::ids().contains(&held_item.item.id) { + matched = true; + if let Some(bundle_contents) = + held_item.get_data_component_mut::() + { + if let Some(extracted_stack) = bundle_contents.try_extract() { + let position = player.position(); + player.world().play_sound( + Sound::ItemBundleRemoveOne, + pumpkin_data::sound::SoundCategory::Players, + &position, + ); + let updated_bundle = held_item.clone(); + drop(held_item); + + player.drop_item(extracted_stack).await; + player.sync_hand_slot(used_slot_index, updated_bundle).await; + } + } + } + + if !matched { + let off_hand_item_ref = player.inventory.off_hand_item().await; + let mut off_hand_item = off_hand_item_ref.lock().await; + if !off_hand_item.is_empty() && Self::ids().contains(&off_hand_item.item.id) { + used_slot_index = 40; // OFF_HAND_SLOT + if let Some(bundle_contents) = + off_hand_item.get_data_component_mut::() + { + if let Some(extracted_stack) = bundle_contents.try_extract() { + let position = player.position(); + player.world().play_sound( + Sound::ItemBundleRemoveOne, + pumpkin_data::sound::SoundCategory::Players, + &position, + ); + let updated_bundle = off_hand_item.clone(); + drop(off_hand_item); + + player.drop_item(extracted_stack).await; + player.sync_hand_slot(used_slot_index, updated_bundle).await; + } + } + } + } + }) + } + + 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 31c44aded..42cdbdc6b 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -4,6 +4,7 @@ pub mod axe; pub mod boat; pub mod bow; pub mod bucket; +pub mod bundle; pub mod crossbow; pub mod dye; pub mod egg; @@ -31,6 +32,7 @@ pub mod wind_charge; 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::map::MapItem; use crate::item::items::minecart::MinecartItem; @@ -105,6 +107,7 @@ pub fn default_registry() -> Arc { manager.register(PotionItem); manager.register(SplashPotionItem); manager.register(LingeringPotionItem); + manager.register(BundleItem); Arc::new(manager) } diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index 59edad4b0..2dcf69159 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -13,11 +13,12 @@ use pumpkin_config::networking::compression::CompressionInfo; use pumpkin_data::packet::CURRENT_MC_VERSION; use pumpkin_data::translation; use pumpkin_protocol::java::server::play::{ - SAttack, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand, - SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport, - SContainerButtonClick, SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, - SJigsawGenerate, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, - SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, + SAttack, SBundleItemSelected, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, + SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer, + SCommandSuggestion, SConfirmTeleport, SContainerButtonClick, + SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SJigsawGenerate, SMoveVehicle, + SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, + SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SRenameItem, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, @@ -911,6 +912,13 @@ impl JavaClient { self.handle_interact(player, SInteract::read(payload, &version)?, server) .await; } + id if id == SBundleItemSelected::to_id(version) => { + self.handle_bundle_item_selected( + player, + SBundleItemSelected::read(payload, &version)?, + ) + .await; + } id if id == SAttack::to_id(version) => { self.handle_attack(player, SAttack::read(payload, &version)?, server) .await; diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index ff9e55f02..e393a5dff 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -65,15 +65,15 @@ use pumpkin_protocol::java::client::play::{ CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction, }; use pumpkin_protocol::java::server::play::{ - Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SChangeGameMode, SChatCommand, - SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer, - SCommandSuggestion, SConfirmTeleport, SCookieResponse as SPCookieResponse, SInteract, - SJigsawGenerate, SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, - SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, - SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, - SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SSelectTrade, SSetCommandBlock, - SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSwingArm, SUpdateSign, - SUseItem, SUseItemOn, Status, + Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected, + SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand, + SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, + SCookieResponse as SPCookieResponse, SInteract, SJigsawGenerate, SKeepAlive, SMoveVehicle, + SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, + SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, + SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, + SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, + SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status, }; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::vector3::Vector3; @@ -2919,4 +2919,27 @@ impl JavaClient { .await; } } + + pub async fn handle_bundle_item_selected( + &self, + player: &Arc, + packet: SBundleItemSelected, + ) { + if !player.has_client_loaded() { + return; + } + player.update_last_action_time(); + + let selected_item_index = packet.selected_item_index.0; + if selected_item_index < 0 && selected_item_index != -1 { + self.kick(TextComponent::text("Invalid selected item index")) + .await; + return; + } + + debug!( + "Bundle item selected: Slot ID {}, Selected Item Index {}", + packet.slot_id.0, selected_item_index + ); + } }