mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user