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 <lilalexmed@proton.me>
This commit is contained in:
4lve
2025-02-20 16:51:48 +01:00
committed by GitHub
parent 3f1fd5a0f1
commit b428fdca1a
5 changed files with 136 additions and 54 deletions

View File

@@ -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<ItemStack>; 4],
@@ -137,24 +149,10 @@ impl PlayerInventory {
false
}
pub fn get_slot_with_item(&self, item_id: u16, max_stack: u8) -> Option<usize> {
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<usize> {
// 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<usize> {
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<usize> {
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<usize> {
// 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<usize> {
self.items
.iter()
.position(|slot| slot.is_none())

View File

@@ -62,7 +62,7 @@ pub async fn drop_loot(server: &Server, world: &Arc<World>, 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;

View File

@@ -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<Player>) {
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;
}
}
}

View File

@@ -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;

View File

@@ -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;