mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add bundles
This commit is contained in:
@@ -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<T: SerializeStruct>(
|
||||
DataComponent::StoredEnchantments => get::<StoredEnchantmentsImpl>(value).serialize(seq),
|
||||
DataComponent::UseCooldown => get::<UseCooldownImpl>(value).serialize(seq),
|
||||
DataComponent::MapId => get::<MapIdImpl>(value).serialize(seq),
|
||||
DataComponent::BundleContents => get::<BundleContentsImpl>(value).serialize(seq),
|
||||
_ => Err(serde::ser::Error::custom(format!(
|
||||
"{} not yet implemented",
|
||||
id.to_name()
|
||||
@@ -910,3 +912,134 @@ impl DataComponentCodec<Self> for UseCooldownImpl {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_item_stack_template<'a, A: SeqAccess<'a>>(
|
||||
seq: &mut A,
|
||||
) -> Result<pumpkin_data::item_stack::ItemStack, A::Error> {
|
||||
let item_id = seq
|
||||
.next_element::<VarInt>()?
|
||||
.ok_or_else(|| de::Error::custom("Missing item_id in ItemStackTemplate"))?
|
||||
.0 as u16;
|
||||
|
||||
let count = seq
|
||||
.next_element::<VarInt>()?
|
||||
.ok_or_else(|| de::Error::custom("Missing count in ItemStackTemplate"))?
|
||||
.0 as u8;
|
||||
|
||||
let num_to_add = seq.next_element::<VarInt>()?.map_or(0, |v| v.0);
|
||||
let num_to_remove = seq.next_element::<VarInt>()?.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::<VarInt>()?
|
||||
.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::<VarInt>()?
|
||||
.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::<VarInt>()?
|
||||
.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<T: SerializeStruct>(
|
||||
stack: &pumpkin_data::item_stack::ItemStack,
|
||||
seq: &mut T,
|
||||
) -> Result<(), T::Error> {
|
||||
seq.serialize_field::<VarInt>("", &VarInt::from(stack.item.id))?;
|
||||
seq.serialize_field::<VarInt>("", &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>("", &VarInt::from(to_add))?;
|
||||
seq.serialize_field::<VarInt>("", &VarInt::from(to_remove))?;
|
||||
|
||||
for (id, data) in &stack.patch {
|
||||
if let Some(data) = data {
|
||||
seq.serialize_field::<VarInt>("", &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>("", &VarInt::from(id.to_id()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for BundleContentsImpl {
|
||||
fn serialize<T: SerializeStruct>(&self, seq: &mut T) -> Result<(), T::Error> {
|
||||
seq.serialize_field::<VarInt>("", &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<Self, A::Error> {
|
||||
const MAX_BUNDLE_ITEMS: usize = 64;
|
||||
|
||||
let len = seq
|
||||
.next_element::<VarInt>()?
|
||||
.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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
81
pumpkin/src/item/items/bundle.rs
Normal file
81
pumpkin/src/item/items/bundle.rs
Normal file
@@ -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<Box<dyn Future<Output = ()> + 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::<BundleContentsImpl>()
|
||||
{
|
||||
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::<BundleContentsImpl>()
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<ItemRegistry> {
|
||||
manager.register(PotionItem);
|
||||
manager.register(SplashPotionItem);
|
||||
manager.register(LingeringPotionItem);
|
||||
manager.register(BundleItem);
|
||||
|
||||
Arc::new(manager)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Player>,
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user