From b428fdca1a8fed32ff423516fd667eb3d5689778 Mon Sep 17 00:00:00 2001 From: 4lve <72332750+4lve@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:51:48 +0100 Subject: [PATCH] Proper item stacking (#562) * Implement item stacking * Fix clippy issues * Fix comments * Some improvements * Fix item picking bug * Fully fix pickblock * Pioritize item held --------- Co-authored-by: Alexander Medvedev --- pumpkin-inventory/src/player.rs | 84 +++++++++++++++++++++++++++------ pumpkin/src/block/mod.rs | 2 +- pumpkin/src/entity/item.rs | 83 +++++++++++++++++++++----------- pumpkin/src/entity/player.rs | 2 +- pumpkin/src/net/packet/play.rs | 19 ++++---- 5 files changed, 136 insertions(+), 54 deletions(-) diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index 5d52bcfb3..b6eb2fa31 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -1,10 +1,22 @@ use crate::container_click::MouseClick; use crate::crafting::check_if_matches_crafting; use crate::{handle_item_change, Container, InventoryError, WindowType}; +use pumpkin_data::item::Item; use pumpkin_world::item::ItemStack; use std::iter::Chain; use std::slice::IterMut; +/* + Inventory Layout: + - 0: Crafting Output + - 1-4: Crafting Input + - 5-8: Armor + - 9-35: Main Inventory + - 36-44: Hotbar + - 45: Offhand + +*/ + pub struct PlayerInventory { // Main Inventory + Hotbar crafting: [Option; 4], @@ -137,24 +149,10 @@ impl PlayerInventory { false } - pub fn get_slot_with_item(&self, item_id: u16, max_stack: u8) -> Option { - for slot in 9..=44 { - match &self.items[slot - 9] { - Some(item) if item.item.id == item_id && item.item_count <= max_stack => { - return Some(slot) - } - _ => continue, - } - } - - None - } - /// Checks if we can merge an existing item into an Stack or if a any new Slot is empty pub fn collect_item_slot(&self, item_id: u16) -> Option { // Lets try to merge first - // TODO: Max stack size - if let Some(stack) = self.get_slot_with_item(item_id, 64) { + if let Some(stack) = self.get_nonfull_slot_with_item(item_id) { return Some(stack); } if let Some(empty) = self.get_empty_slot() { @@ -177,7 +175,63 @@ impl PlayerInventory { self.selected } + pub fn get_nonfull_slot_with_item(&self, item_id: u16) -> Option { + let max_stack = Item::from_id(item_id) + .unwrap_or(Item::AIR) + .components + .max_stack_size; + + // Check selected slot + if let Some(item) = &self.items[self.selected as usize + 36 - 9] { + if item.item.id == item_id && item.item_count < max_stack { + // + 9 - 9 is 0 + return Some(self.selected as usize + 36); + } + } + + // Check hotbar slots (27-35) first + if let Some(index) = self.items[27..36].iter().position(|slot| { + slot.is_some_and(|item| item.item.id == item_id && item.item_count < max_stack) + }) { + return Some(index + 27 + 9); + } + + // Then check main inventory slots (0-26) + if let Some(index) = self.items[0..27].iter().position(|slot| { + slot.is_some_and(|item| item.item.id == item_id && item.item_count < max_stack) + }) { + return Some(index + 9); + } + + None + } + + 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_empty_slot(&self) -> Option { + // Check hotbar slots (27-35) first + if let Some(index) = self.items[27..36].iter().position(|slot| slot.is_none()) { + return Some(index + 27 + 9); + } + + // Then check main inventory slots (0-26) + if let Some(index) = self.items[0..27].iter().position(|slot| slot.is_none()) { + return Some(index + 9); + } + + None + } + + pub fn get_empty_slot_no_order(&self) -> Option { self.items .iter() .position(|slot| slot.is_none()) diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 62abcf80d..4689e0319 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -62,7 +62,7 @@ pub async fn drop_loot(server: &Server, world: &Arc, block: &Block, pos: let entity = server.add_entity(pos, EntityType::ITEM, world); let item_entity = Arc::new(ItemEntity::new( entity, - &ItemStack::new(1, Item::from_id(block.item_id).unwrap()), + ItemStack::new(1, Item::from_id(block.item_id).unwrap()), )); world.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index 87a0bc6eb..b0e9b6da8 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -1,9 +1,12 @@ -use std::sync::{atomic::AtomicI8, Arc}; +use std::sync::{ + atomic::{AtomicI8, AtomicU8}, + Arc, +}; use async_trait::async_trait; use pumpkin_protocol::{ client::play::{CTakeItemEntity, MetaDataType, Metadata}, - codec::{slot::Slot, var_int::VarInt}, + codec::slot::Slot, }; use pumpkin_world::item::ItemStack; @@ -11,26 +14,24 @@ use super::{living::LivingEntity, player::Player, Entity, EntityBase}; pub struct ItemEntity { entity: Entity, - item: Slot, - id: u16, - count: u8, + item: ItemStack, + count: AtomicU8, pickup_delay: AtomicI8, } impl ItemEntity { - pub fn new(entity: Entity, stack: &ItemStack) -> Self { - let slot = Slot::from(stack); + pub fn new(entity: Entity, stack: ItemStack) -> Self { Self { entity, - id: stack.item.id, - count: stack.item_count, - item: slot, + item: stack, + count: AtomicU8::new(stack.item_count), pickup_delay: AtomicI8::new(10), // Vanilla } } pub async fn send_meta_packet(&self) { + let slot = Slot::from(&self.item); self.entity - .send_meta_data(Metadata::new(8, MetaDataType::ItemStack, &self.item)) + .send_meta_data(Metadata::new(8, MetaDataType::ItemStack, &slot)) .await; } } @@ -46,26 +47,54 @@ impl EntityBase for ItemEntity { async fn on_player_collision(&self, player: Arc) { if self.pickup_delay.load(std::sync::atomic::Ordering::Relaxed) == 0 { let mut inv = player.inventory.lock().await; + let mut item = self.item; // Check if we have space in inv - if let Some(slot) = inv.collect_item_slot(self.id) { - let mut item = self.item.clone(); + if let Some(slot) = inv.collect_item_slot(item.item.id) { + let max_stack = item.item.components.max_stack_size; if let Some(stack) = inv.get_slot(slot).unwrap() { - // If we merge into an existing stack lets increase its count - stack.item_count += self.count; - // Since we set the slot with the item, we need to also have the new item count, - // So existing count + self.count - item.item_count = VarInt(i32::from(stack.item_count)); + if stack.item_count + self.count.load(std::sync::atomic::Ordering::Relaxed) + > max_stack + { + // Fill the stack to max and store the overflow + let overflow = stack.item_count + + self.count.load(std::sync::atomic::Ordering::Relaxed) + - max_stack; + + stack.item_count = max_stack; + item.item_count = stack.item_count; + + self.count + .store(overflow, std::sync::atomic::Ordering::Relaxed); + } else { + // Add the item to the stack + stack.item_count += self.count.load(std::sync::atomic::Ordering::Relaxed); + item.item_count = stack.item_count; + + player + .client + .send_packet(&CTakeItemEntity::new( + self.entity.entity_id.into(), + player.entity_id().into(), + item.item_count.into(), + )) + .await; + self.entity.remove().await; + } + } else { + // Add the item as a new stack + item.item_count = self.count.load(std::sync::atomic::Ordering::Relaxed); + + player + .client + .send_packet(&CTakeItemEntity::new( + self.entity.entity_id.into(), + player.entity_id().into(), + item.item_count.into(), + )) + .await; + self.entity.remove().await; } player.update_single_slot(&mut inv, slot as i16, item).await; - player - .client - .send_packet(&CTakeItemEntity::new( - self.entity.entity_id.into(), - player.entity_id().into(), - 1.into(), - )) - .await; - self.entity.remove().await; } } } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index f05772530..975d28fab 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -993,7 +993,7 @@ impl Player { ); let item_entity = Arc::new(ItemEntity::new( entity, - &ItemStack::new(drop_amount, item.item), + ItemStack::new(drop_amount, item.item), )); self.world().await.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index c71f377e0..eb38f9b9a 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -397,14 +397,15 @@ impl Player { &self, inventory: &mut tokio::sync::MutexGuard<'_, PlayerInventory>, slot: i16, - slot_data: Slot, + stack: ItemStack, ) { inventory.state_id += 1; + let slot_data = Slot::from(&stack); 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 as usize, slot_data.to_item(), false) + .set_slot(slot as usize, Some(stack), false) .is_err() { log::error!("Pick item set slot error!"); @@ -428,13 +429,12 @@ impl Player { let mut inventory = self.inventory().lock().await; - // TODO: Max stack - let source_slot = inventory.get_slot_with_item(block.item_id, 64); + let source_slot = inventory.get_slot_with_item(block.item_id); let mut dest_slot = inventory.get_empty_hotbar_slot() as usize; let dest_slot_data = match inventory.get_slot(dest_slot + 36) { - Ok(Some(stack)) => Slot::from(&*stack), - _ => Slot::from(None), + Ok(Some(stack)) => *stack, + _ => ItemStack::new(0, Item::AIR), }; // Early return if no source slot and not in creative mode @@ -452,7 +452,7 @@ impl Player { // Update destination slot let source_slot_data = match inventory.get_slot(slot_index) { - Ok(Some(stack)) => Slot::from(&*stack), + Ok(Some(stack)) => *stack, _ => return, }; self.update_single_slot(&mut inventory, dest_slot as i16 + 36, source_slot_data) @@ -465,12 +465,11 @@ impl Player { 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, Item::from_id(block.item_id).unwrap()); - let slot_data = Slot::from(&item_stack); - self.update_single_slot(&mut inventory, dest_slot as i16 + 36, slot_data) + self.update_single_slot(&mut inventory, dest_slot as i16 + 36, item_stack) .await; // Check if there is any empty slot in the player inventory - if let Some(slot_index) = inventory.get_empty_slot() { + if let Some(slot_index) = inventory.get_empty_slot_no_order() { inventory.state_id += 1; self.update_single_slot(&mut inventory, slot_index as i16, dest_slot_data) .await;