From 8911e84094db161decf2070317d8ddcb7043a6de Mon Sep 17 00:00:00 2001 From: Dominik <80042910+tomasalias@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:49:05 +0100 Subject: [PATCH 1/4] Support dropping items Implement item dropping functionality. * **`pumpkin/src/net/container.rs`** - Add `DropType` to the `use` statement. - Handle `ClickType::DropType` in the `match_click_behaviour` function. - Implement `handle_drop_item` function to handle item dropping logic. * **`pumpkin/src/net/packet/play.rs`** - Handle `SPlayerAction::DropItem` and `SPlayerAction::DropItemStack` actions. - Implement `handle_click_container` function to handle container clicks. - Implement `handle_decrease_item` function to handle item decrease. - Implement `match_click_behaviour` function to match click behavior. - Implement `mouse_click`, `shift_mouse_click`, `number_button_pressed`, `creative_pick_item`, `double_click`, and `mouse_drag` functions to handle various click behaviors. - Implement `handle_drop_item` function to handle item dropping logic. - Implement `get_current_players_in_container` function to get current players in the container. - Implement `send_container_changes` and `send_whole_container_change` functions to send container changes. - Implement `get_open_container` function to get the open container. - Implement `pickup_items` and `give_items` functions to handle item pickup and giving. - Implement `drop_item` and `drop_item_stack` functions to handle item dropping and dropping item stacks. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/tomasalias/Pumpkin?shareId=XXXX-XXXX-XXXX-XXXX). --- pumpkin/src/net/container.rs | 40 ++- pumpkin/src/net/packet/play.rs | 580 ++++++++++++++++++++++++++++++++- 2 files changed, 612 insertions(+), 8 deletions(-) diff --git a/pumpkin/src/net/container.rs b/pumpkin/src/net/container.rs index 7132de47..12fa0991 100644 --- a/pumpkin/src/net/container.rs +++ b/pumpkin/src/net/container.rs @@ -3,7 +3,7 @@ use crate::server::Server; use pumpkin_core::text::TextComponent; use pumpkin_core::GameMode; use pumpkin_inventory::container_click::{ - Click, ClickType, KeyClick, MouseClick, MouseDragState, MouseDragType, + Click, ClickType, KeyClick, MouseClick, MouseDragState, MouseDragType, DropType, }; use pumpkin_inventory::drag_handler::DragHandler; use pumpkin_inventory::window_property::{WindowProperty, WindowPropertyTrait}; @@ -272,9 +272,9 @@ impl Player { self.mouse_drag(drag_handler, opened_container, drag_state) .await } - ClickType::DropType(_drop_type) => { - log::debug!("todo"); - Ok(()) + ClickType::DropType(drop_type) => { + self.handle_drop_item(opened_container, drop_type, click.slot) + .await } } } @@ -469,6 +469,38 @@ impl Player { } } + async fn handle_drop_item( + &self, + opened_container: Option<&mut Box>, + drop_type: DropType, + slot: container_click::Slot, + ) -> Result<(), InventoryError> { + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + match slot { + container_click::Slot::Normal(slot) => { + let mut carried_item = self.carried_item.load(); + let res = match drop_type { + DropType::SingleItem => container.handle_item_change( + &mut carried_item, + slot, + MouseClick::Right, + false, + ), + DropType::FullStack => container.handle_item_change( + &mut carried_item, + slot, + MouseClick::Left, + false, + ), + }; + self.carried_item.store(carried_item); + res + } + container_click::Slot::OutsideInventory => Ok(()), + } + } + async fn get_current_players_in_container(&self, server: &Server) -> Vec> { let player_ids: Vec = { let open_containers = server.open_containers.read().await; diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index 7f402ce9..50ed3caf 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -153,7 +153,7 @@ impl Player { // // Player is falling down fast, we should account for that // let max_speed = if self.fall_flying { 300.0 } else { 100.0 }; - // teleport when more than 8 blocks (i guess 8 blocks) + // // teleport when more than 8 blocks (i guess 8 blocks) // TODO: REPLACE * 2.0 by movement packets. see vanilla for details // if delta.length_squared() - velocity.length_squared() > max_speed * 2.0 { // self.teleport(x, y, z, self.entity.yaw, self.entity.pitch); @@ -757,9 +757,13 @@ impl Player { .await; } } - Status::DropItemStack - | Status::DropItem - | Status::ShootArrowOrFinishEating + Status::DropItemStack => { + self.drop_item_stack().await; + } + Status::DropItem => { + self.drop_item().await; + } + Status::ShootArrowOrFinishEating | Status::SwapItem => { log::debug!("todo"); } @@ -1095,4 +1099,572 @@ impl Player { .await; Ok(true) } + + pub async fn handle_click_container( + &self, + server: &Arc, + packet: SClickContainer, + ) -> Result<(), InventoryError> { + let opened_container = self.get_open_container(server).await; + let mut opened_container = match opened_container.as_ref() { + Some(container) => Some(container.lock().await), + None => None, + }; + let drag_handler = &server.drag_handler; + + let state_id = self.inventory().lock().await.state_id; + // This is just checking for regular desync, client hasn't done anything malicious + if state_id != packet.state_id.0 as u32 { + self.set_container_content(opened_container.as_deref_mut()) + .await; + return Ok(()); + } + + if opened_container.is_some() { + let total_containers = self.inventory().lock().await.total_opened_containers; + if packet.window_id.0 != total_containers { + return Err(InventoryError::ClosedContainerInteract(self.entity_id())); + } + } else if packet.window_id.0 != 0 { + return Err(InventoryError::ClosedContainerInteract(self.entity_id())); + } + + let click = Click::new(packet.mode, packet.button, packet.slot)?; + let (crafted_item, crafted_item_slot) = { + let mut inventory = self.inventory().lock().await; + let combined = + OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut()); + ( + combined.crafted_item_slot(), + combined.crafting_output_slot(), + ) + }; + let crafted_is_picked = crafted_item.is_some() + && match click.slot { + container_click::Slot::Normal(slot) => { + crafted_item_slot.is_some_and(|crafted_slot| crafted_slot == slot) + } + container_click::Slot::OutsideInventory => false, + }; + let mut update_whole_container = false; + + let click_slot = click.slot; + self.match_click_behaviour( + opened_container.as_deref_mut(), + click, + drag_handler, + &mut update_whole_container, + crafted_is_picked, + ) + .await?; + // Checks for if crafted item has been taken + { + let mut inventory = self.inventory().lock().await; + let mut combined = + OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut()); + if combined.crafted_item_slot().is_none() && crafted_item.is_some() { + combined.recipe_used(); + } + + // TODO: `combined.craft` uses rayon! It should be called from `rayon::spawn` and its + // result passed to the tokio runtime via a channel! + if combined.craft() { + drop(inventory); + self.set_container_content(opened_container.as_deref_mut()) + .await; + } + } + + if let Some(mut opened_container) = opened_container { + if update_whole_container { + drop(opened_container); + self.send_whole_container_change(server).await?; + } else if let container_click::Slot::Normal(slot_index) = click_slot { + let mut inventory = self.inventory().lock().await; + let combined_container = + OptionallyCombinedContainer::new(&mut inventory, Some(&mut opened_container)); + if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) { + let slot = Slot::from(slot); + drop(opened_container); + self.send_container_changes(server, slot_index, slot) + .await?; + } + } + } + Ok(()) + } + + pub async fn handle_decrease_item( + &self, + _server: &Server, + slot_index: usize, + item_stack: Option<&ItemStack>, + state_id: &mut u32, + ) -> Result<(), InventoryError> { + // TODO: this will not update hotbar when server admin is peeking + // TODO: check and iterate over all players in player inventory + let slot = Slot::from(item_stack); + *state_id += 1; + let packet = CSetContainerSlot::new(0, *state_id as i32, slot_index, &slot); + self.client.send_packet(&packet).await; + Ok(()) + } + + async fn match_click_behaviour( + &self, + opened_container: Option<&mut Box>, + click: Click, + drag_handler: &DragHandler, + update_whole_container: &mut bool, + using_crafting_slot: bool, + ) -> Result<(), InventoryError> { + match click.click_type { + ClickType::MouseClick(mouse_click) => { + self.mouse_click( + opened_container, + mouse_click, + click.slot, + using_crafting_slot, + ) + .await + } + ClickType::ShiftClick => { + self.shift_mouse_click(opened_container, click.slot, using_crafting_slot) + .await + } + ClickType::KeyClick(key_click) => match click.slot { + container_click::Slot::Normal(slot) => { + self.number_button_pressed( + opened_container, + key_click, + slot, + using_crafting_slot, + ) + .await + } + container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket), + }, + ClickType::CreativePickItem => { + if let container_click::Slot::Normal(slot) = click.slot { + self.creative_pick_item(opened_container, slot).await + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::DoubleClick => { + *update_whole_container = true; + if let container_click::Slot::Normal(slot) = click.slot { + self.double_click(opened_container, slot).await + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::MouseDrag { drag_state } => { + if (drag_state == MouseDragState::End) { + *update_whole_container = true; + } + self.mouse_drag(drag_handler, opened_container, drag_state) + .await + } + ClickType::DropType(drop_type) => { + self.handle_drop_item(opened_container, drop_type, click.slot) + .await + } + } + } + + async fn mouse_click( + &self, + opened_container: Option<&mut Box>, + mouse_click: MouseClick, + slot: container_click::Slot, + taking_crafted: bool, + ) -> Result<(), InventoryError> { + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + match slot { + container_click::Slot::Normal(slot) => { + let mut carried_item = self.carried_item.load(); + let res = container.handle_item_change( + &mut carried_item, + slot, + mouse_click, + taking_crafted, + ); + self.carried_item.store(carried_item); + res + } + container_click::Slot::OutsideInventory => Ok(()), + } + } + + async fn shift_mouse_click( + &self, + opened_container: Option<&mut Box>, + slot: container_click::Slot, + taking_crafted: bool, + ) -> Result<(), InventoryError> { + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + + match slot { + container_click::Slot::Normal(slot) => { + let all_slots = container.all_slots(); + if let Some(item_in_pressed_slot) = all_slots[slot].to_owned() { + let slots = all_slots.into_iter().enumerate(); + // Hotbar + let find_condition = |(slot_number, slot): (usize, &mut Option)| { + // TODO: Check for max item count here + match slot { + Some(item) => (item.item_id == item_in_pressed_slot.item_id + && item.item_count != 64) + .then_some(slot_number), + None => Some(slot_number), + } + }; + + let slots = if slot > 35 { + slots.skip(9).find_map(find_condition) + } else { + slots.skip(36).rev().find_map(find_condition) + }; + if let Some(slot) = slots { + let mut item_slot = container.all_slots()[slot].map(|i| i); + container.handle_item_change( + &mut item_slot, + slot, + MouseClick::Left, + taking_crafted, + )?; + *container.all_slots()[slot] = item_slot; + } + } + } + container_click::Slot::OutsideInventory => (), + }; + Ok(()) + } + + async fn number_button_pressed( + &self, + opened_container: Option<&mut Box>, + key_click: KeyClick, + slot: usize, + taking_crafted: bool, + ) -> Result<(), InventoryError> { + let changing_slot = match key_click { + KeyClick::Slot(slot) => slot, + KeyClick::Offhand => 45, + }; + let mut inventory = self.inventory().lock().await; + let mut changing_item_slot = inventory.get_slot(changing_slot as usize)?.to_owned(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + + container.handle_item_change( + &mut changing_item_slot, + slot, + MouseClick::Left, + taking_crafted, + )?; + *inventory.get_slot(changing_slot as usize)? = changing_item_slot; + Ok(()) + } + + async fn creative_pick_item( + &self, + opened_container: Option<&mut Box>, + slot: usize, + ) -> Result<(), InventoryError> { + if self.gamemode.load() != GameMode::Creative { + return Err(InventoryError::PermissionError); + } + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + if let Some(Some(item)) = container.all_slots().get_mut(slot) { + self.carried_item.store(Some(item.to_owned())); + } + Ok(()) + } + + async fn double_click( + &self, + opened_container: Option<&mut Box>, + slot: usize, + ) -> Result<(), InventoryError> { + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + let mut slots = container.all_slots(); + + let Some(item) = slots.get_mut(slot) else { + return Ok(()); + }; + let Some(mut carried_item) = **item else { + return Ok(()); + }; + **item = None; + + for slot in slots.iter_mut().filter_map(|slot| slot.as_mut()) { + if slot.item_id == carried_item.item_id { + // TODO: Check for max stack size + if slot.item_count + carried_item.item_count <= 64 { + slot.item_count = 0; + carried_item.item_count = 64; + } else { + let to_remove = slot.item_count - (64 - carried_item.item_count); + slot.item_count -= to_remove; + carried_item.item_count += to_remove; + } + + if carried_item.item_count == 64 { + break; + } + } + } + self.carried_item.store(Some(carried_item)); + Ok(()) + } + + async fn mouse_drag( + &self, + drag_handler: &DragHandler, + opened_container: Option<&mut Box>, + mouse_drag_state: MouseDragState, + ) -> Result<(), InventoryError> { + let player_id = self.entity_id(); + let container_id = opened_container + .as_ref() + .map_or(player_id as u64, |container| { + container.internal_pumpkin_id() + }); + match mouse_drag_state { + MouseDragState::Start(drag_type) => { + if drag_type == MouseDragType::Middle && self.gamemode.load() != GameMode::Creative + { + Err(InventoryError::PermissionError)?; + } + drag_handler + .new_drag(container_id, player_id, drag_type) + .await + } + MouseDragState::AddSlot(slot) => { + drag_handler.add_slot(container_id, player_id, slot).await + } + MouseDragState::End => { + let mut inventory = self.inventory().lock().await; + let mut container = + OptionallyCombinedContainer::new(&mut inventory, opened_container); + let mut carried_item = self.carried_item.load(); + let res = drag_handler + .apply_drag(&mut carried_item, &mut container, &container_id, player_id) + .await; + self.carried_item.store(carried_item); + res + } + } + } + + async fn handle_drop_item( + &self, + opened_container: Option<&mut Box>, + drop_type: DropType, + slot: container_click::Slot, + ) -> Result<(), InventoryError> { + let mut inventory = self.inventory().lock().await; + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); + match slot { + container_click::Slot::Normal(slot) => { + let mut carried_item = self.carried_item.load(); + let res = match drop_type { + DropType::SingleItem => container.handle_item_change( + &mut carried_item, + slot, + MouseClick::Right, + false, + ), + DropType::FullStack => container.handle_item_change( + &mut carried_item, + slot, + MouseClick::Left, + false, + ), + }; + self.carried_item.store(carried_item); + res + } + container_click::Slot::OutsideInventory => Ok(()), + } + } + + async fn get_current_players_in_container(&self, server: &Server) -> Vec> { + let player_ids: Vec = { + let open_containers = server.open_containers.read().await; + open_containers + .get(&self.open_container.load().unwrap()) + .unwrap() + .all_player_ids() + .into_iter() + .filter(|player_id| *player_id != self.entity_id()) + .collect() + }; + let player_token = self.gameprofile.id; + + // TODO: Figure out better way to get only the players from player_ids + // Also refactor out a better method to get individual advanced state ids + + let players = self + .living_entity + .entity + .world + .current_players + .lock() + .await + .iter() + .filter_map(|(token, player)| { + if *token == player_token { + None + } else { + let entity_id = player.entity_id(); + player_ids.contains(&entity_id).then(|| player.clone()) + } + }) + .collect(); + players + } + + async fn send_container_changes( + &self, + server: &Server, + slot_index: usize, + slot: Slot, + ) -> Result<(), InventoryError> { + for player in self.get_current_players_in_container(server).await { + let mut inventory = player.inventory().lock().await; + let total_opened_containers = inventory.total_opened_containers; + + // Returns previous value + inventory.state_id += 1; + let packet = CSetContainerSlot::new( + total_opened_containers as i8, + (inventory.state_id) as i32, + slot_index, + &slot, + ); + player.client.send_packet(&packet).await; + } + Ok(()) + } + + pub async fn send_whole_container_change(&self, server: &Server) -> Result<(), InventoryError> { + let players = self.get_current_players_in_container(server).await; + + for player in players { + let container = player.get_open_container(server).await; + let mut container = match container.as_ref() { + Some(container) => Some(container.lock().await), + None => None, + }; + player.set_container_content(container.as_deref_mut()).await; + } + Ok(()) + } + + pub async fn get_open_container( + &self, + server: &Server, + ) -> Option>>> { + match self.open_container.load() { + Some(id) => server.try_get_container(self.entity_id(), id).await, + None => None, + } + } + + async fn pickup_items(&self, item: &Item, mut amount: u32) { + let max_stack = item.components.max_stack_size; + let mut inventory = self.inventory().lock().await; + let slots = inventory.slots_with_hotbar_first(); + + let matching_slots = slots.filter_map(|slot| { + if let Some(item_slot) = slot.as_mut() { + (item_slot.item_id == item.id && item_slot.item_count < max_stack).then(|| { + let item_count = item_slot.item_count; + (item_slot, item_count) + }) + } else { + None + } + }); + + for (slot, item_count) in matching_slots { + if amount == 0 { + return; + } + let amount_to_add = max_stack - item_count; + if let Some(amount_left) = amount.checked_sub(u32::from(amount_to_add)) { + amount = amount_left; + *slot = ItemStack { + item_id: item.id, + item_count: item.components.max_stack_size, + }; + } else { + *slot = ItemStack { + item_id: item.id, + item_count: max_stack - (amount_to_add - amount as u8), + }; + return; + } + } + + let empty_slots = inventory + .slots_with_hotbar_first() + .filter(|slot| slot.is_none()); + for slot in empty_slots { + if amount == 0 { + return; + } + if let Some(remaining_amount) = amount.checked_sub(u32::from(max_stack)) { + amount = remaining_amount; + *slot = Some(ItemStack { + item_id: item.id, + item_count: max_stack, + }); + } else { + *slot = Some(ItemStack { + item_id: item.id, + item_count: amount as u8, + }); + return; + } + } + log::warn!( + "{amount} items were discarded because dropping them to the ground is not implemented" + ); + } + + /// Add items to inventory if there's space, else drop them to the ground. + /// + /// This method automatically syncs changes with the client. + pub async fn give_items(&self, item: &Item, amount: u32) { + self.pickup_items(item, amount).await; + self.set_container_content(None).await; + } + + pub async fn drop_item(&self) { + let mut inventory = self.inventory().lock().await; + let slot = inventory.get_selected(); + if let Some(item) = inventory.get_slot(slot).unwrap().as_mut() { + if item.item_count > 1 { + item.item_count -= 1; + } else { + inventory.set_slot(slot, None, false).unwrap(); + } + } + self.set_container_content(None).await; + } + + pub async fn drop_item_stack(&self) { + let mut inventory = self.inventory().lock().await; + let slot = inventory.get_selected(); + inventory.set_slot(slot, None, false).unwrap(); + self.set_container_content(None).await; + } } From 5209ea7b1551e0b501f267884509b2a3b1017e52 Mon Sep 17 00:00:00 2001 From: Dominik <80042910+tomasalias@users.noreply.github.com> Date: Fri, 3 Jan 2025 21:38:05 +0100 Subject: [PATCH 2/4] Update README.md From f837a76b64410f9e60b62d6596a5c471c6d672f1 Mon Sep 17 00:00:00 2001 From: Dominik <80042910+tomasalias@users.noreply.github.com> Date: Fri, 3 Jan 2025 21:56:20 +0100 Subject: [PATCH 3/4] Update play.rs --- pumpkin/src/net/packet/play.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index 50ed3caf..fe8e1c08 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -51,6 +51,9 @@ use pumpkin_world::{ }; use thiserror::Error; +use pumpkin_inventory::container_click::{Click, ClickType, DropType, MouseClick, MouseDragState, MouseDragType, KeyClick}; +use pumpkin_inventory::OptionallyCombinedContainer; + #[derive(Debug, Error)] pub enum BlockPlacingError { BlockOutOfReach, From a590ae5b76c7d6d86929f8db48a0a33a446eabb6 Mon Sep 17 00:00:00 2001 From: Dominik <80042910+tomasalias@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:21:12 +0100 Subject: [PATCH 4/4] Update play.rs --- pumpkin/src/net/packet/play.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index fe8e1c08..71df7d6c 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -53,6 +53,7 @@ use thiserror::Error; use pumpkin_inventory::container_click::{Click, ClickType, DropType, MouseClick, MouseDragState, MouseDragType, KeyClick}; use pumpkin_inventory::OptionallyCombinedContainer; +use pumpkin_inventory::container_click; #[derive(Debug, Error)] pub enum BlockPlacingError {