diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index 3a230385..f406ba5a 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -116,6 +116,35 @@ impl PlayerInventory { &mut self.items[self.selected + 36 - 9] } + pub fn get_slot_with_item(&self, item_id: u16) -> Option { + for slot in 9..=44 { + match &self.items[slot - 9] { + Some(item) if item.item_id == item_id => return Some(slot), + _ => continue, + } + } + + None + } + + pub fn get_pick_item_hotbar_slot(&self) -> usize { + if self.items[self.selected + 36 - 9].is_none() { + return self.selected; + } + + for slot in 0..9 { + if self.items[slot + 36 - 9].is_none() { + return slot; + } + } + + self.selected + } + + pub fn get_empty_slot(&self) -> Option { + (9..=44).find(|&slot| self.items[slot - 9].is_none()) + } + pub fn slots(&self) -> Vec> { let mut slots = vec![self.crafting_output.as_ref()]; slots.extend(self.crafting.iter().map(|c| c.as_ref())); diff --git a/pumpkin-protocol/src/server/play/mod.rs b/pumpkin-protocol/src/server/play/mod.rs index 3fa607d6..3995f332 100644 --- a/pumpkin-protocol/src/server/play/mod.rs +++ b/pumpkin-protocol/src/server/play/mod.rs @@ -10,6 +10,7 @@ mod s_confirm_teleport; mod s_cookie_response; mod s_interact; mod s_keep_alive; +mod s_pick_item; mod s_ping_request; mod s_player_abilities; mod s_player_action; @@ -37,6 +38,7 @@ pub use s_confirm_teleport::*; pub use s_cookie_response::*; pub use s_interact::*; pub use s_keep_alive::*; +pub use s_pick_item::*; pub use s_ping_request::*; pub use s_player_abilities::*; pub use s_player_action::*; diff --git a/pumpkin-protocol/src/server/play/s_pick_item.rs b/pumpkin-protocol/src/server/play/s_pick_item.rs new file mode 100644 index 00000000..8ff25b1d --- /dev/null +++ b/pumpkin-protocol/src/server/play/s_pick_item.rs @@ -0,0 +1,17 @@ +use pumpkin_core::math::position::WorldPosition; +use pumpkin_macros::server_packet; +use serde::Deserialize; + +#[derive(Deserialize)] +#[server_packet("play:pick_item_from_block")] +pub struct SPickItemFromBlock { + pub pos: WorldPosition, + pub include_data: bool, +} + +#[derive(Deserialize)] +#[server_packet("play:pick_item_from_entity")] +pub struct SPickItemFromEntity { + pub id: i32, + pub include_data: bool, +} diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index c197f58f..5d58a7d7 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -37,9 +37,10 @@ use pumpkin_protocol::{ }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, - SCommandSuggestion, SConfirmTeleport, SInteract, SPlayerAbilities, SPlayerAction, - SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, - SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem, SUseItemOn, + SCommandSuggestion, SConfirmTeleport, SInteract, SPickItemFromBlock, SPlayerAbilities, + SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, + SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem, + SUseItemOn, }, RawPacket, ServerPacket, SoundCategory, }; @@ -675,6 +676,7 @@ impl Player { } } + #[allow(clippy::too_many_lines)] pub async fn handle_play_packet( self: &Arc, server: &Arc, @@ -726,6 +728,10 @@ impl Player { SSetPlayerGround::PACKET_ID => { self.handle_player_ground(&SSetPlayerGround::read(bytebuf)?); } + SPickItemFromBlock::PACKET_ID => { + self.handle_pick_item_from_block(SPickItemFromBlock::read(bytebuf)?) + .await; + } SPlayerAbilities::PACKET_ID => { self.handle_player_abilities(SPlayerAbilities::read(bytebuf)?) .await; diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index baf90465..d5caf923 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -18,7 +18,10 @@ use pumpkin_core::{ text::TextComponent, GameMode, }; +use pumpkin_inventory::player::PlayerInventory; use pumpkin_inventory::InventoryError; +use pumpkin_protocol::client::play::{CSetContainerSlot, CSetHeldItem}; +use pumpkin_protocol::codec::slot::Slot; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse; use pumpkin_protocol::{ @@ -32,13 +35,14 @@ use pumpkin_protocol::{ }, server::play::{ Action, ActionType, SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, - SConfirmTeleport, SInteract, SPlayPingRequest, SPlayerAbilities, SPlayerAction, - SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, - SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status, + SConfirmTeleport, SInteract, SPickItemFromBlock, SPickItemFromEntity, SPlayPingRequest, + SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, + SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status, }, }; use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace}; use pumpkin_world::item::item_registry::get_item_by_id; +use pumpkin_world::item::ItemStack; use thiserror::Error; fn modulus(a: f32, b: f32) -> f32 { @@ -310,6 +314,101 @@ impl Player { .store(ground.on_ground, std::sync::atomic::Ordering::Relaxed); } + async fn update_single_slot( + &self, + inventory: &mut tokio::sync::MutexGuard<'_, PlayerInventory>, + slot: usize, + slot_data: Slot, + ) { + inventory.state_id += 1; + let dest_packet = CSetContainerSlot::new(0, inventory.state_id as i32, slot, &slot_data); + self.client.send_packet(&dest_packet).await; + + if inventory + .set_slot(slot, slot_data.to_item(), false) + .is_err() + { + log::error!("Pick item set slot error!"); + } + } + + pub async fn handle_pick_item_from_block(&self, pick_item: SPickItemFromBlock) { + if !self.can_interact_with_block_at(&pick_item.pos, 1.0) { + return; + } + + let Ok(block) = self.world().get_block(pick_item.pos).await else { + return; + }; + + if block.item_id == 0 { + // Invalid block id (blocks such as tall seagrass) + return; + } + + let mut inventory = self.inventory().lock().await; + + let source_slot = inventory.get_slot_with_item(block.item_id); + let mut dest_slot = inventory.get_pick_item_hotbar_slot(); + + let dest_slot_data = match inventory.get_slot(dest_slot + 36) { + Ok(Some(stack)) => Slot::from(&*stack), + _ => Slot::from(None), + }; + + // Early return if no source slot and not in creative mode + if source_slot.is_none() && self.gamemode.load() != GameMode::Creative { + return; + } + + match source_slot { + Some(slot_index) if (36..=44).contains(&slot_index) => { + // Case where item is in hotbar + dest_slot = slot_index - 36; + } + Some(slot_index) => { + // Case where item is in inventory + + // Update destination slot + let source_slot_data = match inventory.get_slot(slot_index) { + Ok(Some(stack)) => Slot::from(&*stack), + _ => return, + }; + self.update_single_slot(&mut inventory, dest_slot + 36, source_slot_data) + .await; + + // Update source slot + self.update_single_slot(&mut inventory, slot_index, dest_slot_data) + .await; + } + None if self.gamemode.load() == GameMode::Creative => { + // Case where item is not present, if in creative mode create the item + let item_stack = ItemStack::new(1, block.item_id); + let slot_data = Slot::from(&item_stack); + self.update_single_slot(&mut inventory, dest_slot + 36, slot_data) + .await; + + // Check if there is any empty slot in the player inventory + if let Some(slot_index) = inventory.get_empty_slot() { + inventory.state_id += 1; + self.update_single_slot(&mut inventory, slot_index, dest_slot_data) + .await; + } + } + _ => return, + } + + // Update held item + inventory.set_selected(dest_slot); + self.client + .send_packet(&CSetHeldItem::new(dest_slot as i8)) + .await; + } + + pub fn handle_pick_item_from_entity(&self, _pick_item: SPickItemFromEntity) { + // TODO: Implement and merge any redundant code with pick_item_from_block + } + pub async fn handle_player_command(&self, command: SPlayerCommand) { if command.entity_id != self.entity_id().into() { return;