531 lines
18 KiB
Rust
531 lines
18 KiB
Rust
//! Player inventory implementation.
|
|
//!
|
|
//! This module implements the player's inventory, which consists of:
|
|
//! - 36 main inventory slots (3 rows of 9 + hotbar)
|
|
//! - Equipment slots (armor + off-hand)
|
|
//!
|
|
//! The first 9 slots of the main inventory are the hotbar (accessible with number keys).
|
|
//! Slots 0-35 are the main inventory, with slot 40 being the off-hand slot.
|
|
|
|
use crate::entity_equipment::EntityEquipment;
|
|
use crate::screen_handler::InventoryPlayer;
|
|
|
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
|
use pumpkin_data::item::Item;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_protocol::java::client::play::CSetPlayerInventory;
|
|
use pumpkin_util::Hand;
|
|
use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture};
|
|
use std::any::Any;
|
|
use std::collections::HashMap;
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicU8, Ordering};
|
|
use tokio::sync::{Mutex, RwLock};
|
|
use tracing::warn;
|
|
|
|
/// The player's inventory.
|
|
///
|
|
/// Contains 36 main inventory slots (hotbar + main storage) plus
|
|
/// equipment slots accessed through [`EntityEquipment`].
|
|
pub struct PlayerInventory {
|
|
/// The 36 main inventory slots (slots 0-35).
|
|
///
|
|
/// The first 9 slots (0-8) are the hotbar, the remaining 27 (9-35) are the main storage.
|
|
pub main_inventory: RwLock<[ItemStack; Self::MAIN_SIZE]>,
|
|
/// Mapping of slot indices to equipment slot types.
|
|
///
|
|
/// Used to identify which slots correspond to armor and off-hand equipment.
|
|
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
|
|
/// The currently selected hotbar slot index (0-8).
|
|
pub selected_slot: AtomicU8,
|
|
/// The entity equipment storage for armor and off-hand items.
|
|
///
|
|
/// This is separate from the main inventory and is rendered on the player model.
|
|
pub entity_equipment: Arc<Mutex<EntityEquipment>>,
|
|
}
|
|
|
|
impl PlayerInventory {
|
|
/// Size of the main inventory (36 slots: 27 storage + 9 hotbar).
|
|
pub const MAIN_SIZE: usize = 36;
|
|
/// Size of the hotbar (9 slots).
|
|
const HOTBAR_SIZE: usize = 9;
|
|
/// Slot index for the off-hand (40).
|
|
pub const OFF_HAND_SLOT: usize = 40;
|
|
|
|
/// Creates a new player inventory.
|
|
///
|
|
/// # Arguments
|
|
/// - `entity_equipment` - The entity equipment storage for armor/off-hand
|
|
/// - `equipment_slots` - Mapping of slot indices to equipment slots
|
|
// TODO: Add inventory load from nbt
|
|
pub fn new(
|
|
entity_equipment: Arc<Mutex<EntityEquipment>>,
|
|
equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
|
|
) -> Self {
|
|
Self {
|
|
main_inventory: RwLock::new(std::array::from_fn(|_| ItemStack::EMPTY.clone())),
|
|
equipment_slots,
|
|
selected_slot: AtomicU8::new(0),
|
|
entity_equipment,
|
|
}
|
|
}
|
|
|
|
/// Fast non-blocking count of an item across all main inventory slots.
|
|
pub fn count_item(&self, item: &'static Item) -> u32 {
|
|
let mut total = 0u32;
|
|
if let Ok(inv) = self.main_inventory.try_read() {
|
|
for stack in inv.iter() {
|
|
if stack.get_item().id == item.id {
|
|
total += u32::from(stack.item_count);
|
|
}
|
|
}
|
|
}
|
|
total
|
|
}
|
|
|
|
/// Fast non-blocking check if the main inventory contains a given item.
|
|
pub fn contains_item(&self, item: &'static Item) -> bool {
|
|
if let Ok(inv) = self.main_inventory.try_read() {
|
|
for stack in inv.iter() {
|
|
if !stack.is_empty() && stack.get_item().id == item.id {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Gets the item in the currently selected hotbar slot.
|
|
///
|
|
/// This is the item the player is currently holding in their main hand.
|
|
pub async fn held_item(&self) -> ItemStack {
|
|
let inv = self.main_inventory.read().await;
|
|
inv[self.get_selected_slot() as usize].clone()
|
|
}
|
|
|
|
/// Sets the item in the currently selected hotbar slot.
|
|
pub async fn set_held_item(&self, stack: ItemStack) {
|
|
let selected = self.get_selected_slot() as usize;
|
|
let mut inv = self.main_inventory.write().await;
|
|
inv[selected] = stack;
|
|
}
|
|
|
|
/// Sets the item in the specified hand.
|
|
pub async fn set_stack_in_hand(&self, hand: Hand, stack: ItemStack) {
|
|
match hand {
|
|
Hand::Right => self.set_held_item(stack).await,
|
|
Hand::Left => {
|
|
let Some(slot) = self.equipment_slots.get(&Self::OFF_HAND_SLOT) else {
|
|
return;
|
|
};
|
|
self.entity_equipment.lock().await.put(slot, stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets the item in the specified hand.
|
|
///
|
|
/// # Arguments
|
|
/// - `hand` - Which hand to get the item from
|
|
pub async fn get_stack_in_hand(&self, hand: Hand) -> ItemStack {
|
|
match hand {
|
|
Hand::Left => self.off_hand_item().await,
|
|
Hand::Right => self.held_item().await,
|
|
}
|
|
}
|
|
|
|
/// Gets the item in the off-hand.
|
|
///
|
|
/// Mojang name: `getOffHandStack`
|
|
pub async fn off_hand_item(&self) -> ItemStack {
|
|
let Some(slot) = self.equipment_slots.get(&Self::OFF_HAND_SLOT) else {
|
|
return ItemStack::EMPTY.clone();
|
|
};
|
|
self.entity_equipment.lock().await.get(slot)
|
|
}
|
|
|
|
/// Swaps the items between main hand and off-hand.
|
|
///
|
|
/// # Returns
|
|
/// The new main hand item and new off-hand item.
|
|
pub async fn swap_item(&self) -> (ItemStack, ItemStack) {
|
|
let Some(slot) = self.equipment_slots.get(&Self::OFF_HAND_SLOT) else {
|
|
return (ItemStack::EMPTY.clone(), ItemStack::EMPTY.clone());
|
|
};
|
|
let mut equipment = self.entity_equipment.lock().await;
|
|
let selected = self.get_selected_slot() as usize;
|
|
let mut main_inv = self.main_inventory.write().await;
|
|
let main_hand_item = main_inv[selected].clone();
|
|
let new_main = equipment.put(slot, main_hand_item.clone());
|
|
main_inv[selected] = new_main.clone();
|
|
(new_main, main_hand_item)
|
|
}
|
|
|
|
/// Checks if a slot index is a valid hotbar slot.
|
|
#[must_use]
|
|
pub const fn is_valid_hotbar_index(slot: usize) -> bool {
|
|
slot < Self::HOTBAR_SIZE
|
|
}
|
|
|
|
/// Adds a stack to any available slot, prioritizing stacking with existing items.
|
|
async fn add_stack(&self, stack: ItemStack) -> usize {
|
|
let mut slot_index = self.get_occupied_slot_with_room_for_stack(&stack).await;
|
|
|
|
if slot_index == -1 {
|
|
slot_index = self.get_empty_slot().await;
|
|
}
|
|
|
|
if slot_index == -1 {
|
|
stack.item_count as usize
|
|
} else {
|
|
self.add_stack_to_slot(slot_index as usize, stack).await
|
|
}
|
|
}
|
|
|
|
/// Adds a stack to a specific slot.
|
|
///
|
|
/// Returns the number of items that couldn't fit.
|
|
async fn add_stack_to_slot(&self, slot: usize, stack: ItemStack) -> usize {
|
|
if slot >= Self::MAIN_SIZE {
|
|
if let Some(slot_type) = self.equipment_slots.get(&slot) {
|
|
let mut equipment = self.entity_equipment.lock().await;
|
|
let current = equipment.get(slot_type);
|
|
if current.is_empty() {
|
|
equipment.put(slot_type, stack);
|
|
return 0;
|
|
}
|
|
}
|
|
return stack.item_count as usize;
|
|
}
|
|
|
|
let mut inv = self.main_inventory.write().await;
|
|
let mut stack_count = stack.item_count;
|
|
let self_stack = &mut inv[slot];
|
|
|
|
if self_stack.is_empty() {
|
|
*self_stack = stack.copy_with_count(0);
|
|
}
|
|
|
|
let count_left = self_stack.get_max_stack_size() - self_stack.item_count;
|
|
let count_min = stack_count.min(count_left);
|
|
|
|
if count_min != 0 {
|
|
stack_count -= count_min;
|
|
self_stack.increment(count_min);
|
|
}
|
|
stack_count as usize
|
|
}
|
|
|
|
/// Finds an empty slot in the inventory.
|
|
///
|
|
/// # Returns
|
|
/// The slot index or -1 if inventory is full.
|
|
async fn get_empty_slot(&self) -> i16 {
|
|
let inv = self.main_inventory.read().await;
|
|
for (i, stack) in inv.iter().enumerate() {
|
|
if stack.is_empty() {
|
|
return i as i16;
|
|
}
|
|
}
|
|
-1
|
|
}
|
|
|
|
/// Checks if a stack can be added to an existing stack.
|
|
fn can_stack_add_more(existing_stack: &ItemStack, stack: &ItemStack) -> bool {
|
|
!existing_stack.is_empty()
|
|
&& existing_stack.are_items_and_components_equal(stack)
|
|
&& existing_stack.is_stackable()
|
|
&& existing_stack.item_count < existing_stack.get_max_stack_size()
|
|
}
|
|
|
|
/// Finds a slot with the same item type that has room for more items.
|
|
///
|
|
/// Checks selected slot, off-hand, then other slots.
|
|
async fn get_occupied_slot_with_room_for_stack(&self, stack: &ItemStack) -> i16 {
|
|
let selected = self.get_selected_slot() as usize;
|
|
let inv = self.main_inventory.read().await;
|
|
if Self::can_stack_add_more(&inv[selected], stack) {
|
|
return selected as i16;
|
|
}
|
|
|
|
let off_hand = self.off_hand_item().await;
|
|
if Self::can_stack_add_more(&off_hand, stack) {
|
|
return Self::OFF_HAND_SLOT as i16;
|
|
}
|
|
|
|
for (i, item) in inv.iter().enumerate() {
|
|
if Self::can_stack_add_more(item, stack) {
|
|
return i as i16;
|
|
}
|
|
}
|
|
|
|
-1
|
|
}
|
|
|
|
/// Inserts a stack into any available slot.
|
|
///
|
|
/// # Arguments
|
|
/// - `stack` - The stack to insert (modified in place)
|
|
///
|
|
/// # Returns
|
|
/// `true` if any items were inserted, `false` otherwise.
|
|
pub async fn insert_stack_anywhere(&self, stack: &mut ItemStack) -> bool {
|
|
self.insert_stack(-1, stack).await
|
|
}
|
|
|
|
/// Inserts a stack into a specific slot or any slot.
|
|
///
|
|
/// # Arguments
|
|
/// - `slot` - The slot index, or -1 for any slot
|
|
/// - `stack` - The stack to insert (modified in place)
|
|
///
|
|
/// # Returns
|
|
/// `true` if any items were inserted, `false` otherwise.
|
|
pub async fn insert_stack(&self, slot: i16, stack: &mut ItemStack) -> bool {
|
|
if stack.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
let mut i;
|
|
|
|
loop {
|
|
i = stack.item_count;
|
|
if slot == -1 {
|
|
stack.set_count(self.add_stack(stack.clone()).await as u8);
|
|
} else {
|
|
stack.set_count(self.add_stack_to_slot(slot as usize, stack.clone()).await as u8);
|
|
}
|
|
|
|
if stack.is_empty() || stack.item_count >= i {
|
|
break;
|
|
}
|
|
}
|
|
|
|
stack.item_count < i
|
|
}
|
|
|
|
/// Finds the first slot containing a matching stack.
|
|
///
|
|
/// # Returns
|
|
/// The slot index or -1 if not found.
|
|
pub async fn get_slot_with_stack(&self, stack: &ItemStack) -> i16 {
|
|
let inv = self.main_inventory.read().await;
|
|
for (i, item) in inv.iter().enumerate() {
|
|
if !item.is_empty() && item.are_items_and_components_equal(stack) {
|
|
return i as i16;
|
|
}
|
|
}
|
|
-1
|
|
}
|
|
|
|
/// Finds an empty hotbar slot to swap an item to.
|
|
///
|
|
/// First looks for empty slots, then slots without enchantments.
|
|
async fn get_swappable_hotbar_slot(&self) -> usize {
|
|
let inv = self.main_inventory.read().await;
|
|
let selected_slot = self.get_selected_slot() as usize;
|
|
for i in 0..Self::HOTBAR_SIZE {
|
|
let check_index = (i + selected_slot) % 9;
|
|
if inv[check_index].is_empty() {
|
|
return check_index;
|
|
}
|
|
}
|
|
|
|
selected_slot
|
|
}
|
|
|
|
/// Swaps an item stack with an item on the hotbar.
|
|
///
|
|
/// Finds an empty hotbar slot and places the stack there.
|
|
pub async fn swap_stack_with_hotbar(&self, stack: ItemStack) {
|
|
let swappable = self.get_swappable_hotbar_slot().await;
|
|
self.set_selected_slot(swappable as u8);
|
|
let selected = self.get_selected_slot() as usize;
|
|
let mut inv = self.main_inventory.write().await;
|
|
|
|
if let Some(empty_slot) = inv.iter().position(ItemStack::is_empty)
|
|
&& !inv[selected].is_empty()
|
|
{
|
|
inv[empty_slot] = inv[selected].clone();
|
|
}
|
|
|
|
inv[selected] = stack;
|
|
}
|
|
|
|
/// Swaps the items at two slot indices.
|
|
pub async fn swap_slot_with_hotbar(&self, slot: usize) {
|
|
let swappable = self.get_swappable_hotbar_slot().await;
|
|
self.set_selected_slot(swappable as u8);
|
|
let selected = self.get_selected_slot() as usize;
|
|
let mut inv = self.main_inventory.write().await;
|
|
inv.swap(selected, slot);
|
|
}
|
|
|
|
/// Gives a stack to the player or drops it if inventory is full.
|
|
pub async fn offer_or_drop_stack(&self, stack: ItemStack, player: &dyn InventoryPlayer) {
|
|
self.offer(stack, true, player).await;
|
|
}
|
|
|
|
/// Gives a stack to the player, optionally notifying the client.
|
|
///
|
|
/// # Arguments
|
|
/// - `stack` - The stack to give
|
|
/// - `notify_client` - Whether to send inventory update packets
|
|
/// - `player` - The player to give the stack to
|
|
pub async fn offer(&self, stack: ItemStack, notify_client: bool, player: &dyn InventoryPlayer) {
|
|
let mut stack = stack;
|
|
while !stack.is_empty() {
|
|
let mut room_for_stack = self.get_occupied_slot_with_room_for_stack(&stack).await;
|
|
if room_for_stack == -1 {
|
|
room_for_stack = self.get_empty_slot().await;
|
|
}
|
|
|
|
if room_for_stack == -1 {
|
|
player.drop_item(stack, false).await;
|
|
break;
|
|
}
|
|
|
|
let items_fit = stack.get_max_stack_size()
|
|
- self.get_stack(room_for_stack as usize).await.item_count;
|
|
if self
|
|
.insert_stack(room_for_stack, &mut stack.split(items_fit))
|
|
.await
|
|
&& notify_client
|
|
{
|
|
player
|
|
.enqueue_slot_set_packet(&CSetPlayerInventory::new(
|
|
i32::from(room_for_stack).into(),
|
|
&self.get_stack(room_for_stack as usize).await.into(),
|
|
))
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Clearable for PlayerInventory {
|
|
fn clear(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
|
Box::pin(async move {
|
|
let mut inv = self.main_inventory.write().await;
|
|
inv.fill_with(|| ItemStack::EMPTY.clone());
|
|
self.entity_equipment.lock().await.clear();
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Inventory for PlayerInventory {
|
|
fn size(&self) -> usize {
|
|
Self::MAIN_SIZE + self.equipment_slots.len()
|
|
}
|
|
|
|
fn is_empty(&self) -> InventoryFuture<'_, bool> {
|
|
Box::pin(async move {
|
|
let inv = self.main_inventory.read().await;
|
|
if inv.iter().any(|s| !s.is_empty()) {
|
|
return false;
|
|
}
|
|
|
|
for slot in self.equipment_slots.values() {
|
|
let eq_item = self.entity_equipment.lock().await.get(slot);
|
|
if !eq_item.is_empty() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
})
|
|
}
|
|
|
|
fn get_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
|
Box::pin(async move {
|
|
if slot < Self::MAIN_SIZE {
|
|
let inv = self.main_inventory.read().await;
|
|
inv[slot].clone()
|
|
} else if let Some(slot) = self.equipment_slots.get(&slot) {
|
|
self.entity_equipment.lock().await.get(slot)
|
|
} else {
|
|
ItemStack::EMPTY.clone()
|
|
}
|
|
})
|
|
}
|
|
|
|
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
|
Box::pin(async move {
|
|
if slot < Self::MAIN_SIZE {
|
|
let mut inv = self.main_inventory.write().await;
|
|
std::mem::replace(&mut inv[slot], ItemStack::EMPTY.clone())
|
|
} else if let Some(slot) = self.equipment_slots.get(&slot) {
|
|
self.entity_equipment
|
|
.lock()
|
|
.await
|
|
.put(slot, ItemStack::EMPTY.clone())
|
|
} else {
|
|
ItemStack::EMPTY.clone()
|
|
}
|
|
})
|
|
}
|
|
|
|
fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> {
|
|
Box::pin(async move {
|
|
if slot < Self::MAIN_SIZE {
|
|
let mut inv = self.main_inventory.write().await;
|
|
if !inv[slot].is_empty() && amount > 0 {
|
|
inv[slot].split(amount)
|
|
} else {
|
|
ItemStack::EMPTY.clone()
|
|
}
|
|
} else if let Some(slot) = self.equipment_slots.get(&slot) {
|
|
let mut equipment = self.entity_equipment.lock().await;
|
|
let mut stack = equipment.get(slot);
|
|
|
|
if !stack.is_empty() && amount > 0 {
|
|
let split = stack.split(amount);
|
|
equipment.put(slot, stack);
|
|
split
|
|
} else {
|
|
ItemStack::EMPTY.clone()
|
|
}
|
|
} else {
|
|
ItemStack::EMPTY.clone()
|
|
}
|
|
})
|
|
}
|
|
|
|
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
if slot < Self::MAIN_SIZE {
|
|
let mut inv = self.main_inventory.write().await;
|
|
inv[slot] = stack;
|
|
} else if let Some(slot) = self.equipment_slots.get(&slot) {
|
|
self.entity_equipment.lock().await.put(slot, stack);
|
|
} else {
|
|
warn!("Failed to get Equipment Slot at {slot}");
|
|
}
|
|
})
|
|
}
|
|
|
|
fn mark_dirty(&self) {}
|
|
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl PlayerInventory {
|
|
/// Sets the selected hotbar slot.
|
|
///
|
|
/// # Panics
|
|
/// Panics if the slot index is not a valid hotbar index.
|
|
pub fn set_selected_slot(&self, slot: u8) {
|
|
if Self::is_valid_hotbar_index(slot as usize) {
|
|
self.selected_slot.store(slot, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
/// Gets the currently selected hotbar slot index.
|
|
pub fn get_selected_slot(&self) -> u8 {
|
|
self.selected_slot.load(Ordering::Relaxed)
|
|
}
|
|
}
|