From 9e36cdf683645da49cdbf090b80be0cea9493352 Mon Sep 17 00:00:00 2001 From: 4lve <72332750+4lve@users.noreply.github.com> Date: Fri, 9 May 2025 16:28:15 +0200 Subject: [PATCH] Full inventory refactor + barrels (#691) * replace slot with ItemStackSeralizer * Dismantle current inv logic * Disassemble even more, items need fixing markd with TODO: Inv * Player inventory methods implmented * make some fields private * remove invalid comment * Partial screen handler and slot * Add armorslot * prepare for crafting * implement some more crafting traits * Fix ItemStack * Fix pickup and remove faulty crafting (getting reworked in inv revamp) * fix * fix error * add readme * move readme file * asd * remove readme * add uppsercase readme * Some more work * merge * Move from generics to dynamics * fix * move to same impl * Some changes * add player open logic * Basic impl of sync and listeners * Why did mojang complicate it so much * bruh * Simplify things with DefaultScreenHandlerBehaviour * Implement partial slot clicks * add * itemhashes are broken * Start implementing packets * fix deadlock * new vanilla update * Use static item refs and use Arc Mutex on invs * working normal click * fix deadlock * implement sync handler * small patch * Implement quick_move * Implement Swap click type * pickup, update bug * start fixing updates * fix pickup desync * Make Inventories non mutexes * Implement some finishing stuff * close screen on player leave * fix pick item * Move to item hashes * remove some logs * begin implementing barrels * Begin implementing barrel * implement barrels * fix clippy + typos * Fix merge conflicts * Fix lint error * don't use unwrap when getting equipment slot * Fix typo and single threaded * Accidentally removed tokio completley * Fix crash when opening barrel holding item * Add support for marking block entities dirty * Player Equipment changes * Make ScreenHandlerFactory own inventory instead of passing in optional * remove log * replace todo's * fix clippy --------- Co-authored-by: Alexander Medvedev --- pumpkin-data/build/item.rs | 8 +- pumpkin-inventory/Cargo.toml | 2 + pumpkin-inventory/src/container_click.rs | 6 +- .../src/crafting/crafting_inventory.rs | 87 ++ .../src/crafting/crafting_screen_handler.rs | 71 ++ pumpkin-inventory/src/crafting/mod.rs | 3 + pumpkin-inventory/src/crafting/recipes.rs | 18 + pumpkin-inventory/src/drag_handler.rs | 8 +- pumpkin-inventory/src/entity_equipment.rs | 58 ++ pumpkin-inventory/src/equipment_slot.rs | 140 ++++ .../src/generic_container_screen_handler.rs | 127 +++ pumpkin-inventory/src/lib.rs | 304 +------ pumpkin-inventory/src/open_container.rs | 233 ------ pumpkin-inventory/src/player.rs | 381 --------- pumpkin-inventory/src/player/mod.rs | 2 + .../src/player/player_inventory.rs | 409 ++++++++++ .../src/player/player_screen_handler.rs | 170 ++++ pumpkin-inventory/src/screen_handler.rs | 744 ++++++++++++++++++ pumpkin-inventory/src/slot.rs | 274 +++++++ pumpkin-inventory/src/sync_handler.rs | 156 ++++ .../src/client/play/close_container.rs | 4 +- pumpkin-protocol/src/client/play/mod.rs | 4 + .../src/client/play/open_screen.rs | 4 +- .../src/client/play/set_cursor_slot.rs | 17 + .../src/client/play/set_equipment.rs | 20 +- .../src/client/play/set_held_item.rs | 4 +- .../src/client/play/set_player_inventory.rs | 19 + .../src/codec/item_stack_seralizer.rs | 130 ++- .../src/server/play/click_container.rs | 34 +- pumpkin-world/src/block/entities/barrel.rs | 129 +++ pumpkin-world/src/block/entities/bed.rs | 8 +- pumpkin-world/src/block/entities/chest.rs | 8 +- .../src/block/entities/comparator.rs | 8 +- pumpkin-world/src/block/entities/mod.rs | 25 +- pumpkin-world/src/block/entities/sign.rs | 12 +- pumpkin-world/src/chunk/format/anvil.rs | 21 +- pumpkin-world/src/chunk/format/linear.rs | 1 + .../src/chunk/io/chunk_file_manager.rs | 2 +- pumpkin-world/src/inventory/inventory.rs | 162 ++++ pumpkin-world/src/inventory/mod.rs | 20 + pumpkin-world/src/item/mod.rs | 36 +- pumpkin-world/src/lib.rs | 1 + pumpkin/src/block/blocks/barrel.rs | 110 +++ pumpkin/src/block/blocks/chest.rs | 117 +-- pumpkin/src/block/blocks/crafting_table.rs | 92 +-- pumpkin/src/block/blocks/furnace.rs | 70 +- pumpkin/src/block/blocks/mod.rs | 116 +-- pumpkin/src/block/loot.rs | 12 +- pumpkin/src/block/mod.rs | 5 +- pumpkin/src/block/pumpkin_block.rs | 11 - pumpkin/src/block/registry.rs | 17 - pumpkin/src/command/args/resource/item.rs | 2 +- pumpkin/src/command/commands/clear.rs | 18 +- pumpkin/src/command/commands/give.rs | 7 +- pumpkin/src/data/player_server_data.rs | 9 + pumpkin/src/entity/combat.rs | 8 +- pumpkin/src/entity/item.rs | 113 +-- pumpkin/src/entity/living.rs | 12 +- pumpkin/src/entity/mod.rs | 4 +- pumpkin/src/entity/player.rs | 387 +++++++-- pumpkin/src/item/items/bucket.rs | 59 +- pumpkin/src/item/items/hoe.rs | 6 +- pumpkin/src/net/container.rs | 724 ----------------- pumpkin/src/net/mod.rs | 1 - pumpkin/src/net/packet/play.rs | 269 ++----- pumpkin/src/server/mod.rs | 71 +- pumpkin/src/world/mod.rs | 4 +- typos.toml | 8 +- 68 files changed, 3482 insertions(+), 2640 deletions(-) create mode 100644 pumpkin-inventory/src/crafting/crafting_inventory.rs create mode 100644 pumpkin-inventory/src/crafting/crafting_screen_handler.rs create mode 100644 pumpkin-inventory/src/crafting/mod.rs create mode 100644 pumpkin-inventory/src/crafting/recipes.rs create mode 100644 pumpkin-inventory/src/entity_equipment.rs create mode 100644 pumpkin-inventory/src/equipment_slot.rs create mode 100644 pumpkin-inventory/src/generic_container_screen_handler.rs delete mode 100644 pumpkin-inventory/src/open_container.rs delete mode 100644 pumpkin-inventory/src/player.rs create mode 100644 pumpkin-inventory/src/player/mod.rs create mode 100644 pumpkin-inventory/src/player/player_inventory.rs create mode 100644 pumpkin-inventory/src/player/player_screen_handler.rs create mode 100644 pumpkin-inventory/src/screen_handler.rs create mode 100644 pumpkin-inventory/src/slot.rs create mode 100644 pumpkin-inventory/src/sync_handler.rs create mode 100644 pumpkin-protocol/src/client/play/set_cursor_slot.rs create mode 100644 pumpkin-protocol/src/client/play/set_player_inventory.rs create mode 100644 pumpkin-world/src/block/entities/barrel.rs create mode 100644 pumpkin-world/src/inventory/inventory.rs create mode 100644 pumpkin-world/src/inventory/mod.rs create mode 100644 pumpkin/src/block/blocks/barrel.rs delete mode 100644 pumpkin/src/net/container.rs diff --git a/pumpkin-data/build/item.rs b/pumpkin-data/build/item.rs index a5e9ee20c..85996e51d 100644 --- a/pumpkin-data/build/item.rs +++ b/pumpkin-data/build/item.rs @@ -251,11 +251,11 @@ pub(crate) fn build() -> TokenStream { }); type_from_raw_id_arms.extend(quote! { - #id_lit => Some(Self::#const_ident), + #id_lit => Some(&Self::#const_ident), }); type_from_name.extend(quote! { - #name => Some(Self::#const_ident), + #name => Some(&Self::#const_ident), }); } @@ -334,7 +334,7 @@ pub(crate) fn build() -> TokenStream { } #[doc = "Try to parse an item from a resource location string."] - pub fn from_registry_key(name: &str) -> Option { + pub fn from_registry_key(name: &str) -> Option<&'static Self> { match name { #type_from_name _ => None @@ -342,7 +342,7 @@ pub(crate) fn build() -> TokenStream { } #[doc = "Try to parse an item from a raw id."] - pub const fn from_id(id: u16) -> Option { + pub const fn from_id(id: u16) -> Option<&'static Self> { match id { #type_from_raw_id_arms _ => None diff --git a/pumpkin-inventory/Cargo.toml b/pumpkin-inventory/Cargo.toml index a83fd927e..42a8b18a9 100644 --- a/pumpkin-inventory/Cargo.toml +++ b/pumpkin-inventory/Cargo.toml @@ -15,4 +15,6 @@ log.workspace = true rayon.workspace = true tokio.workspace = true thiserror.workspace = true +async-trait.workspace = true +crossbeam-utils = "0.8.21" diff --git a/pumpkin-inventory/src/container_click.rs b/pumpkin-inventory/src/container_click.rs index 2bb7568f4..5ab125b6b 100644 --- a/pumpkin-inventory/src/container_click.rs +++ b/pumpkin-inventory/src/container_click.rs @@ -1,4 +1,4 @@ -use crate::{InventoryError, player::SLOT_INDEX_OUTSIDE}; +use crate::InventoryError; use pumpkin_protocol::server::play::SlotActionType; use pumpkin_world::item::ItemStack; @@ -15,6 +15,8 @@ const KEY_CLICK_OFFHAND: i8 = 40; const KEY_CLICK_HOTBAR_START: i8 = 0; const KEY_CLICK_HOTBAR_END: i8 = 9; +const SLOT_INDEX_OUTSIDE: i16 = -999; + impl Click { pub fn new(mode: SlotActionType, button: i8, slot: i16) -> Result { match mode { @@ -122,7 +124,7 @@ pub enum ClickType { MouseDrag { drag_state: MouseDragState }, DoubleClick, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum MouseClick { Left, Right, diff --git a/pumpkin-inventory/src/crafting/crafting_inventory.rs b/pumpkin-inventory/src/crafting/crafting_inventory.rs new file mode 100644 index 000000000..45a6baf4a --- /dev/null +++ b/pumpkin-inventory/src/crafting/crafting_inventory.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use pumpkin_world::{inventory::split_stack, item::ItemStack}; +use tokio::sync::Mutex; + +use pumpkin_world::inventory::{Clearable, Inventory}; + +use super::recipes::RecipeInputInventory; + +#[derive(Debug, Clone)] +pub struct CraftingInventory { + pub width: u8, + pub height: u8, + pub items: Vec>>, +} + +impl CraftingInventory { + pub fn new(width: u8, height: u8) -> Self { + Self { + width, + height, + items: { + // Creates a Vec with different Mutexes for each slot + let mut v = Vec::with_capacity(width as usize * height as usize); + (0..width as usize * height as usize) + .for_each(|_| v.push(Arc::new(Mutex::new(ItemStack::EMPTY)))); + v + }, + } + } +} + +#[async_trait] +impl Inventory for CraftingInventory { + fn size(&self) -> usize { + self.items.len() + } + + async fn is_empty(&self) -> bool { + for slot in self.items.iter() { + if !slot.lock().await.is_empty() { + return false; + } + } + + true + } + + async fn get_stack(&self, slot: usize) -> Arc> { + self.items[slot].clone() + } + + async fn remove_stack(&self, slot: usize) -> ItemStack { + let mut removed = ItemStack::EMPTY; + let mut guard = self.items[slot].lock().await; + std::mem::swap(&mut removed, &mut *guard); + removed + } + + async fn remove_stack_specific(&self, slot: usize, amount: u8) -> ItemStack { + split_stack(&self.items, slot, amount).await + } + + async fn set_stack(&self, slot: usize, stack: ItemStack) { + *self.items[slot].lock().await = stack; + } +} + +impl RecipeInputInventory for CraftingInventory { + fn get_width(&self) -> usize { + self.width as usize + } + + fn get_height(&self) -> usize { + self.height as usize + } +} + +#[async_trait] +impl Clearable for CraftingInventory { + async fn clear(&self) { + for slot in self.items.iter() { + *slot.lock().await = ItemStack::EMPTY; + } + } +} diff --git a/pumpkin-inventory/src/crafting/crafting_screen_handler.rs b/pumpkin-inventory/src/crafting/crafting_screen_handler.rs new file mode 100644 index 000000000..29740589f --- /dev/null +++ b/pumpkin-inventory/src/crafting/crafting_screen_handler.rs @@ -0,0 +1,71 @@ +use std::sync::{Arc, atomic::AtomicU8}; + +use async_trait::async_trait; +use pumpkin_world::inventory::Inventory; + +use crate::{ + screen_handler::ScreenHandler, + slot::{NormalSlot, Slot}, +}; + +use super::recipes::{RecipeFinderScreenHandler, RecipeInputInventory}; + +// TODO: Implement ResultSlot +// CraftingResultSlot.java +#[derive(Debug)] +pub struct ResultSlot { + pub inventory: Arc, + pub index: usize, + pub id: AtomicU8, +} + +impl ResultSlot { + pub fn new(inventory: Arc, index: usize) -> Self { + Self { + inventory, + index, + id: AtomicU8::new(0), + } + } +} +#[async_trait] +impl Slot for ResultSlot { + fn get_inventory(&self) -> &Arc { + &self.inventory + } + + fn get_index(&self) -> usize { + self.index + } + + fn set_id(&self, id: usize) { + self.id + .store(id as u8, std::sync::atomic::Ordering::Relaxed); + } + + async fn mark_dirty(&self) { + self.inventory.mark_dirty(); + } +} + +// AbstractCraftingScreenHandler.java +#[async_trait] +pub trait CraftingScreenHandler: + RecipeFinderScreenHandler + ScreenHandler +{ + async fn add_result_slot(&mut self, crafing_inventory: &Arc) { + let result_slot = ResultSlot::new(crafing_inventory.clone(), 0); + self.add_slot(Arc::new(result_slot)); + } + + async fn add_input_slots(&mut self, crafing_inventory: &Arc) { + let width = crafing_inventory.get_width(); + let height = crafing_inventory.get_height(); + for i in 0..width { + for j in 0..height { + let input_slot = NormalSlot::new(crafing_inventory.clone(), j + i * width); + self.add_slot(Arc::new(input_slot)); + } + } + } +} diff --git a/pumpkin-inventory/src/crafting/mod.rs b/pumpkin-inventory/src/crafting/mod.rs new file mode 100644 index 000000000..e44adf6cc --- /dev/null +++ b/pumpkin-inventory/src/crafting/mod.rs @@ -0,0 +1,3 @@ +pub mod crafting_inventory; +pub mod crafting_screen_handler; +pub mod recipes; diff --git a/pumpkin-inventory/src/crafting/recipes.rs b/pumpkin-inventory/src/crafting/recipes.rs new file mode 100644 index 000000000..218863eb5 --- /dev/null +++ b/pumpkin-inventory/src/crafting/recipes.rs @@ -0,0 +1,18 @@ +use pumpkin_world::inventory::Inventory; + +// RecipeMatcher.java +pub struct RecipeMatcher {} + +// RecipeFinder.java +pub struct RecipeFinder {} + +// AbstractRecipeScreenHandle.java +pub trait RecipeFinderScreenHandler {} + +pub trait RecipeInputInventory: Inventory { + fn get_width(&self) -> usize; + fn get_height(&self) -> usize; + //fn get_held_stacks(), Get a lock on the inventory instead + // createRecipeInput + // createPositionedRecipeInput +} diff --git a/pumpkin-inventory/src/drag_handler.rs b/pumpkin-inventory/src/drag_handler.rs index 3fccf61e7..fc93caca4 100644 --- a/pumpkin-inventory/src/drag_handler.rs +++ b/pumpkin-inventory/src/drag_handler.rs @@ -1,9 +1,4 @@ -use crate::container_click::MouseDragType; -use crate::{Container, InventoryError}; -use pumpkin_world::item::ItemStack; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{Mutex, RwLock}; +/* #[derive(Debug, Default)] pub struct DragHandler(RwLock>>>); @@ -173,3 +168,4 @@ impl Drag { .collect() } } + */ diff --git a/pumpkin-inventory/src/entity_equipment.rs b/pumpkin-inventory/src/entity_equipment.rs new file mode 100644 index 000000000..34050070a --- /dev/null +++ b/pumpkin-inventory/src/entity_equipment.rs @@ -0,0 +1,58 @@ +use std::{collections::HashMap, sync::Arc}; + +use pumpkin_world::item::ItemStack; +use tokio::sync::Mutex; + +use crate::equipment_slot::EquipmentSlot; + +// EntityEquipment.java +#[derive(Debug, Clone)] +pub struct EntityEquipment { + pub equipment: HashMap>>, +} + +impl Default for EntityEquipment { + fn default() -> Self { + Self::new() + } +} + +impl EntityEquipment { + pub fn new() -> Self { + Self { + equipment: HashMap::new(), + } + } + + pub async fn put(&mut self, slot: &EquipmentSlot, stack: ItemStack) -> ItemStack { + *self + .equipment + .insert(slot.clone(), Arc::new(Mutex::new(stack))) + .unwrap_or(Arc::new(Mutex::new(ItemStack::EMPTY))) + .lock() + .await + } + + pub fn get(&self, slot: &EquipmentSlot) -> Arc> { + self.equipment + .get(slot) + .cloned() + .unwrap_or(Arc::new(Mutex::new(ItemStack::EMPTY))) + } + + pub async fn is_empty(&self) -> bool { + for stack in self.equipment.values() { + if !stack.lock().await.is_empty() { + return false; + } + } + + true + } + + pub fn clear(&mut self) { + self.equipment.clear(); + } + + // TODO: tick +} diff --git a/pumpkin-inventory/src/equipment_slot.rs b/pumpkin-inventory/src/equipment_slot.rs new file mode 100644 index 000000000..6c8e0d52d --- /dev/null +++ b/pumpkin-inventory/src/equipment_slot.rs @@ -0,0 +1,140 @@ +use std::borrow::Cow; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum EquipmentType { + Hand, + HumanoidArmor, + AnimalArmor, + Saddle, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct EquipmentSlotData { + pub slot_type: EquipmentType, + pub entity_id: i32, + pub max_count: i32, + pub index: i32, + pub name: Cow<'static, str>, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[repr(i8)] +pub enum EquipmentSlot { + MainHand(EquipmentSlotData), + OffHand(EquipmentSlotData), + Feet(EquipmentSlotData), + Legs(EquipmentSlotData), + Chest(EquipmentSlotData), + Head(EquipmentSlotData), + Body(EquipmentSlotData), + Saddle(EquipmentSlotData), +} + +impl EquipmentSlot { + pub const MAIN_HAND: Self = Self::MainHand(EquipmentSlotData { + slot_type: EquipmentType::Hand, + entity_id: 0, + index: 0, + max_count: 0, + name: Cow::Borrowed("mainhand"), + }); + pub const OFF_HAND: Self = Self::OffHand(EquipmentSlotData { + slot_type: EquipmentType::Hand, + entity_id: 1, + index: 5, + max_count: 0, + name: Cow::Borrowed("offhand"), + }); + pub const FEET: Self = Self::Feet(EquipmentSlotData { + slot_type: EquipmentType::HumanoidArmor, + entity_id: 0, + index: 1, + max_count: 1, + name: Cow::Borrowed("feet"), + }); + pub const LEGS: Self = Self::Legs(EquipmentSlotData { + slot_type: EquipmentType::HumanoidArmor, + entity_id: 1, + index: 2, + max_count: 1, + name: Cow::Borrowed("legs"), + }); + pub const CHEST: Self = Self::Chest(EquipmentSlotData { + slot_type: EquipmentType::HumanoidArmor, + entity_id: 2, + index: 3, + max_count: 1, + name: Cow::Borrowed("chest"), + }); + pub const HEAD: Self = Self::Head(EquipmentSlotData { + slot_type: EquipmentType::HumanoidArmor, + entity_id: 3, + index: 4, + max_count: 1, + name: Cow::Borrowed("head"), + }); + pub const BODY: Self = Self::Body(EquipmentSlotData { + slot_type: EquipmentType::AnimalArmor, + entity_id: 0, + index: 6, + max_count: 1, + name: Cow::Borrowed("body"), + }); + pub const SADDLE: Self = Self::Saddle(EquipmentSlotData { + slot_type: EquipmentType::Saddle, + entity_id: 0, + index: 7, + max_count: 1, + name: Cow::Borrowed("saddle"), + }); + + pub fn get_entity_slot_id(&self) -> i32 { + match self { + Self::MainHand(data) => data.entity_id, + Self::OffHand(data) => data.entity_id, + Self::Feet(data) => data.entity_id, + Self::Legs(data) => data.entity_id, + Self::Chest(data) => data.entity_id, + Self::Head(data) => data.entity_id, + Self::Body(data) => data.entity_id, + Self::Saddle(data) => data.entity_id, + } + } + + pub fn get_offset_entity_slot_id(&self, offset: i32) -> i32 { + self.get_entity_slot_id() + offset + } + + pub fn slot_type(&self) -> EquipmentType { + match self { + Self::MainHand(data) => data.slot_type, + Self::OffHand(data) => data.slot_type, + Self::Feet(data) => data.slot_type, + Self::Legs(data) => data.slot_type, + Self::Chest(data) => data.slot_type, + Self::Head(data) => data.slot_type, + Self::Body(data) => data.slot_type, + Self::Saddle(data) => data.slot_type, + } + } + + pub fn is_armor_slot(&self) -> bool { + matches!( + self.slot_type(), + EquipmentType::HumanoidArmor | EquipmentType::AnimalArmor + ) + } + + pub fn discriminant(&self) -> i8 { + match self { + Self::MainHand(_) => 0, + Self::OffHand(_) => 1, + Self::Feet(_) => 2, + Self::Legs(_) => 3, + Self::Chest(_) => 4, + Self::Head(_) => 5, + Self::Body(_) => 6, + Self::Saddle(_) => 7, + } + } +} diff --git a/pumpkin-inventory/src/generic_container_screen_handler.rs b/pumpkin-inventory/src/generic_container_screen_handler.rs new file mode 100644 index 000000000..0b2ce285b --- /dev/null +++ b/pumpkin-inventory/src/generic_container_screen_handler.rs @@ -0,0 +1,127 @@ +use std::{any::Any, sync::Arc}; + +use async_trait::async_trait; +use pumpkin_data::screen::WindowType; +use pumpkin_world::{inventory::Inventory, item::ItemStack}; + +use crate::{ + player::player_inventory::PlayerInventory, + screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour}, + slot::NormalSlot, +}; + +pub fn create_generic_9x3( + sync_id: u8, + player_inventory: &Arc, + inventory: Arc, +) -> GenericContainerScreenHandler { + GenericContainerScreenHandler::new( + WindowType::Generic9x3, + sync_id, + player_inventory, + inventory, + 3, + ) +} + +pub struct GenericContainerScreenHandler { + pub inventory: Arc, + pub rows: u8, + behaviour: ScreenHandlerBehaviour, +} + +impl GenericContainerScreenHandler { + fn new( + screen_type: WindowType, + sync_id: u8, + player_inventory: &Arc, + inventory: Arc, + rows: u8, + ) -> Self { + let mut handler = Self { + inventory, + rows, + behaviour: ScreenHandlerBehaviour::new(sync_id, Some(screen_type)), + }; + + //inventory.onOpen(player); + handler.add_inventory_slots(); + let player_inventory: Arc = player_inventory.clone(); + handler.add_player_slots(&player_inventory); + + handler + } + + fn add_inventory_slots(&mut self) { + for i in 0..self.rows { + for j in 0..9 { + self.add_slot(Arc::new(NormalSlot::new( + self.inventory.clone(), + (j + i * 9) as usize, + ))); + } + } + } +} + +#[async_trait] +impl ScreenHandler for GenericContainerScreenHandler { + async fn on_closed(&mut self, player: &dyn InventoryPlayer) { + self.default_on_closed(player).await; + //TODO: self.inventory.on_closed(player).await; + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn get_behaviour(&self) -> &ScreenHandlerBehaviour { + &self.behaviour + } + + fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour { + &mut self.behaviour + } + + async fn quick_move(&mut self, _player: &dyn InventoryPlayer, slot_index: i32) -> ItemStack { + let mut stack_left = ItemStack::EMPTY; + let slot = self.get_behaviour().slots[slot_index as usize].clone(); + + if slot.has_stack().await { + let slot_stack = slot.get_stack().await; + stack_left = *slot_stack.lock().await; + + if slot_index < (self.rows * 9) as i32 { + if !self + .insert_item( + &mut *slot_stack.lock().await, + (self.rows * 9).into(), + self.get_behaviour().slots.len() as i32, + true, + ) + .await + { + return ItemStack::EMPTY; + } + } else if !self + .insert_item( + &mut *slot_stack.lock().await, + 0, + (self.rows * 9).into(), + false, + ) + .await + { + return ItemStack::EMPTY; + } + + if stack_left.is_empty() { + slot.set_stack(ItemStack::EMPTY).await; + } else { + slot.mark_dirty().await; + } + } + + return stack_left; + } +} diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index 2f5762f03..fd06406f5 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -1,304 +1,14 @@ -use crate::container_click::MouseClick; -use crate::player::PlayerInventory; -use pumpkin_data::screen::WindowType; -use pumpkin_world::item::ItemStack; - pub mod container_click; +pub mod crafting; pub mod drag_handler; +pub mod entity_equipment; +pub mod equipment_slot; mod error; -mod open_container; +pub mod generic_container_screen_handler; pub mod player; +pub mod screen_handler; +pub mod slot; +pub mod sync_handler; pub mod window_property; pub use error::InventoryError; -pub use open_container::*; - -pub struct ContainerStruct([Option; SLOTS]); - -// `Container` needs to be `Sync + Send` to be able to be in the async server. -pub trait Container: Sync + Send { - fn window_type(&self) -> &'static WindowType; - - fn window_name(&self) -> &'static str; - - fn handle_item_change( - &mut self, - carried_item: &mut Option, - slot: usize, - mouse_click: MouseClick, - taking_crafted: bool, - ) -> Result<(), InventoryError> { - let all_slots = self.all_slots(); - if slot > all_slots.len() { - Err(InventoryError::InvalidSlot)? - } - if taking_crafted { - match (all_slots[slot].as_mut(), carried_item.as_mut()) { - (Some(s1), Some(s2)) => { - if s1.item.id == s2.item.id { - handle_item_change(all_slots[slot], carried_item, mouse_click); - } - } - (Some(_), None) => handle_item_change(all_slots[slot], carried_item, mouse_click), - (None, None) | (None, Some(_)) => (), - } - return Ok(()); - } - handle_item_change(carried_item, all_slots[slot], mouse_click); - - Ok(()) - } - - fn all_slots(&mut self) -> Box<[&mut Option]>; - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]>; - - fn clear_all_slots(&mut self) { - let all_slots = self.all_slots(); - for stack in all_slots { - *stack = None; - } - } - - fn all_combinable_slots(&self) -> Box<[Option<&ItemStack>]> { - self.all_slots_ref() - } - - fn all_combinable_slots_mut(&mut self) -> Box<[&mut Option]> { - self.all_slots() - } - - fn internal_pumpkin_id(&self) -> u64 { - 0 - } - - fn craft(&mut self) -> bool { - false - } - - fn crafting_output_slot(&self) -> Option { - None - } - - fn slot_in_crafting_input_slots(&self, _slot: &usize) -> bool { - false - } - - fn crafted_item_slot(&self) -> Option<&ItemStack> { - *self.all_slots_ref().get(self.crafting_output_slot()?)? - } - - fn recipe_used(&mut self) {} -} - -pub struct EmptyContainer; - -impl Container for EmptyContainer { - fn window_type(&self) -> &'static WindowType { - unreachable!( - "You should never be able to get here because this type is always wrapped in an `Option`." - ); - } - - fn window_name(&self) -> &'static str { - unreachable!( - "You should never be able to get here because this type is always wrapped in an `Option`." - ); - } - - fn all_slots(&mut self) -> Box<[&mut Option]> { - unreachable!( - "You should never be able to get here because this type is always wrapped in an `Option`." - ); - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - unreachable!( - "You should never be able to get here because this type is always wrapped in an `Option`." - ); - } -} - -pub fn handle_item_take( - carried_item: &mut Option, - item_slot: &mut Option, - mouse_click: MouseClick, -) { - let Some(item_stack) = item_slot else { - return; - }; - let mut new_item = item_stack.clone(); - - match mouse_click { - MouseClick::Left => { - *item_slot = None; - } - MouseClick::Right => { - let half = item_stack.item_count / 2; - new_item.item_count = half; - item_stack.item_count -= half; - if item_stack.item_count == 0 { - *item_slot = None; - } - } - } - *carried_item = Some(new_item); -} -pub fn handle_item_change( - carried_slot: &mut Option, - current_slot: &mut Option, - mouse_click: MouseClick, -) { - match (current_slot.as_mut(), carried_slot.as_mut()) { - // Swap or combine current and carried - (Some(current), Some(carried)) => { - if current.item.id == carried.item.id { - combine_stacks(carried_slot, current, mouse_click); - } else if mouse_click == MouseClick::Left { - std::mem::swap(carried_slot, current_slot); - } - } - // Put held stack into empty slot - (None, Some(carried_item_stack)) => match mouse_click { - MouseClick::Left => { - std::mem::swap(carried_slot, current_slot); - } - MouseClick::Right => { - let new_stack = ItemStack { - item: carried_item_stack.item.clone(), - item_count: 1, - }; - *current_slot = Some(new_stack); - carried_item_stack.item_count -= 1; - if carried_item_stack.item_count == 0 { - *carried_slot = None; - } - } - }, - // Take stack into carried - (Some(_current), None) => handle_item_take(carried_slot, current_slot, mouse_click), - (None, None) => (), - } -} - -pub fn combine_stacks( - carried_slot: &mut Option, - slot: &mut ItemStack, - mouse_click: MouseClick, -) { - let Some(carried_item) = carried_slot else { - return; - }; - - debug_assert!(carried_item.item.id == slot.item.id); - let max_size = carried_item.item.components.max_stack_size; - - let carried_change = match mouse_click { - MouseClick::Left => carried_item.item_count, - MouseClick::Right => 1, - }; - - // TODO: Check for item stack max size here - if slot.item_count + carried_change <= max_size { - slot.item_count += carried_change; - carried_item.item_count -= carried_change; - if carried_item.item_count == 0 { - *carried_slot = None; - } - } else { - let left_over = slot.item_count + carried_change - max_size; - slot.item_count = max_size; - carried_item.item_count = left_over; - } -} - -pub struct OptionallyCombinedContainer<'a, 'b> { - container: Option<&'a mut Box>, - inventory: &'b mut PlayerInventory, -} -impl<'a, 'b> OptionallyCombinedContainer<'a, 'b> { - pub fn new( - player_inventory: &'b mut PlayerInventory, - container: Option<&'a mut Box>, - ) -> Self { - Self { - inventory: player_inventory, - container, - } - } - /// Returns `None` if the slot is in the player's inventory. Returns `Some(Option<&ItemStack>)` if it's inside of the container. - pub fn get_slot_excluding_inventory(&self, slot: usize) -> Option> { - self.container.as_ref()?.all_slots_ref().get(slot).copied() - } -} - -impl<'a> Container for OptionallyCombinedContainer<'a, 'a> { - fn window_type(&self) -> &'static WindowType { - if let Some(container) = &self.container { - container.window_type() - } else { - &WindowType::Generic9x1 - } - } - - fn window_name(&self) -> &'static str { - self.container - .as_ref() - .map(|container| container.window_name()) - .unwrap_or(self.inventory.window_name()) - } - - fn all_slots(&mut self) -> Box<[&mut Option]> { - match &mut self.container { - Some(container) => { - let mut slots = container.all_slots().into_vec(); - slots.extend(self.inventory.all_combinable_slots_mut()); - slots.into_boxed_slice() - } - None => self.inventory.all_slots(), - } - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - match &self.container { - Some(container) => { - let mut slots = container.all_slots_ref().into_vec(); - slots.extend(self.inventory.all_combinable_slots()); - slots.into_boxed_slice() - } - None => self.inventory.all_slots_ref(), - } - } - - fn craft(&mut self) -> bool { - match &mut self.container { - Some(container) => container.craft(), - None => self.inventory.craft(), - } - } - - fn crafting_output_slot(&self) -> Option { - match &self.container { - Some(container) => container.crafting_output_slot(), - None => self.inventory.crafting_output_slot(), - } - } - - fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { - match &self.container { - Some(container) => { - // We don't have to worry about length due to inventory crafting slots being inaccessible - // while inside container interfaces. - container.slot_in_crafting_input_slots(slot) - } - None => self.inventory.slot_in_crafting_input_slots(slot), - } - } - - fn recipe_used(&mut self) { - match &mut self.container { - Some(container) => container.recipe_used(), - None => self.inventory.recipe_used(), - } - } -} diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs deleted file mode 100644 index 67ca91136..000000000 --- a/pumpkin-inventory/src/open_container.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::Container; -use pumpkin_data::Block; -use pumpkin_data::screen::WindowType; -use pumpkin_util::math::position::BlockPos; -use pumpkin_world::item::ItemStack; -use std::sync::Arc; -use tokio::sync::Mutex; - -pub struct OpenContainer { - // TODO: unique id should be here - // TODO: should this be uuid? - players: Vec, - container: Arc>>, - location: Option, - block: Option, -} - -impl OpenContainer { - pub fn try_open(&self, player_id: i32) -> Option<&Arc>>> { - if !self.players.contains(&player_id) { - log::debug!("couldn't open container"); - return None; - } - let container = &self.container; - Some(container) - } - - pub fn add_player(&mut self, player_id: i32) { - if !self.players.contains(&player_id) { - self.players.push(player_id); - } - } - - pub fn remove_player(&mut self, player_id: i32) { - if let Some(index) = self.players.iter().enumerate().find_map(|(index, id)| { - if *id == player_id { Some(index) } else { None } - }) { - self.players.remove(index); - } - } - - pub fn new_empty_container( - player_id: i32, - location: Option, - block: Option, - ) -> Self { - Self { - players: vec![player_id], - container: Arc::new(Mutex::new(Box::new(C::default()))), - location, - block, - } - } - - pub fn is_location(&self, try_position: BlockPos) -> bool { - if let Some(location) = self.location { - location == try_position - } else { - false - } - } - - pub async fn clear_all_slots(&self) { - self.container.lock().await.clear_all_slots(); - } - - pub fn clear_all_players(&mut self) { - self.players.clear(); - } - - pub fn all_player_ids(&self) -> Vec { - self.players.clone() - } - - pub fn get_number_of_players(&self) -> usize { - self.players.len() - } - - pub fn get_location(&self) -> Option { - self.location - } - - pub async fn set_location(&mut self, location: Option) { - self.location = location; - } - - pub fn get_block(&self) -> Option { - self.block.clone() - } -} -#[derive(Default)] -pub struct ChestContainer([Option; 27]); - -impl ChestContainer { - pub fn new() -> Self { - Self([const { None }; 27]) - } -} -impl Container for ChestContainer { - fn window_type(&self) -> &'static WindowType { - &WindowType::Generic9x3 - } - - fn window_name(&self) -> &'static str { - "Chest" - } - fn all_slots(&mut self) -> Box<[&mut Option]> { - self.0.iter_mut().collect() - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - self.0.iter().map(|slot| slot.as_ref()).collect() - } -} - -#[derive(Default)] -pub struct CraftingTable { - input: [[Option; 3]; 3], - output: Option, -} - -impl CraftingTable { - const SLOT_OUTPUT: usize = 0; - const SLOT_INPUT_START: usize = 1; - const SLOT_INPUT_END: usize = 9; -} - -impl Container for CraftingTable { - fn window_type(&self) -> &'static WindowType { - &WindowType::Crafting - } - - fn window_name(&self) -> &'static str { - "Crafting Table" - } - fn all_slots(&mut self) -> Box<[&mut Option]> { - let slots = vec![&mut self.output]; - - slots - .into_iter() - .chain(self.input.iter_mut().flatten()) - .collect() - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - let slots = vec![self.output.as_ref()]; - - slots - .into_iter() - .chain(self.input.iter().flatten().map(|i| i.as_ref())) - .collect() - } - - fn all_combinable_slots(&self) -> Box<[Option<&ItemStack>]> { - self.input.iter().flatten().map(|s| s.as_ref()).collect() - } - - fn all_combinable_slots_mut(&mut self) -> Box<[&mut Option]> { - self.input.iter_mut().flatten().collect() - } - - fn craft(&mut self) -> bool { - // TODO: Is there a better way to do this? - let _check = [ - [ - self.input[0][0].as_ref(), - self.input[0][1].as_ref(), - self.input[0][2].as_ref(), - ], - [ - self.input[1][0].as_ref(), - self.input[1][1].as_ref(), - self.input[1][2].as_ref(), - ], - [ - self.input[2][0].as_ref(), - self.input[2][1].as_ref(), - self.input[2][2].as_ref(), - ], - ]; - - let new_output = None; //check_if_matches_crafting(check); - let result = new_output != self.output - || self.input.iter().flatten().any(|s| s.is_some()) - || new_output.is_some(); - - self.output = new_output; - result - } - - fn crafting_output_slot(&self) -> Option { - Some(Self::SLOT_OUTPUT) - } - - fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { - (Self::SLOT_INPUT_START..=Self::SLOT_INPUT_END).contains(slot) - } - fn recipe_used(&mut self) { - self.input.iter_mut().flatten().for_each(|slot| { - if let Some(item) = slot { - if item.item_count > 1 { - item.item_count -= 1; - } else { - *slot = None; - } - } - }) - } -} - -#[derive(Default)] -pub struct Furnace { - cook: Option, - fuel: Option, - output: Option, -} - -impl Container for Furnace { - fn window_type(&self) -> &'static WindowType { - &WindowType::Furnace - } - - fn window_name(&self) -> &'static str { - "Furnace" - } - fn all_slots(&mut self) -> Box<[&mut Option]> { - Box::new([&mut self.cook, &mut self.fuel, &mut self.output]) - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - Box::new([self.cook.as_ref(), self.fuel.as_ref(), self.output.as_ref()]) - } -} diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs deleted file mode 100644 index 724c3e868..000000000 --- a/pumpkin-inventory/src/player.rs +++ /dev/null @@ -1,381 +0,0 @@ -use crate::container_click::MouseClick; -use crate::{Container, InventoryError, WindowType, handle_item_change}; -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 const SLOT_CRAFT_OUTPUT: usize = 0; -pub const SLOT_CRAFT_INPUT_START: usize = 1; -pub const SLOT_CRAFT_INPUT_END: usize = 4; -pub const SLOT_HELM: usize = 5; -pub const SLOT_CHEST: usize = 6; -pub const SLOT_LEG: usize = 7; -pub const SLOT_BOOT: usize = 8; -pub const SLOT_INV_START: usize = 9; -pub const SLOT_INV_END: usize = 35; -pub const SLOT_HOTBAR_START: usize = 36; -pub const SLOT_HOTBAR_END: usize = 44; -pub const SLOT_OFFHAND: usize = 45; - -pub const SLOT_HOTBAR_INDEX: usize = SLOT_HOTBAR_END - SLOT_HOTBAR_START; -pub const SLOT_MAX: usize = SLOT_OFFHAND; -pub const SLOT_INDEX_OUTSIDE: i16 = -999; - -pub struct PlayerInventory { - // Main inventory + hotbar - crafting: [Option; 4], - crafting_output: Option, - items: [Option; 36], - armor: [Option; 4], - offhand: Option, - /// The hotbar's current selected slot. - pub selected: usize, - pub state_id: u32, - // Notchian server wraps this value at 100, we can just keep it as a u8 that automatically wraps. - pub total_opened_containers: i32, -} - -impl Default for PlayerInventory { - fn default() -> Self { - Self::new() - } -} - -impl PlayerInventory { - pub const CONTAINER_ID: i8 = 0; - - pub fn new() -> Self { - Self { - crafting: [const { None }; 4], - crafting_output: None, - items: [const { None }; 36], - armor: [const { None }; 4], - offhand: None, - // TODO: What happens when a player spawns in with a different index? - selected: 0, - state_id: 0, - total_opened_containers: 2, - } - } - /// Set the contents of an item in a slot. - /// - /// ## `item` - /// The optional item to place in the slot - /// - /// ## `item_allowed_override` - /// An override, which when enabled, makes it so that invalid items can be placed in slots they normally can't. - /// Useful functionality for plugins in the future. - pub fn set_slot( - &mut self, - slot: usize, - item: Option, - item_allowed_override: bool, - ) -> Result<(), InventoryError> { - if item_allowed_override { - if !(0..=SLOT_MAX).contains(&slot) { - Err(InventoryError::InvalidSlot)? - } - *self.all_slots()[slot] = item; - return Ok(()); - } - let slot_condition = self.slot_condition(slot)?; - if let Some(item) = item { - if slot_condition(&item) { - *self.all_slots()[slot] = Some(item); - } - } - Ok(()) - } - #[allow(clippy::type_complexity)] - pub fn slot_condition( - &self, - slot: usize, - ) -> Result bool>, InventoryError> { - if !(0..=SLOT_MAX).contains(&slot) { - return Err(InventoryError::InvalidSlot); - } - - Ok(Box::new(match slot { - SLOT_CRAFT_OUTPUT..=SLOT_CRAFT_INPUT_END | SLOT_INV_START..=SLOT_OFFHAND => |_| true, - SLOT_HELM => |item: &ItemStack| item.is_helmet(), - SLOT_CHEST => |item: &ItemStack| item.is_chestplate(), - SLOT_LEG => |item: &ItemStack| item.is_leggings(), - SLOT_BOOT => |item: &ItemStack| item.is_boots(), - _ => unreachable!(), - })) - } - pub fn get_slot(&mut self, slot: usize) -> Result<&mut Option, InventoryError> { - match slot { - SLOT_CRAFT_OUTPUT => { - // TODO: Add crafting check here - Ok(&mut self.crafting_output) - } - SLOT_CRAFT_INPUT_START..=SLOT_CRAFT_INPUT_END => { - Ok(&mut self.crafting[slot - SLOT_CRAFT_INPUT_START]) - } - SLOT_HELM..=SLOT_BOOT => Ok(&mut self.armor[slot - SLOT_HELM]), - SLOT_INV_START..=SLOT_HOTBAR_END => Ok(&mut self.items[slot - SLOT_INV_START]), - SLOT_OFFHAND => Ok(&mut self.offhand), - _ => Err(InventoryError::InvalidSlot), - } - } - pub fn set_selected(&mut self, slot: usize) { - debug_assert!((0..=SLOT_HOTBAR_INDEX).contains(&slot)); - self.selected = slot; - } - - pub fn get_selected_slot(&self) -> usize { - self.selected + SLOT_HOTBAR_START - } - - pub fn increment_state_id(&mut self) { - self.state_id = self.state_id % 100 + 1; - } - - pub async fn get_mining_speed(&self, block_name: &str) -> f32 { - self.held_item() - .map_or_else(|| 1.0, |e| e.get_speed(block_name)) - } - - // NOTE: We actually want &mut Option instead of Option<&mut> - pub fn held_item_mut(&mut self) -> &mut Option { - debug_assert!((0..=SLOT_HOTBAR_INDEX).contains(&self.selected)); - &mut self.items[self.get_selected_slot() - SLOT_INV_START] - } - - #[inline] - pub fn held_item(&self) -> Option<&ItemStack> { - debug_assert!((0..=SLOT_HOTBAR_INDEX).contains(&self.selected)); - self.items[self.get_selected_slot() - SLOT_INV_START].as_ref() - } - - pub fn decrease_current_stack(&mut self, amount: u8) -> bool { - let held_item = self.held_item_mut(); - if let Some(item_stack) = held_item { - item_stack.item_count -= amount; - if item_stack.item_count == 0 { - *held_item = None; - } - return true; - }; - false - } - - pub fn get_empty_hotbar_slot(&self) -> usize { - if self.held_item().is_none() { - return self.selected; - } - - for slot in SLOT_HOTBAR_START..=SLOT_HOTBAR_END { - if self.items[slot - SLOT_INV_START].is_none() { - return slot - SLOT_HOTBAR_START; - } - } - - self.selected - } - - pub fn get_slot_filtered(&self, filter: &F) -> Option - where - F: Fn(Option<&ItemStack>) -> bool, - { - // Check selected slot - if filter(self.items[self.get_selected_slot() - SLOT_INV_START].as_ref()) { - Some(self.get_selected_slot()) - } - // Check hotbar slots (27-35) first - else if let Some(index) = self.items - [SLOT_HOTBAR_START - SLOT_INV_START..=SLOT_HOTBAR_END - SLOT_INV_START] - .iter() - .enumerate() - .position(|(index, item_stack)| index != self.selected && filter(item_stack.as_ref())) - { - Some(index + SLOT_HOTBAR_START) - } - // Then check main inventory slots (0-26) - else if let Some(index) = self.items[0..=SLOT_INV_END - SLOT_INV_START] - .iter() - .position(|item_stack| filter(item_stack.as_ref())) - { - Some(index + SLOT_INV_START) - } - // Check offhand - else if filter(self.offhand.as_ref()) { - Some(SLOT_OFFHAND) - } else { - None - } - } - - pub fn get_nonfull_slot_with_item(&self, item_id: u16) -> Option { - let max_stack = Item::from_id(item_id) - .expect("We passed an invalid item id") - .components - .max_stack_size; - - self.get_slot_filtered(&|item_stack| { - item_stack.is_some_and(|item_stack| { - item_stack.item.id == item_id && item_stack.item_count < max_stack - }) - }) - } - - /// Returns a slot that has an item with less than the max stack size. If none, returns an empty - /// slot. If none, returns `None`.` - pub fn get_pickup_item_slot(&self, item_id: u16) -> Option { - self.get_nonfull_slot_with_item(item_id) - .or_else(|| self.get_empty_slot()) - } - - pub fn get_slot_with_item(&self, item_id: u16) -> Option { - self.get_slot_filtered(&|item_stack| { - item_stack.is_some_and(|item_stack| item_stack.item.id == item_id) - }) - } - - pub fn get_empty_slot(&self) -> Option { - self.get_slot_filtered(&|item_stack| item_stack.is_none()) - } - - pub fn get_empty_slot_no_order(&self) -> Option { - self.items - .iter() - .position(|slot| slot.is_none()) - .map(|index| index + SLOT_INV_START) - } - - pub fn slots(&self) -> Box<[Option<&ItemStack>]> { - let mut slots = vec![self.crafting_output.as_ref()]; - slots.extend(self.crafting.iter().map(|c| c.as_ref())); - slots.extend(self.armor.iter().map(|c| c.as_ref())); - slots.extend(self.items.iter().map(|c| c.as_ref())); - slots.push(self.offhand.as_ref()); - slots.into_boxed_slice() - } - - pub fn slots_mut(&mut self) -> Box<[&mut Option]> { - let mut slots = vec![&mut self.crafting_output]; - slots.extend(self.crafting.iter_mut()); - slots.extend(self.armor.iter_mut()); - slots.extend(self.items.iter_mut()); - slots.push(&mut self.offhand); - slots.into_boxed_slice() - } - - pub fn armor_slots(&self) -> Box<[Option<&ItemStack>]> { - self.armor.iter().map(|item| item.as_ref()).collect() - } - - pub fn crafting_slots(&self) -> Box<[Option<&ItemStack>]> { - let mut slots = vec![self.crafting_output.as_ref()]; - slots.extend(self.crafting.iter().map(|c| c.as_ref())); - slots.into_boxed_slice() - } - - pub fn item_slots(&self) -> Box<[Option<&ItemStack>]> { - self.items.iter().map(|item| item.as_ref()).collect() - } - - pub fn offhand_slot(&self) -> Option<&ItemStack> { - self.offhand.as_ref() - } - - pub fn iter_items_mut(&mut self) -> IterMut> { - self.items.iter_mut() - } - - pub fn slots_with_hotbar_first( - &mut self, - ) -> Chain>, IterMut>> { - let (items, hotbar) = self.items.split_at_mut(SLOT_HOTBAR_START - SLOT_INV_START); - hotbar.iter_mut().chain(items) - } -} - -impl Container for PlayerInventory { - fn window_type(&self) -> &'static WindowType { - &WindowType::Generic9x1 - } - - fn window_name(&self) -> &'static str { - // We never send an `OpenContainer` with inventory, so it has no name. - "" - } - - fn handle_item_change( - &mut self, - carried_slot: &mut Option, - slot: usize, - mouse_click: MouseClick, - invert: bool, - ) -> Result<(), InventoryError> { - let slot_condition = self.slot_condition(slot)?; - let item_slot = self.get_slot(slot)?; - if let Some(item) = carried_slot { - debug_assert!( - item.item_count > 0, - "We aren't setting the stack to `None` somewhere" - ); - if slot_condition(item) { - if invert { - handle_item_change(item_slot, carried_slot, mouse_click); - } else { - handle_item_change(carried_slot, item_slot, mouse_click); - } - } else { - return Err(InventoryError::InvalidSlot); - } - } else if invert { - handle_item_change(item_slot, carried_slot, mouse_click); - } else { - handle_item_change(carried_slot, item_slot, mouse_click) - } - Ok(()) - } - - fn all_slots(&mut self) -> Box<[&mut Option]> { - self.slots_mut() - } - - fn all_slots_ref(&self) -> Box<[Option<&ItemStack>]> { - self.slots() - } - - fn all_combinable_slots(&self) -> Box<[Option<&ItemStack>]> { - self.items.iter().map(|item| item.as_ref()).collect() - } - - fn all_combinable_slots_mut(&mut self) -> Box<[&mut Option]> { - self.items.iter_mut().collect() - } - - fn craft(&mut self) -> bool { - let v1 = [self.crafting[0].as_ref(), self.crafting[1].as_ref(), None]; - let v2 = [self.crafting[2].as_ref(), self.crafting[3].as_ref(), None]; - let v3 = [const { None }; 3]; - let _together = [v1, v2, v3]; - - self.crafting_output = None; //check_if_matches_crafting(together); - self.crafting.iter().any(|s| s.is_some()) - } - - fn crafting_output_slot(&self) -> Option { - Some(SLOT_CRAFT_OUTPUT) - } - - fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { - (SLOT_CRAFT_INPUT_START..=SLOT_CRAFT_INPUT_END).contains(slot) - } -} diff --git a/pumpkin-inventory/src/player/mod.rs b/pumpkin-inventory/src/player/mod.rs new file mode 100644 index 000000000..459a4be7c --- /dev/null +++ b/pumpkin-inventory/src/player/mod.rs @@ -0,0 +1,2 @@ +pub mod player_inventory; +pub mod player_screen_handler; diff --git a/pumpkin-inventory/src/player/player_inventory.rs b/pumpkin-inventory/src/player/player_inventory.rs new file mode 100644 index 000000000..c4a1336a7 --- /dev/null +++ b/pumpkin-inventory/src/player/player_inventory.rs @@ -0,0 +1,409 @@ +use crate::entity_equipment::EntityEquipment; +use crate::equipment_slot::EquipmentSlot; +use crate::screen_handler::InventoryPlayer; +use async_trait::async_trait; +use pumpkin_protocol::client::play::CSetPlayerInventory; +use pumpkin_world::inventory::split_stack; +use pumpkin_world::inventory::{Clearable, Inventory}; +use pumpkin_world::item::ItemStack; +use std::array::from_fn; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU8; +use tokio::sync::Mutex; + +#[derive(Debug)] +pub struct PlayerInventory { + pub main_inventory: [Arc>; Self::MAIN_SIZE], + pub equipment_slots: HashMap, + selected_slot: AtomicU8, + pub entity_equipment: Arc>, +} + +impl PlayerInventory { + const MAIN_SIZE: usize = 36; + const HOTBAR_SIZE: usize = 9; + const OFF_HAND_SLOT: usize = 40; + + // TODO: Add inventory load from nbt + pub fn new(entity_equipment: Arc>) -> Self { + Self { + // Normal syntax can't be used here because Arc doesn't implement Copy + main_inventory: from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY))), + equipment_slots: Self::build_equipment_slots(), + selected_slot: AtomicU8::new(0), + entity_equipment, + } + } + + /// getSelectedStack in source + pub fn held_item(&self) -> Arc> { + self.main_inventory + .get(self.get_selected_slot() as usize) + .unwrap() + .clone() + } + + pub fn is_valid_hotbar_index(slot: usize) -> bool { + slot < Self::HOTBAR_SIZE + } + + fn build_equipment_slots() -> HashMap { + let mut equipment_slots = HashMap::new(); + equipment_slots.insert( + EquipmentSlot::FEET.get_offset_entity_slot_id(Self::MAIN_SIZE as i32) as usize, + EquipmentSlot::FEET, + ); + equipment_slots.insert( + EquipmentSlot::LEGS.get_offset_entity_slot_id(Self::MAIN_SIZE as i32) as usize, + EquipmentSlot::LEGS, + ); + equipment_slots.insert( + EquipmentSlot::CHEST.get_offset_entity_slot_id(Self::MAIN_SIZE as i32) as usize, + EquipmentSlot::CHEST, + ); + equipment_slots.insert( + EquipmentSlot::HEAD.get_offset_entity_slot_id(Self::MAIN_SIZE as i32) as usize, + EquipmentSlot::HEAD, + ); + equipment_slots.insert(40, EquipmentSlot::OFF_HAND); + equipment_slots + } + + 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 { + return self.add_stack_to_slot(slot_index as usize, stack).await; + } + } + + async fn add_stack_to_slot(&self, slot: usize, stack: ItemStack) -> usize { + let mut stack_count = stack.item_count; + let binding = self.get_stack(slot).await; + let mut self_stack = binding.lock().await; + + if self_stack.is_empty() { + *self_stack = stack.copy_with_count(0); + //self.set_stack(slot, self_stack).await; + } + + 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 as usize + } else { + stack_count -= count_min; + self_stack.increment(count_min); + stack_count as usize + } + } + + async fn get_empty_slot(&self) -> i16 { + for i in 0..Self::MAIN_SIZE { + if self.main_inventory[i].lock().await.is_empty() { + return i as i16; + } + } + + -1 + } + + fn can_stack_add_more(&self, 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() + } + + async fn get_occupied_slot_with_room_for_stack(&self, stack: &ItemStack) -> i16 { + if self.can_stack_add_more( + &*self + .get_stack(self.get_selected_slot() as usize) + .await + .lock() + .await, + stack, + ) { + self.get_selected_slot() as i16 + } else if self.can_stack_add_more( + &*self.get_stack(Self::OFF_HAND_SLOT).await.lock().await, + stack, + ) { + return Self::OFF_HAND_SLOT as i16; + } else { + for i in 0..Self::MAIN_SIZE { + if self.can_stack_add_more(&*self.main_inventory[i].lock().await, stack) { + return i as i16; + } + } + + return -1; + } + } + + pub async fn insert_stack_anywhere(&self, stack: &mut ItemStack) -> bool { + self.insert_stack(-1, stack).await + } + + pub async fn insert_stack(&self, slot: i16, stack: &mut ItemStack) -> bool { + if stack.is_empty() { + return false; + } + + // TODO: if (stack.isDamaged()) { + + let mut i; + + loop { + i = stack.item_count; + if slot == -1 { + stack.set_count(self.add_stack(*stack).await as u8); + } else { + stack.set_count(self.add_stack_to_slot(slot as usize, *stack).await as u8); + } + + if stack.is_empty() || stack.item_count >= i { + break; + } + } + + // TODO: Creative mode check + + stack.item_count < i + } + + pub async fn get_slot_with_stack(&self, stack: &ItemStack) -> i16 { + for i in 0..Self::MAIN_SIZE { + if !self.main_inventory[i].lock().await.is_empty() + && self.main_inventory[i] + .lock() + .await + .are_items_and_components_equal(stack) + { + return i as i16; + } + } + + -1 + } + + pub async fn get_swappable_hotbar_slot(&self) -> usize { + let selected_slot = self.get_selected_slot() as usize; + for i in 0..Self::HOTBAR_SIZE { + let check_index = (i + selected_slot) % 9; + if self.main_inventory[check_index].lock().await.is_empty() { + return check_index; + } + } + + for i in 0..Self::HOTBAR_SIZE { + let check_index = (i + selected_slot) % 9; + if true + /*TODO: If item has an enchantment skip it */ + { + return check_index; + } + } + + self.get_selected_slot() as usize + } + + pub async fn swap_stack_with_hotbar(&self, stack: ItemStack) { + self.set_selected_slot(self.get_swappable_hotbar_slot().await as u8); + + if !self.main_inventory[self.get_selected_slot() as usize] + .lock() + .await + .is_empty() + { + let empty_slot = self.get_empty_slot().await; + if empty_slot != -1 { + self.set_stack( + empty_slot as usize, + *self.main_inventory[self.get_selected_slot() as usize] + .lock() + .await, + ) + .await; + } + } + + self.set_stack(self.get_selected_slot() as usize, stack) + .await; + } + + pub async fn swap_slot_with_hotbar(&self, slot: usize) { + self.set_selected_slot(self.get_swappable_hotbar_slot().await as u8); + let stack = *self.main_inventory[self.get_selected_slot() as usize] + .lock() + .await; + self.set_stack( + self.get_selected_slot() as usize, + *self.main_inventory[slot].lock().await, + ) + .await; + self.set_stack(slot, stack).await; + } + + pub async fn offer_or_drop_stack(&self, stack: ItemStack, player: &dyn InventoryPlayer) { + self.offer(stack, true, player).await; + } + + 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 + .lock() + .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( + (room_for_stack as i32).into(), + &stack.into(), + )) + .await; + } + } + } +} + +#[async_trait] +impl Clearable for PlayerInventory { + async fn clear(&self) { + for item in self.main_inventory.iter() { + *item.lock().await = ItemStack::EMPTY; + } + + self.entity_equipment.lock().await.clear(); + } +} + +#[async_trait] +impl Inventory for PlayerInventory { + fn size(&self) -> usize { + self.main_inventory.len() + self.equipment_slots.len() + } + + async fn is_empty(&self) -> bool { + for item in self.main_inventory.iter() { + if !item.lock().await.is_empty() { + return false; + } + } + + for slot in self.equipment_slots.values() { + if !self + .entity_equipment + .lock() + .await + .get(slot) + .lock() + .await + .is_empty() + { + return false; + } + } + + true + } + + async fn get_stack(&self, slot: usize) -> Arc> { + if slot < self.main_inventory.len() { + self.main_inventory[slot].clone() + } else { + let slot = self.equipment_slots.get(&slot).unwrap(); + self.entity_equipment.lock().await.get(slot) + } + } + + async fn remove_stack_specific(&self, slot: usize, amount: u8) -> ItemStack { + if slot < self.main_inventory.len() { + split_stack(&self.main_inventory, slot, amount).await + } else { + let slot = self.equipment_slots.get(&slot).unwrap(); + + let equipment = self.entity_equipment.lock().await.get(slot); + let mut stack = equipment.lock().await; + + if !stack.is_empty() { + return stack.split(amount); + } + + ItemStack::EMPTY + } + } + + async fn remove_stack(&self, slot: usize) -> ItemStack { + if slot < self.main_inventory.len() { + let mut removed = ItemStack::EMPTY; + let mut guard = self.main_inventory[slot].lock().await; + std::mem::swap(&mut removed, &mut *guard); + removed + } else { + let slot = self.equipment_slots.get(&slot).unwrap(); + self.entity_equipment + .lock() + .await + .put(slot, ItemStack::EMPTY) + .await + } + } + + async fn set_stack(&self, slot: usize, stack: ItemStack) { + if slot < self.main_inventory.len() { + *self.main_inventory[slot].lock().await = stack; + } else { + match self.equipment_slots.get(&slot) { + Some(slot) => { + self.entity_equipment.lock().await.put(slot, stack).await; + } + None => log::warn!("Failed to get Equipment Slot at {0}", slot), + } + } + } + + fn mark_dirty(&self) {} +} + +impl PlayerInventory { + pub fn set_selected_slot(&self, slot: u8) { + if Self::is_valid_hotbar_index(slot as usize) { + self.selected_slot + .store(slot, std::sync::atomic::Ordering::Relaxed); + } else { + panic!("Invalid hotbar slot: {}", slot); + } + } + + pub fn get_selected_slot(&self) -> u8 { + self.selected_slot + .load(std::sync::atomic::Ordering::Relaxed) + } +} diff --git a/pumpkin-inventory/src/player/player_screen_handler.rs b/pumpkin-inventory/src/player/player_screen_handler.rs new file mode 100644 index 000000000..21a6a3aed --- /dev/null +++ b/pumpkin-inventory/src/player/player_screen_handler.rs @@ -0,0 +1,170 @@ +use std::{any::Any, sync::Arc}; + +use crate::{ + crafting::{ + crafting_inventory::CraftingInventory, + crafting_screen_handler::CraftingScreenHandler, + recipes::{RecipeFinderScreenHandler, RecipeInputInventory}, + }, + equipment_slot::EquipmentSlot, + screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour}, + slot::{ArmorSlot, NormalSlot, Slot}, +}; +use async_trait::async_trait; +use pumpkin_data::screen::WindowType; +use pumpkin_world::inventory::Inventory; +use pumpkin_world::item::ItemStack; + +use super::player_inventory::PlayerInventory; + +pub struct PlayerScreenHandler { + behaviour: ScreenHandlerBehaviour, + crafting_inventory: Arc, +} + +impl RecipeFinderScreenHandler for PlayerScreenHandler {} + +impl CraftingScreenHandler for PlayerScreenHandler {} + +// TODO: Fully implement this +impl PlayerScreenHandler { + const EQUIPMENT_SLOT_ORDER: [EquipmentSlot; 4] = [ + EquipmentSlot::HEAD, + EquipmentSlot::CHEST, + EquipmentSlot::LEGS, + EquipmentSlot::FEET, + ]; + + pub fn is_in_hotbar(slot: u8) -> bool { + (36..45).contains(&slot) || slot == 45 + } + + pub async fn get_slot(&self, slot: usize) -> Arc { + self.behaviour.slots[slot].clone() + } + + pub async fn new( + player_inventory: &Arc, + window_type: Option, + sync_id: u8, + ) -> Self { + let crafting_inventory: Arc = + Arc::new(CraftingInventory::new(2, 2)); + + let mut player_screen_handler = PlayerScreenHandler { + behaviour: ScreenHandlerBehaviour::new(sync_id, window_type), + crafting_inventory: crafting_inventory.clone(), + }; + + player_screen_handler + .add_result_slot(&crafting_inventory) + .await; + + player_screen_handler + .add_input_slots(&crafting_inventory) + .await; + + for i in 0..4 { + player_screen_handler.add_slot(Arc::new(ArmorSlot::new( + player_inventory.clone(), + 39 - i, + Self::EQUIPMENT_SLOT_ORDER[i].clone(), + ))); + } + + let player_inventory: Arc = player_inventory.clone(); + + player_screen_handler.add_player_slots(&player_inventory); + + // Offhand + // TODO: public void setStack(ItemStack stack, ItemStack previousStack) { owner.onEquipStack(EquipmentSlot.OFFHAND, previousStack, stack); + player_screen_handler.add_slot(Arc::new(NormalSlot::new(player_inventory.clone(), 40))); + + player_screen_handler + } +} + +#[async_trait] +impl ScreenHandler for PlayerScreenHandler { + async fn on_closed(&mut self, player: &dyn InventoryPlayer) { + self.default_on_closed(player).await; + //TODO: this.craftingResultInventory.clear(); + self.drop_inventory(player, self.crafting_inventory.clone()) + .await; + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn get_behaviour(&self) -> &ScreenHandlerBehaviour { + &self.behaviour + } + + fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour { + &mut self.behaviour + } + + async fn quick_move(&mut self, player: &dyn InventoryPlayer, slot_index: i32) -> ItemStack { + let mut stack_left = ItemStack::EMPTY; + let slot = self.get_behaviour().slots[slot_index as usize].clone(); + + // TODO: Equippable component + + if slot.has_stack().await { + let slot_stack = slot.get_stack().await; + let mut slot_stack = slot_stack.lock().await; + stack_left = *slot_stack; + + #[allow(clippy::if_same_then_else)] + if slot_index == 0 { + if !self.insert_item(&mut slot_stack, 9, 45, true).await { + return ItemStack::EMPTY; + } + + slot.on_quick_transfer(*slot_stack, stack_left); + } else if (1..5).contains(&slot_index) { + if !self.insert_item(&mut slot_stack, 9, 45, false).await { + return ItemStack::EMPTY; + } + } else if (5..9).contains(&slot_index) { + if !self.insert_item(&mut slot_stack, 9, 45, false).await { + return ItemStack::EMPTY; + } + } else if (9..36).contains(&slot_index) { + if !self.insert_item(&mut slot_stack, 36, 45, false).await { + return ItemStack::EMPTY; + } + } else if (36..45).contains(&slot_index) { + if !self.insert_item(&mut slot_stack, 9, 36, false).await { + return ItemStack::EMPTY; + } + } else if !self.insert_item(&mut slot_stack, 9, 45, false).await { + return ItemStack::EMPTY; + } + + if slot_stack.is_empty() { + drop(slot_stack); + slot.set_stack_prev(ItemStack::EMPTY, stack_left).await; + } else { + drop(slot_stack); + slot.mark_dirty().await; + } + + let slot_stack = slot.get_stack().await; + let slot_stack = slot_stack.lock().await; + + if slot_stack.item_count == stack_left.item_count { + return ItemStack::EMPTY; + } + + slot.on_take_item(player, &slot_stack).await; + + if slot_index == 0 { + player.drop_item(*slot_stack, false).await; + } + } + + return stack_left; + } +} diff --git a/pumpkin-inventory/src/screen_handler.rs b/pumpkin-inventory/src/screen_handler.rs new file mode 100644 index 000000000..7737a435e --- /dev/null +++ b/pumpkin-inventory/src/screen_handler.rs @@ -0,0 +1,744 @@ +use std::{any::Any, collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use log::warn; +use pumpkin_data::screen::WindowType; +use pumpkin_protocol::{ + client::play::{ + CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, + CSetPlayerInventory, + }, + codec::item_stack_seralizer::OptionalItemStackHash, + server::play::SlotActionType, +}; +use pumpkin_util::text::TextComponent; +use pumpkin_world::inventory::{ComparableInventory, Inventory}; +use pumpkin_world::item::ItemStack; +use tokio::sync::Mutex; + +use crate::{ + container_click::MouseClick, + player::player_inventory::PlayerInventory, + slot::{NormalSlot, Slot}, + sync_handler::{SyncHandler, TrackedStack}, +}; + +const SLOT_INDEX_OUTSIDE: i32 = -999; + +pub struct ScreenProperty { + _old_value: i32, + _index: u8, + value: i32, +} + +impl ScreenProperty { + pub fn get(&self) -> i32 { + self.value + } + + pub fn set(&mut self, value: i32) { + self.value = value; + } +} + +#[async_trait] +pub trait InventoryPlayer: Send + Sync { + async fn drop_item(&self, item: ItemStack, retain_ownership: bool); + fn get_inventory(&self) -> Arc; + async fn enqueue_inventory_packet(&self, packet: &CSetContainerContent); + async fn enqueue_slot_packet(&self, packet: &CSetContainerSlot); + async fn enqueue_cursor_packet(&self, packet: &CSetCursorItem); + async fn enqueue_property_packet(&self, packet: &CSetContainerProperty); + async fn enqueue_slot_set_packet(&self, packet: &CSetPlayerInventory); +} + +pub async fn offer_or_drop_stack(player: &dyn InventoryPlayer, stack: ItemStack) { + // TODO: Super weird disconnect logic in vanilla, investigate this later + player + .get_inventory() + .offer_or_drop_stack(stack, player) + .await; +} + +//ScreenHandler.java +// TODO: Fully implement this +#[async_trait] +pub trait ScreenHandler: Send + Sync { + /// Get the window type of the screen handler, otherwise panics + fn window_type(&self) -> Option { + self.get_behaviour().window_type + } + + fn as_any(&self) -> &dyn Any; + + fn sync_id(&self) -> u8 { + self.get_behaviour().sync_id + } + + async fn on_closed(&mut self, player: &dyn InventoryPlayer) { + self.default_on_closed(player).await; + } + + async fn default_on_closed(&mut self, player: &dyn InventoryPlayer) { + let behaviour = self.get_behaviour_mut(); + if !behaviour.cursor_stack.lock().await.is_empty() { + offer_or_drop_stack(player, *behaviour.cursor_stack.lock().await).await; + *behaviour.cursor_stack.lock().await = ItemStack::EMPTY; + } + } + + fn can_use(&self, _player: &dyn InventoryPlayer) -> bool { + true + } + + async fn drop_inventory(&self, player: &dyn InventoryPlayer, inventory: Arc) { + for i in 0..inventory.size() { + offer_or_drop_stack(player, inventory.remove_stack(i).await).await; + } + } + + fn get_behaviour(&self) -> &ScreenHandlerBehaviour; + + fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour; + + fn add_slot(&mut self, slot: Arc) -> Arc { + let behaviour = self.get_behaviour_mut(); + slot.set_id(behaviour.slots.len()); + behaviour.slots.push(slot.clone()); + behaviour.tracked_stacks.push(ItemStack::EMPTY); + behaviour.previous_tracked_stacks.push(TrackedStack::EMPTY); + + slot + } + + fn add_player_hotbar_slots(&mut self, player_inventory: &Arc) { + for i in 0..9 { + self.add_slot(Arc::new(NormalSlot::new(player_inventory.clone(), i))); + } + } + + fn add_player_inventory_slots(&mut self, player_inventory: &Arc) { + for i in 0..3 { + for j in 0..9 { + self.add_slot(Arc::new(NormalSlot::new( + player_inventory.clone(), + j + (i + 1) * 9, + ))); + } + } + } + + fn add_player_slots(&mut self, player_inventory: &Arc) { + self.add_player_inventory_slots(player_inventory); + self.add_player_hotbar_slots(player_inventory); + } + + async fn copy_shared_slots(&mut self, other: Arc>) { + let mut table: HashMap> = HashMap::new(); + let other_binding = other.lock().await; + let other_behaviour = other_binding.get_behaviour(); + + for i in 0..other_behaviour.slots.len() { + let other_slot = other_behaviour.slots[i].clone(); + let mut hash_map = HashMap::new(); + hash_map.insert(other_slot.get_index(), i); + table.insert( + ComparableInventory(other_slot.get_inventory().clone()), + hash_map, + ); + } + + for i in 0..self.get_behaviour().slots.len() { + let slot = self.get_behaviour().slots[i].clone(); + let inventory = slot.get_inventory(); + let index = slot.get_index(); + + if let Some(hash_map) = table.get(&ComparableInventory(inventory.clone())) { + if let Some(other_index) = hash_map.get(&index) { + self.get_behaviour_mut().tracked_stacks[i] = + other_behaviour.tracked_stacks[*other_index]; + self.get_behaviour_mut().previous_tracked_stacks[i] = + other_behaviour.previous_tracked_stacks[*other_index].clone(); + } + } + } + } + + async fn set_received_hash(&mut self, slot: usize, hash: OptionalItemStackHash) { + let behaviour = self.get_behaviour_mut(); + if slot < behaviour.previous_tracked_stacks.len() { + behaviour.previous_tracked_stacks[slot].set_received_hash(hash); + } else { + warn!( + "Incorrect slot index: {} available slots: {}", + slot, + behaviour.previous_tracked_stacks.len() + ); + } + } + + async fn set_received_stack(&mut self, slot: usize, stack: ItemStack) { + let behaviour = self.get_behaviour_mut(); + behaviour.previous_tracked_stacks[slot].set_received_stack(stack); + } + + async fn set_received_cursor_hash(&mut self, hash: OptionalItemStackHash) { + let behaviour = self.get_behaviour_mut(); + behaviour.previous_cursor_stack.set_received_hash(hash); + } + + async fn sync_state(&mut self) { + let behaviour = self.get_behaviour_mut(); + let mut previous_tracked_stacks = Vec::new(); + + for i in 0..behaviour.slots.len() { + let stack = behaviour.slots[i].get_cloned_stack().await; + previous_tracked_stacks.push(stack); + behaviour.previous_tracked_stacks[i].set_received_stack(stack); + } + + let cursor_stack = *behaviour.cursor_stack.lock().await; + behaviour + .previous_cursor_stack + .set_received_stack(cursor_stack); + + for i in 0..behaviour.properties.len() { + let property_val = behaviour.properties[i].get(); + behaviour.tracked_property_values[i] = property_val; + } + + let next_revision = behaviour.next_revision(); + + if let Some(sync_handler) = behaviour.sync_handler.as_ref() { + sync_handler + .update_state( + behaviour, + &previous_tracked_stacks, + &cursor_stack, + behaviour.tracked_property_values.clone(), + next_revision, + ) + .await; + } + } + + async fn add_listener(&mut self, listener: Arc) { + self.get_behaviour_mut().listeners.push(listener); + self.send_content_updates().await; + } + + async fn update_sync_handler(&mut self, sync_handler: Arc) { + let behaviour = self.get_behaviour_mut(); + behaviour.sync_handler = Some(sync_handler.clone()); + self.sync_state().await; + } + + fn add_property(&mut self, property: ScreenProperty) { + let behaviour = self.get_behaviour_mut(); + behaviour.properties.push(property); + behaviour.tracked_property_values.push(0); + } + + fn add_properties(&mut self, properties: Vec) { + for property in properties { + self.add_property(property); + } + } + + async fn update_to_client(&mut self) { + for i in 0..self.get_behaviour().slots.len() { + let behaviour = self.get_behaviour_mut(); + let slot = behaviour.slots[i].clone(); + let stack = slot.get_cloned_stack().await; + self.update_tracked_slot(i, stack).await; + } + + /* TODO: Implement this + for i in 0..self.prop_size() { + let property = self.get_property(i); + self.set_tracked_property(i, property); + } */ + + self.sync_state().await; + } + + async fn update_tracked_slot(&mut self, slot: usize, stack: ItemStack) { + let behaviour = self.get_behaviour_mut(); + let other_stack = &behaviour.tracked_stacks[slot]; + if !other_stack.are_equal(&stack) { + behaviour.tracked_stacks[slot] = stack; + + for listener in behaviour.listeners.iter() { + listener.on_slot_update(behaviour, slot as u8, stack); + } + } + } + + async fn check_slot_updates(&mut self, slot: usize, stack: ItemStack) { + let behaviour = self.get_behaviour_mut(); + if !behaviour.disable_sync { + let prev_stack = &mut behaviour.previous_tracked_stacks[slot]; + + if !prev_stack.is_in_sync(&stack) { + prev_stack.set_received_stack(stack); + let next_revision = behaviour.next_revision(); + if let Some(sync_handler) = behaviour.sync_handler.as_ref() { + sync_handler + .update_slot(behaviour, slot, &stack, next_revision) + .await; + } + } + } + } + + async fn check_cursor_stack_updates(&mut self) { + let behaviour = self.get_behaviour_mut(); + if !behaviour.disable_sync { + let cursor_stack = behaviour.cursor_stack.lock().await; + if !behaviour.previous_cursor_stack.is_in_sync(&cursor_stack) { + behaviour + .previous_cursor_stack + .set_received_stack(*cursor_stack); + if let Some(sync_handler) = behaviour.sync_handler.as_ref() { + sync_handler + .update_cursor_stack(behaviour, &cursor_stack) + .await; + } + } + } + } + + async fn send_content_updates(&mut self) { + let slots_len = self.get_behaviour().slots.len(); + + for i in 0..slots_len { + let slot = self.get_behaviour().slots[i].clone(); + let stack = slot.get_cloned_stack().await; + + self.update_tracked_slot(i, stack).await; + self.check_slot_updates(i, stack).await; + } + + self.check_cursor_stack_updates().await; + + /* TODO: Implement this + for i in 0..self.prop_size() { + let property = self.get_property(i); + self.set_tracked_property(i, property); + } */ + } + + async fn is_slot_valid(&self, slot: i32) -> bool { + slot == -1 || slot == -999 || slot < self.get_behaviour().slots.len() as i32 + } + + async fn get_slot_index(&self, inventory: &Arc, slot: usize) -> Option { + for i in 0..self.get_behaviour().slots.len() { + if Arc::ptr_eq(self.get_behaviour().slots[i].get_inventory(), inventory) + && self.get_behaviour().slots[i].get_index() == slot + { + return Some(i); + } + } + + None + } + + async fn quick_move(&mut self, player: &dyn InventoryPlayer, slot_index: i32) -> ItemStack; + + async fn insert_item( + &mut self, + stack: &mut ItemStack, + start_index: i32, + end_index: i32, + from_last: bool, + ) -> bool { + let mut success = false; + let mut current_index = if from_last { + end_index - 1 + } else { + start_index + }; + + if stack.is_stackable() { + while !stack.is_empty() + && (if from_last { + current_index >= start_index + } else { + current_index < end_index + }) + { + let slot = self.get_behaviour().slots[current_index as usize].clone(); + let slot_stack = slot.get_stack().await; + let mut slot_stack = slot_stack.lock().await; + + if !slot_stack.is_empty() && slot_stack.are_items_and_components_equal(stack) { + let combined_count = slot_stack.item_count + stack.item_count; + let max_slot_count = slot.get_max_item_count_for_stack(&slot_stack).await; + if combined_count <= max_slot_count { + stack.set_count(0); + slot_stack.set_count(combined_count); + drop(slot_stack); + slot.mark_dirty().await; + success = true; + } else if slot_stack.item_count < max_slot_count { + stack.decrement(max_slot_count - slot_stack.item_count); + slot_stack.set_count(max_slot_count); + drop(slot_stack); + slot.mark_dirty().await; + success = true; + } + } + + if from_last { + current_index -= 1; + } else { + current_index += 1; + } + } + } + + if !stack.is_empty() { + if from_last { + current_index = end_index - 1; + } else { + current_index = start_index; + } + + while if from_last { + current_index >= start_index + } else { + current_index < end_index + } { + let slot = self.get_behaviour().slots[current_index as usize].clone(); + let slot_stack = slot.get_stack().await; + let slot_stack = slot_stack.lock().await; + + if slot_stack.is_empty() && slot.can_insert(stack).await { + let max_count = slot.get_max_item_count_for_stack(stack).await; + drop(slot_stack); + slot.set_stack(stack.split(max_count.min(stack.item_count))) + .await; + slot.mark_dirty().await; + success = true; + break; + } + + if from_last { + current_index -= 1; + } else { + current_index += 1; + } + } + } + + success + } + + async fn handle_slot_click( + &self, + _player: &dyn InventoryPlayer, + _click_type: MouseClick, + _slot: Arc, + _slot_stack: ItemStack, + _cursor_stack: ItemStack, + ) -> bool { + // TODO: required for bundle in the future + false + } + + async fn on_slot_click( + &mut self, + slot_index: i32, + button: i32, + action_type: SlotActionType, + player: &dyn InventoryPlayer, + ) { + self.internal_on_slot_click(slot_index, button, action_type, player) + .await; + } + + async fn internal_on_slot_click( + &mut self, + slot_index: i32, + button: i32, + action_type: SlotActionType, + player: &dyn InventoryPlayer, + ) { + //TODO: Implement quickcraft, Clone, PickupAll, Throw + if (action_type == SlotActionType::Pickup || action_type == SlotActionType::QuickMove) + && (button == 0 || button == 1) + { + let click_type = if button == 0 { + MouseClick::Left + } else { + MouseClick::Right + }; + + // Drop item if outside inventory + if slot_index == SLOT_INDEX_OUTSIDE { + let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await; + if !cursor_stack.is_empty() { + if click_type == MouseClick::Left { + player.drop_item(*cursor_stack, true).await; + *cursor_stack = ItemStack::EMPTY; + } else { + player.drop_item(cursor_stack.split(1), true).await; + } + } + } else if action_type == SlotActionType::QuickMove { + if slot_index < 0 { + return; + } + + let slot = self.get_behaviour().slots[slot_index as usize].clone(); + + if !slot.can_take_items(player).await { + return; + } + + let mut moved_stack = self.quick_move(player, slot_index).await; + + while !moved_stack.is_empty() + && ItemStack::are_items_and_components_equal( + &slot.get_cloned_stack().await, + &moved_stack, + ) + { + moved_stack = self.quick_move(player, slot_index).await; + } + } else { + // Pickup + if slot_index < 0 { + return; + } + + let slot = self.get_behaviour().slots[slot_index as usize].clone(); + + if click_type == MouseClick::Left { + slot.on_click(player).await; + } + + let slot_stack = slot.get_cloned_stack().await; + let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await; + + if self + .handle_slot_click( + player, + click_type.clone(), + slot.clone(), + slot_stack, + *cursor_stack, + ) + .await + { + return; + } + + if slot_stack.is_empty() { + if !cursor_stack.is_empty() { + //println!("Cursor -> Slot"); + let transfer_count = if click_type == MouseClick::Left { + cursor_stack.item_count + } else { + 1 + }; + *cursor_stack = + slot.insert_stack_count(*cursor_stack, transfer_count).await; + } + } else if slot.can_take_items(player).await { + if cursor_stack.is_empty() { + //println!("Slot -> Cursor"); + let take_count = if click_type == MouseClick::Left { + slot_stack.item_count + } else { + slot_stack.item_count.div_ceil(2) + }; + let taken = slot.try_take_stack_range(take_count, u8::MAX, player).await; + if let Some(taken) = taken { + // Reverse order of operations, shouldn't affect anything + *cursor_stack = taken; + slot.on_take_item(player, &taken).await; + } + } else if slot.can_insert(&cursor_stack).await { + if ItemStack::are_items_and_components_equal(&slot_stack, &cursor_stack) { + let insert_count = if click_type == MouseClick::Left { + cursor_stack.item_count + } else { + 1 + }; + *cursor_stack = + slot.insert_stack_count(*cursor_stack, insert_count).await; + } else if cursor_stack.item_count + <= slot.get_max_item_count_for_stack(&cursor_stack).await + { + let old_cursor_stack = *cursor_stack; + *cursor_stack = slot_stack; + slot.set_stack(old_cursor_stack).await; + } + } else if ItemStack::are_items_and_components_equal(&slot_stack, &cursor_stack) + { + let taken = slot + .try_take_stack_range( + slot_stack.item_count, + cursor_stack + .get_max_stack_size() + .saturating_sub(cursor_stack.item_count), + player, + ) + .await; + + if let Some(taken) = taken { + cursor_stack.increment(taken.item_count); + slot.on_take_item(player, &taken).await; + } + } + } + + slot.mark_dirty().await; + } + } else if action_type == SlotActionType::Swap && (0..9).contains(&button) || button == 40 { + let mut button_stack = *player + .get_inventory() + .get_stack(button as usize) + .await + .lock() + .await; + let source_slot = self.get_behaviour().slots[slot_index as usize].clone(); + let source_stack = source_slot.get_cloned_stack().await; + + if !button_stack.is_empty() || !source_stack.is_empty() { + if button_stack.is_empty() { + if source_slot.can_take_items(player).await { + player + .get_inventory() + .set_stack(button as usize, source_stack) + .await; + source_slot.on_take(source_stack.item_count); + source_slot.set_stack(ItemStack::EMPTY).await; + source_slot.on_take_item(player, &source_stack).await; + } + } else if source_stack.is_empty() { + if source_slot.can_insert(&button_stack).await { + let max_count = source_slot + .get_max_item_count_for_stack(&button_stack) + .await; + if button_stack.item_count > max_count { + // button_stack might need to be a ref instead of a clone + source_slot.set_stack(button_stack.split(max_count)).await; + } else { + player + .get_inventory() + .set_stack(button as usize, ItemStack::EMPTY) + .await; + source_slot.set_stack(button_stack).await; + } + } + } else if source_slot.can_take_items(player).await + && source_slot.can_insert(&button_stack).await + { + let max_count = source_slot + .get_max_item_count_for_stack(&button_stack) + .await; + if button_stack.item_count > max_count { + source_slot.set_stack(button_stack.split(max_count)).await; + source_slot.on_take_item(player, &button_stack).await; + if !player + .get_inventory() + .insert_stack_anywhere(&mut button_stack) + .await + { + player.drop_item(button_stack, true).await; + } + } else { + player + .get_inventory() + .set_stack(button as usize, source_stack) + .await; + source_slot.set_stack(button_stack).await; + source_slot.on_take_item(player, &button_stack).await; + } + } + } + } + } + + async fn disable_sync(&mut self) { + let behaviour = self.get_behaviour_mut(); + behaviour.disable_sync = true; + } + + async fn enable_sync(&mut self) { + let behaviour = self.get_behaviour_mut(); + behaviour.disable_sync = false; + } +} + +pub trait ScreenHandlerListener: Send + Sync { + fn on_slot_update( + &self, + _screen_handler: &ScreenHandlerBehaviour, + _slot: u8, + _stack: ItemStack, + ) { + } + fn on_property_update( + &self, + _screen_handler: &ScreenHandlerBehaviour, + _property: u8, + _value: i32, + ) { + } +} + +pub trait ScreenHandlerFactory: Send + Sync { + fn create_screen_handler( + &self, + sync_id: u8, + player_inventory: &Arc, + player: &dyn InventoryPlayer, + ) -> Option>>; + fn get_display_name(&self) -> TextComponent; +} + +pub struct ScreenHandlerBehaviour { + pub slots: Vec>, + pub sync_id: u8, + pub listeners: Vec>, + pub sync_handler: Option>, + //TODO: Check if this is needed + pub tracked_stacks: Vec, + pub cursor_stack: Arc>, + pub previous_tracked_stacks: Vec, + pub previous_cursor_stack: TrackedStack, + pub revision: u32, + pub disable_sync: bool, + pub properties: Vec, + pub tracked_property_values: Vec, + pub window_type: Option, +} + +impl ScreenHandlerBehaviour { + pub fn new(sync_id: u8, window_type: Option) -> Self { + Self { + slots: Vec::new(), + sync_id, + listeners: Vec::new(), + sync_handler: None, + tracked_stacks: Vec::new(), + cursor_stack: Arc::new(Mutex::new(ItemStack::EMPTY)), + previous_tracked_stacks: Vec::new(), + previous_cursor_stack: TrackedStack::EMPTY, + revision: 0, + disable_sync: false, + properties: Vec::new(), + tracked_property_values: Vec::new(), + window_type, + } + } + + pub fn next_revision(&mut self) -> u32 { + self.revision = (self.revision + 1) & 32767; + self.revision + } +} diff --git a/pumpkin-inventory/src/slot.rs b/pumpkin-inventory/src/slot.rs new file mode 100644 index 000000000..36fa38cf1 --- /dev/null +++ b/pumpkin-inventory/src/slot.rs @@ -0,0 +1,274 @@ +#![warn(unused)] +use std::{ + fmt::Debug, + sync::{Arc, atomic::AtomicU8}, + time::Duration, +}; + +use async_trait::async_trait; +use pumpkin_world::inventory::Inventory; +use pumpkin_world::item::ItemStack; +use tokio::{sync::Mutex, time::timeout}; + +use crate::{equipment_slot::EquipmentSlot, screen_handler::InventoryPlayer}; + +// Slot.java +// This is a trait due to crafting slots being a thing +#[async_trait] +pub trait Slot: Send + Sync + Debug { + fn get_inventory(&self) -> &Arc; + + fn get_index(&self) -> usize; + + fn set_id(&self, index: usize); + + fn on_quick_transfer(&self, new_item: ItemStack, original: ItemStack) { + let diff = new_item.item_count - original.item_count; + if diff > 0 { + self.on_crafted(original, diff); + } + } + + fn on_crafted(&self, _stack: ItemStack, _amount: u8) {} + + fn on_crafted_single(&self, _stack: ItemStack) {} + + fn on_take(&self, _amount: u8) {} + + async fn on_take_item(&self, _player: &dyn InventoryPlayer, _stack: &ItemStack) { + self.mark_dirty().await; + } + + // Used for plugins + async fn on_click(&self, _player: &dyn InventoryPlayer) {} + + async fn can_insert(&self, _stack: &ItemStack) -> bool { + true + } + + async fn get_stack(&self) -> Arc> { + self.get_inventory().get_stack(self.get_index()).await + } + + async fn get_cloned_stack(&self) -> ItemStack { + let stack = self.get_inventory().get_stack(self.get_index()).await; + let lock = timeout(Duration::from_secs(5), stack.lock()) + .await + .expect("Timed out while trying to acquire lock"); + + *lock + } + + async fn has_stack(&self) -> bool { + let inv = self.get_inventory(); + !inv.get_stack(self.get_index()) + .await + .lock() + .await + .is_empty() + } + + /// Make sure to drop any locks to the slot stack before calling this + async fn set_stack(&self, stack: ItemStack) { + self.set_stack_no_callbacks(stack).await; + } + + async fn set_stack_prev(&self, stack: ItemStack, _previous_stack: ItemStack) { + self.set_stack_no_callbacks(stack).await; + } + + async fn set_stack_no_callbacks(&self, stack: ItemStack) { + let inv = self.get_inventory(); + inv.set_stack(self.get_index(), stack).await; + self.mark_dirty().await; + } + + async fn mark_dirty(&self); + + async fn get_max_item_count(&self) -> u8 { + self.get_inventory().get_max_count_per_stack() + } + + async fn get_max_item_count_for_stack(&self, stack: &ItemStack) -> u8 { + self.get_max_item_count() + .await + .min(stack.get_max_stack_size()) + } + + async fn take_stack(&self, amount: u8) -> ItemStack { + let inv = self.get_inventory(); + + inv.remove_stack_specific(self.get_index(), amount).await + } + + async fn can_take_items(&self, _player: &dyn InventoryPlayer) -> bool { + true + } + + async fn try_take_stack_range( + &self, + min: u8, + max: u8, + _player: &dyn InventoryPlayer, + ) -> Option { + let min = min.min(max); + let stack = self.take_stack(min).await; + + if stack.is_empty() { + None + } else { + if self + .get_inventory() + .get_stack(self.get_index()) + .await + .lock() + .await + .is_empty() + { + self.set_stack_prev(ItemStack::EMPTY, stack).await; + } + + Some(stack) + } + } + + async fn take_stack_range(&self, min: u8, max: u8, player: &dyn InventoryPlayer) -> ItemStack { + let stack = self.try_take_stack_range(min, max, player).await; + + if let Some(stack) = &stack { + self.on_take_item(player, stack).await; + } + + stack.unwrap_or(ItemStack::EMPTY) + } + + async fn insert_stack(&self, stack: ItemStack) -> ItemStack { + let stack_item_count = stack.item_count; + self.insert_stack_count(stack, stack_item_count).await + } + + async fn insert_stack_count(&self, mut stack: ItemStack, count: u8) -> ItemStack { + if !stack.is_empty() && self.can_insert(&stack).await { + let stack_mutex = self.get_stack().await; + let mut stack_self = stack_mutex.lock().await; + let min_count = count + .min(stack.item_count) + .min(self.get_max_item_count_for_stack(&stack).await - stack_self.item_count); + + if min_count == 0 { + return stack; + } else { + if stack_self.is_empty() { + drop(stack_self); + self.set_stack(stack.split(min_count)).await; + } else if stack.are_items_and_components_equal(&stack_self) { + stack.decrement(min_count); + stack_self.increment(min_count); + let cloned_stack = *stack_self; + drop(stack_self); + self.set_stack(cloned_stack).await; + } + + return stack; + } + } else { + stack + } + } +} + +#[derive(Debug)] +/// Just called Slot in Vanilla +pub struct NormalSlot { + pub inventory: Arc, + pub index: usize, + pub id: AtomicU8, +} + +impl NormalSlot { + pub fn new(inventory: Arc, index: usize) -> Self { + Self { + inventory, + index, + id: AtomicU8::new(0), + } + } +} +#[async_trait] +impl Slot for NormalSlot { + fn get_inventory(&self) -> &Arc { + &self.inventory + } + + fn get_index(&self) -> usize { + self.index + } + + fn set_id(&self, id: usize) { + self.id + .store(id as u8, std::sync::atomic::Ordering::Relaxed); + } + + async fn mark_dirty(&self) { + self.inventory.mark_dirty(); + } +} + +// ArmorSlot.java +#[derive(Debug)] +pub struct ArmorSlot { + pub inventory: Arc, + pub index: usize, + pub id: AtomicU8, + pub equipment_slot: EquipmentSlot, +} + +impl ArmorSlot { + pub fn new(inventory: Arc, index: usize, equipment_slot: EquipmentSlot) -> Self { + Self { + inventory, + index, + id: AtomicU8::new(0), + equipment_slot, + } + } +} + +#[async_trait] +impl Slot for ArmorSlot { + fn get_inventory(&self) -> &Arc { + &self.inventory + } + + fn get_index(&self) -> usize { + self.index + } + + fn set_id(&self, id: usize) { + self.id + .store(id as u8, std::sync::atomic::Ordering::Relaxed); + } + + async fn get_max_item_count(&self) -> u8 { + 1 + } + + async fn set_stack_prev(&self, stack: ItemStack, _previous_stack: ItemStack) { + //TODO: this.entity.onEquipStack(this.equipmentSlot, previousStack, stack); + self.set_stack_no_callbacks(stack).await; + } + + async fn can_insert(&self, _stack: &ItemStack) -> bool { + // TODO: return this.entity.canEquip(stack, this.equipmentSlot); + true + } + + async fn can_take_items(&self, _player: &dyn InventoryPlayer) -> bool { + // TODO: Check enchantments + true + } + + async fn mark_dirty(&self) { + self.inventory.mark_dirty(); + } +} diff --git a/pumpkin-inventory/src/sync_handler.rs b/pumpkin-inventory/src/sync_handler.rs new file mode 100644 index 000000000..c4e4e1416 --- /dev/null +++ b/pumpkin-inventory/src/sync_handler.rs @@ -0,0 +1,156 @@ +use std::sync::Arc; + +use pumpkin_protocol::{ + client::play::{ + CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, + }, + codec::{ + item_stack_seralizer::{ItemStackSerializer, OptionalItemStackHash}, + var_int::VarInt, + }, +}; +use pumpkin_world::item::ItemStack; +use tokio::sync::Mutex; + +use crate::screen_handler::{InventoryPlayer, ScreenHandlerBehaviour}; + +pub struct SyncHandler { + player: Mutex>>, +} + +impl Default for SyncHandler { + fn default() -> Self { + Self::new() + } +} + +impl SyncHandler { + pub fn new() -> Self { + Self { + player: Mutex::new(None), + } + } + + pub async fn store_player(&self, player: Arc) { + self.player.lock().await.replace(player); + } + + pub async fn update_state( + &self, + screen_handler: &ScreenHandlerBehaviour, + stacks: &[ItemStack], + cursor_stack: &ItemStack, + properties: Vec, + next_revision: u32, + ) { + if let Some(player) = self.player.lock().await.as_ref() { + player + .enqueue_inventory_packet(&CSetContainerContent::new( + VarInt(screen_handler.sync_id.into()), + VarInt(next_revision as i32), + stacks + .iter() + .map(|stack| ItemStackSerializer::from(*stack)) + .collect::>() + .as_slice(), + &ItemStackSerializer::from(*cursor_stack), + )) + .await; + + for (i, property) in properties.iter().enumerate() { + player + .enqueue_property_packet(&CSetContainerProperty::new( + VarInt(screen_handler.sync_id.into()), + i as i16, + *property as i16, + )) + .await; + } + } + } + + pub async fn update_slot( + &self, + screen_handler: &ScreenHandlerBehaviour, + slot: usize, + stack: &ItemStack, + next_revision: u32, + ) { + if let Some(player) = self.player.lock().await.as_ref() { + player + .enqueue_slot_packet(&CSetContainerSlot::new( + screen_handler.sync_id as i8, + next_revision as i32, + slot as i16, + &ItemStackSerializer::from(*stack), + )) + .await; + } + } + + pub async fn update_cursor_stack( + &self, + _screen_handler: &ScreenHandlerBehaviour, + stack: &ItemStack, + ) { + if let Some(player) = self.player.lock().await.as_ref() { + player + .enqueue_cursor_packet(&CSetCursorItem::new(&ItemStackSerializer::from(*stack))) + .await; + } + } + + pub async fn update_property( + &self, + screen_handler: &ScreenHandlerBehaviour, + property: i32, + value: i32, + ) { + if let Some(player) = self.player.lock().await.as_ref() { + player + .enqueue_property_packet(&CSetContainerProperty::new( + VarInt(screen_handler.sync_id.into()), + property as i16, + value as i16, + )) + .await; + } + } +} + +// TrackedSlot in vanilla +#[derive(Debug, Clone)] +pub struct TrackedStack { + pub received_stack: Option, + pub received_hash: Option, +} + +impl TrackedStack { + pub const EMPTY: TrackedStack = TrackedStack { + received_stack: None, + received_hash: None, + }; + + pub fn set_received_stack(&mut self, stack: ItemStack) { + self.received_stack = Some(stack); + self.received_hash = None; + } + + pub fn set_received_hash(&mut self, hash: OptionalItemStackHash) { + self.received_hash = Some(hash); + self.received_stack = None; + } + + pub fn is_in_sync(&mut self, actual_stack: &ItemStack) -> bool { + if let Some(stack) = &self.received_stack { + return stack.are_equal(actual_stack); + } else if let Some(hash) = &self.received_hash { + if hash.hash_equals(actual_stack) { + self.received_stack = Some(*actual_stack); + return true; + } + } + + false + } +} diff --git a/pumpkin-protocol/src/client/play/close_container.rs b/pumpkin-protocol/src/client/play/close_container.rs index a0c522043..0ddeba515 100644 --- a/pumpkin-protocol/src/client/play/close_container.rs +++ b/pumpkin-protocol/src/client/play/close_container.rs @@ -7,11 +7,11 @@ use crate::VarInt; #[derive(Serialize)] #[packet(PLAY_CONTAINER_CLOSE)] pub struct CCloseContainer { - window_id: VarInt, + sync_id: VarInt, } impl CCloseContainer { pub const fn new(window_id: VarInt) -> Self { - Self { window_id } + Self { sync_id: window_id } } } diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index c2e473274..97415afee 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -59,10 +59,12 @@ mod set_border_warning_distance; mod set_container_content; mod set_container_property; mod set_container_slot; +mod set_cursor_slot; mod set_equipment; mod set_experience; mod set_health; mod set_held_item; +mod set_player_inventory; mod set_time; mod set_title; mod sound_effect; @@ -144,10 +146,12 @@ pub use set_border_warning_distance::*; pub use set_container_content::*; pub use set_container_property::*; pub use set_container_slot::*; +pub use set_cursor_slot::*; pub use set_equipment::*; pub use set_experience::*; pub use set_health::*; pub use set_held_item::*; +pub use set_player_inventory::*; pub use set_time::*; pub use set_title::*; pub use sound_effect::*; diff --git a/pumpkin-protocol/src/client/play/open_screen.rs b/pumpkin-protocol/src/client/play/open_screen.rs index c50fe1710..eb72e2b65 100644 --- a/pumpkin-protocol/src/client/play/open_screen.rs +++ b/pumpkin-protocol/src/client/play/open_screen.rs @@ -9,7 +9,7 @@ use crate::VarInt; #[derive(Serialize)] #[packet(PLAY_OPEN_SCREEN)] pub struct COpenScreen<'a> { - window_id: VarInt, + sync_id: VarInt, window_type: VarInt, window_title: &'a TextComponent, } @@ -17,7 +17,7 @@ pub struct COpenScreen<'a> { impl<'a> COpenScreen<'a> { pub fn new(window_id: VarInt, window_type: VarInt, window_title: &'a TextComponent) -> Self { Self { - window_id, + sync_id: window_id, window_type, window_title, } diff --git a/pumpkin-protocol/src/client/play/set_cursor_slot.rs b/pumpkin-protocol/src/client/play/set_cursor_slot.rs new file mode 100644 index 000000000..87e1b0dce --- /dev/null +++ b/pumpkin-protocol/src/client/play/set_cursor_slot.rs @@ -0,0 +1,17 @@ +use crate::codec::item_stack_seralizer::ItemStackSerializer; + +use pumpkin_data::packet::clientbound::PLAY_SET_CURSOR_ITEM; +use pumpkin_macros::packet; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(PLAY_SET_CURSOR_ITEM)] +pub struct CSetCursorItem<'a> { + stack: &'a ItemStackSerializer<'a>, +} + +impl<'a> CSetCursorItem<'a> { + pub fn new(stack: &'a ItemStackSerializer<'a>) -> Self { + Self { stack } + } +} diff --git a/pumpkin-protocol/src/client/play/set_equipment.rs b/pumpkin-protocol/src/client/play/set_equipment.rs index fec42753f..861e51565 100644 --- a/pumpkin-protocol/src/client/play/set_equipment.rs +++ b/pumpkin-protocol/src/client/play/set_equipment.rs @@ -13,14 +13,11 @@ use crate::{ #[packet(PLAY_SET_EQUIPMENT)] pub struct CSetEquipment { entity_id: VarInt, - equipment: Vec<(EquipmentSlot, ItemStackSerializer<'static>)>, + equipment: Vec<(i8, ItemStackSerializer<'static>)>, } impl CSetEquipment { - pub fn new( - entity_id: VarInt, - equipment: Vec<(EquipmentSlot, ItemStackSerializer<'static>)>, - ) -> Self { + pub fn new(entity_id: VarInt, equipment: Vec<(i8, ItemStackSerializer<'static>)>) -> Self { Self { entity_id, equipment, @@ -39,7 +36,7 @@ impl ClientPacket for CSetEquipment { if i != self.equipment.len() - 1 { write.write_i8_be(-128)?; } else { - write.write_i8_be(*slot as i8)?; + write.write_i8_be(*slot)?; } let mut serializer = Serializer::new(&mut write); equipment @@ -51,14 +48,3 @@ impl ClientPacket for CSetEquipment { Ok(()) } } - -#[derive(Clone, Copy)] -pub enum EquipmentSlot { - MainHand, - OffHand, - Feet, - Legs, - Chest, - Head, - Body, -} diff --git a/pumpkin-protocol/src/client/play/set_held_item.rs b/pumpkin-protocol/src/client/play/set_held_item.rs index 1643d15f8..8f67300f7 100644 --- a/pumpkin-protocol/src/client/play/set_held_item.rs +++ b/pumpkin-protocol/src/client/play/set_held_item.rs @@ -4,11 +4,11 @@ use serde::Serialize; #[derive(Serialize)] #[packet(PLAY_SET_HELD_SLOT)] -pub struct CSetHeldItem { +pub struct CSetSelectedSlot { slot: i8, } -impl CSetHeldItem { +impl CSetSelectedSlot { pub fn new(slot: i8) -> Self { Self { slot } } diff --git a/pumpkin-protocol/src/client/play/set_player_inventory.rs b/pumpkin-protocol/src/client/play/set_player_inventory.rs new file mode 100644 index 000000000..32e00f956 --- /dev/null +++ b/pumpkin-protocol/src/client/play/set_player_inventory.rs @@ -0,0 +1,19 @@ +use crate::VarInt; +use crate::codec::item_stack_seralizer::ItemStackSerializer; + +use pumpkin_data::packet::clientbound::PLAY_SET_PLAYER_INVENTORY; +use pumpkin_macros::packet; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(PLAY_SET_PLAYER_INVENTORY)] +pub struct CSetPlayerInventory<'a> { + slot: VarInt, + item: &'a ItemStackSerializer<'a>, +} + +impl<'a> CSetPlayerInventory<'a> { + pub fn new(slot: VarInt, item: &'a ItemStackSerializer<'a>) -> Self { + Self { slot, item } + } +} diff --git a/pumpkin-protocol/src/codec/item_stack_seralizer.rs b/pumpkin-protocol/src/codec/item_stack_seralizer.rs index 9b1d3972b..0efee09ef 100644 --- a/pumpkin-protocol/src/codec/item_stack_seralizer.rs +++ b/pumpkin-protocol/src/codec/item_stack_seralizer.rs @@ -38,6 +38,7 @@ impl<'de> Deserialize<'de> for ItemStackSerializer<'static> { let item_id = seq .next_element::()? .ok_or(de::Error::custom("No item id VarInt!"))?; + let num_components_to_add = seq .next_element::()? .ok_or(de::Error::custom("No component add length VarInt!"))?; @@ -58,7 +59,7 @@ impl<'de> Deserialize<'de> for ItemStackSerializer<'static> { ItemStackSerializer(Cow::Owned(ItemStack::new( item_count.0 as u8, - Item::from_id(item_id).unwrap_or(Item::AIR), + Item::from_id(item_id).unwrap_or(&Item::AIR), ))) }; @@ -119,3 +120,130 @@ impl From> for ItemStackSerializer<'_> { } } } + +#[derive(Debug, Clone)] +pub struct ItemComponentHash { + pub added: Vec<(VarInt, VarInt)>, + pub removed: Vec, +} + +#[derive(Debug, Clone)] +pub struct ItemStackHash { + item_id: VarInt, + count: VarInt, + #[allow(dead_code)] + components: ItemComponentHash, +} + +impl OptionalItemStackHash { + pub fn hash_equals(&self, other: &ItemStack) -> bool { + if let Some(hash) = &self.0 { + // TODO: Components + hash.item_id == other.item.id.into() && hash.count == other.item_count.into() + } else { + other.is_empty() + } + } +} + +#[derive(Debug, Clone)] +pub struct OptionalItemStackHash(pub Option); + +impl<'de> Deserialize<'de> for OptionalItemStackHash { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = OptionalItemStackHash; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid Slot encoded in a byte sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let is_some = seq + .next_element::()? + .ok_or(de::Error::custom("No is some bool!"))?; + if is_some { + let item_id = seq + .next_element::()? + .ok_or(de::Error::custom("No item id VarInt!"))?; + let count = seq + .next_element::()? + .ok_or(de::Error::custom("No item count VarInt!"))?; + + let hashed_components = seq + .next_element::()? + .ok_or(de::Error::custom("No item component hash!"))?; + + let item_stack_hash = ItemStackHash { + item_id, + count, + components: hashed_components, + }; + Ok(OptionalItemStackHash(Some(item_stack_hash))) + } else { + Ok(OptionalItemStackHash(None)) + } + } + } + + deserializer.deserialize_seq(Visitor) + } +} + +impl<'de> Deserialize<'de> for ItemComponentHash { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = ItemComponentHash; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid Slot encoded in a byte sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut added = Vec::new(); + let mut removed = Vec::new(); + + let added_length = seq + .next_element::()? + .ok_or(de::Error::custom("No added length VarInt!"))?; + for _ in 0..added_length.0 { + let component_id = seq + .next_element::()? + .ok_or(de::Error::custom("No component id VarInt!"))?; + let component_value = seq + .next_element::()? + .ok_or(de::Error::custom("No component value VarInt!"))?; + added.push((component_id, component_value)); + } + + let removed_length = seq + .next_element::()? + .ok_or(de::Error::custom("No removed length VarInt!"))?; + for _ in 0..removed_length.0 { + let component_id = seq + .next_element::()? + .ok_or(de::Error::custom("No component id VarInt!"))?; + removed.push(component_id); + } + + Ok(ItemComponentHash { added, removed }) + } + } + + deserializer.deserialize_seq(Visitor) + } +} diff --git a/pumpkin-protocol/src/server/play/click_container.rs b/pumpkin-protocol/src/server/play/click_container.rs index adb6e3f56..b7fd9853a 100644 --- a/pumpkin-protocol/src/server/play/click_container.rs +++ b/pumpkin-protocol/src/server/play/click_container.rs @@ -1,5 +1,5 @@ use crate::VarInt; -use crate::codec::item_stack_seralizer::ItemStackSerializer; +use crate::codec::item_stack_seralizer::OptionalItemStackHash; use pumpkin_data::packet::serverbound::PLAY_CONTAINER_CLICK; use pumpkin_macros::packet; use serde::de::SeqAccess; @@ -7,25 +7,25 @@ use serde::{Deserialize, de}; #[derive(Debug)] #[packet(PLAY_CONTAINER_CLICK)] -pub struct SClickContainer { - pub window_id: VarInt, - pub state_id: VarInt, +pub struct SClickSlot { + pub sync_id: VarInt, + pub revision: VarInt, pub slot: i16, pub button: i8, pub mode: SlotActionType, pub length_of_array: VarInt, - pub array_of_changed_slots: Vec<(i16, ItemStackSerializer<'static>)>, - pub carried_item: ItemStackSerializer<'static>, + pub array_of_changed_slots: Vec<(i16, OptionalItemStackHash)>, + pub carried_item: OptionalItemStackHash, } -impl<'de> Deserialize<'de> for SClickContainer { +impl<'de> Deserialize<'de> for SClickSlot { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { - type Value = SClickContainer; + type Value = SClickSlot; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a valid VarInt encoded in a byte sequence") @@ -35,10 +35,10 @@ impl<'de> Deserialize<'de> for SClickContainer { where A: SeqAccess<'de>, { - let window_id = seq - .next_element::()? + let sync_id = seq + .next_element::()? .ok_or(de::Error::custom("Failed to decode u8"))?; - let state_id = seq + let revision = seq .next_element::()? .ok_or(de::Error::custom("Failed to decode VarInt"))?; @@ -60,18 +60,18 @@ impl<'de> Deserialize<'de> for SClickContainer { .next_element::()? .ok_or(de::Error::custom("Unable to parse slot"))?; let slot = seq - .next_element::()? + .next_element::()? .ok_or(de::Error::custom("Unable to parse item"))?; array_of_changed_slots.push((slot_number, slot)); } let carried_item = seq - .next_element::()? + .next_element::()? .ok_or(de::Error::custom("Failed to decode carried item"))?; - Ok(SClickContainer { - window_id: window_id.into(), - state_id, + Ok(SClickSlot { + sync_id, + revision, slot, button, mode: SlotActionType::try_from(mode.0) @@ -87,7 +87,7 @@ impl<'de> Deserialize<'de> for SClickContainer { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, PartialEq, Clone)] pub enum SlotActionType { /// Performs a normal slot click. This can pick up or place items in the slot, possibly merging the cursor stack into the slot, or swapping the slot stack with the cursor stack if they can't be merged. Pickup, diff --git a/pumpkin-world/src/block/entities/barrel.rs b/pumpkin-world/src/block/entities/barrel.rs new file mode 100644 index 000000000..39102be7f --- /dev/null +++ b/pumpkin-world/src/block/entities/barrel.rs @@ -0,0 +1,129 @@ +use std::{ + array::from_fn, + sync::{Arc, atomic::AtomicBool}, +}; + +use async_trait::async_trait; +use pumpkin_util::math::position::BlockPos; +use tokio::sync::Mutex; + +use crate::{ + inventory::{ + split_stack, {Clearable, Inventory}, + }, + item::ItemStack, +}; + +use super::BlockEntity; + +#[derive(Debug)] +pub struct BarrelBlockEntity { + pub position: BlockPos, + pub items: [Arc>; 27], + pub dirty: AtomicBool, +} + +#[async_trait] +impl BlockEntity for BarrelBlockEntity { + fn identifier(&self) -> &'static str { + Self::ID + } + + fn get_position(&self) -> BlockPos { + self.position + } + + fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self + where + Self: Sized, + { + let barrel = Self { + position, + items: from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY))), + dirty: AtomicBool::new(false), + }; + + barrel.read_data(nbt, &barrel.items); + + barrel + } + + async fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + self.write_data(nbt, &self.items, true).await; + // Safety precaution + //self.clear().await; + } + + fn get_inventory(self: Arc) -> Option> { + Some(self) + } + + fn is_dirty(&self) -> bool { + self.dirty.load(std::sync::atomic::Ordering::Relaxed) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl BarrelBlockEntity { + pub const ID: &'static str = "minecraft:barrel"; + pub fn new(position: BlockPos) -> Self { + println!("Creating barrel"); + Self { + position, + items: from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY))), + dirty: AtomicBool::new(false), + } + } +} + +#[async_trait] +impl Inventory for BarrelBlockEntity { + fn size(&self) -> usize { + self.items.len() + } + + async fn is_empty(&self) -> bool { + for slot in self.items.iter() { + if !slot.lock().await.is_empty() { + return false; + } + } + + true + } + + async fn get_stack(&self, slot: usize) -> Arc> { + self.items[slot].clone() + } + + async fn remove_stack(&self, slot: usize) -> ItemStack { + let mut removed = ItemStack::EMPTY; + let mut guard = self.items[slot].lock().await; + std::mem::swap(&mut removed, &mut *guard); + removed + } + + async fn remove_stack_specific(&self, slot: usize, amount: u8) -> ItemStack { + split_stack(&self.items, slot, amount).await + } + + async fn set_stack(&self, slot: usize, stack: ItemStack) { + *self.items[slot].lock().await = stack; + } + + fn mark_dirty(&self) { + self.dirty.store(true, std::sync::atomic::Ordering::Relaxed); + } +} + +#[async_trait] +impl Clearable for BarrelBlockEntity { + async fn clear(&self) { + for slot in self.items.iter() { + *slot.lock().await = ItemStack::EMPTY; + } + } +} diff --git a/pumpkin-world/src/block/entities/bed.rs b/pumpkin-world/src/block/entities/bed.rs index b82222cff..cdc83e0fc 100644 --- a/pumpkin-world/src/block/entities/bed.rs +++ b/pumpkin-world/src/block/entities/bed.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use pumpkin_util::math::position::BlockPos; use super::BlockEntity; @@ -10,6 +11,7 @@ impl BedBlockEntity { pub const ID: &'static str = "minecraft:bed"; } +#[async_trait] impl BlockEntity for BedBlockEntity { fn identifier(&self) -> &'static str { Self::ID @@ -26,5 +28,9 @@ impl BlockEntity for BedBlockEntity { Self { position } } - fn write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) {} + async fn write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) {} + + fn as_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin-world/src/block/entities/chest.rs b/pumpkin-world/src/block/entities/chest.rs index b4dc99885..e76ec309c 100644 --- a/pumpkin-world/src/block/entities/chest.rs +++ b/pumpkin-world/src/block/entities/chest.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use pumpkin_util::math::position::BlockPos; use super::BlockEntity; @@ -7,6 +8,7 @@ pub struct ChestBlockEntity { //pub items: [Item; 27], } +#[async_trait] impl BlockEntity for ChestBlockEntity { fn identifier(&self) -> &'static str { Self::ID @@ -23,7 +25,11 @@ impl BlockEntity for ChestBlockEntity { Self { position } } - fn write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) {} + async fn write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) {} + + fn as_any(&self) -> &dyn std::any::Any { + self + } } impl ChestBlockEntity { diff --git a/pumpkin-world/src/block/entities/comparator.rs b/pumpkin-world/src/block/entities/comparator.rs index 4189f570c..ddfc656ad 100644 --- a/pumpkin-world/src/block/entities/comparator.rs +++ b/pumpkin-world/src/block/entities/comparator.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use pumpkin_util::math::position::BlockPos; use super::BlockEntity; @@ -13,6 +14,7 @@ impl ComparatorBlockEntity { const OUTPUT_SIGNAL: &str = "OutputSignal"; +#[async_trait] impl BlockEntity for ComparatorBlockEntity { fn identifier(&self) -> &'static str { Self::ID @@ -33,7 +35,11 @@ impl BlockEntity for ComparatorBlockEntity { } } - fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + async fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { nbt.put_int(OUTPUT_SIGNAL, self.output_signal); } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/pumpkin-world/src/block/entities/mod.rs b/pumpkin-world/src/block/entities/mod.rs index 72de669d5..71fba448e 100644 --- a/pumpkin-world/src/block/entities/mod.rs +++ b/pumpkin-world/src/block/entities/mod.rs @@ -1,5 +1,7 @@ -use std::sync::Arc; +use std::{any::Any, sync::Arc}; +use async_trait::async_trait; +use barrel::BarrelBlockEntity; use bed::BedBlockEntity; use chest::ChestBlockEntity; use comparator::ComparatorBlockEntity; @@ -7,25 +9,30 @@ use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; use sign::SignBlockEntity; +use crate::inventory::Inventory; + +pub mod barrel; pub mod bed; pub mod chest; pub mod comparator; pub mod sign; +//TODO: We need a mark_dirty for chests +#[async_trait] pub trait BlockEntity: Send + Sync { - fn write_nbt(&self, nbt: &mut NbtCompound); + async fn write_nbt(&self, nbt: &mut NbtCompound); fn from_nbt(nbt: &NbtCompound, position: BlockPos) -> Self where Self: Sized; fn identifier(&self) -> &'static str; fn get_position(&self) -> BlockPos; - fn write_internal(&self, nbt: &mut NbtCompound) { + async fn write_internal(&self, nbt: &mut NbtCompound) { nbt.put_string("id", self.identifier().to_string()); let position = self.get_position(); nbt.put_int("x", position.0.x); nbt.put_int("y", position.0.y); nbt.put_int("z", position.0.z); - self.write_nbt(nbt); + self.write_nbt(nbt).await; } fn get_id(&self) -> u32 { pumpkin_data::block_properties::BLOCK_ENTITY_TYPES @@ -38,6 +45,13 @@ pub trait BlockEntity: Send + Sync { fn chunk_data_nbt(&self) -> Option { None } + fn get_inventory(self: Arc) -> Option> { + None + } + fn is_dirty(&self) -> bool { + false + } + fn as_any(&self) -> &dyn Any; } pub fn block_entity_from_generic(nbt: &NbtCompound) -> T { @@ -56,6 +70,9 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option> ComparatorBlockEntity::ID => Some(Arc::new(block_entity_from_generic::< ComparatorBlockEntity, >(nbt))), + BarrelBlockEntity::ID => Some(Arc::new(block_entity_from_generic::( + nbt, + ))), _ => None, } } diff --git a/pumpkin-world/src/block/entities/sign.rs b/pumpkin-world/src/block/entities/sign.rs index af6a880b1..015c00d92 100644 --- a/pumpkin-world/src/block/entities/sign.rs +++ b/pumpkin-world/src/block/entities/sign.rs @@ -1,4 +1,5 @@ use super::BlockEntity; +use async_trait::async_trait; use num_derive::FromPrimitive; use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; use pumpkin_util::math::position::BlockPos; @@ -141,6 +142,7 @@ impl Text { } } +#[async_trait] impl BlockEntity for SignBlockEntity { fn identifier(&self) -> &'static str { Self::ID @@ -165,7 +167,7 @@ impl BlockEntity for SignBlockEntity { } } - fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + async fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { nbt.put("front_text", self.front_text.clone()); nbt.put("back_text", self.back_text.clone()); nbt.put_bool("is_waxed", self.is_waxed); @@ -173,9 +175,15 @@ impl BlockEntity for SignBlockEntity { fn chunk_data_nbt(&self) -> Option { let mut nbt = NbtCompound::new(); - self.write_nbt(&mut nbt); + nbt.put("front_text", self.front_text.clone()); + nbt.put("back_text", self.back_text.clone()); + nbt.put_bool("is_waxed", self.is_waxed); Some(nbt) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } impl SignBlockEntity { diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 8166c88f1..27dc972e2 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use bytes::*; use flate2::read::{GzDecoder, GzEncoder, ZlibDecoder, ZlibEncoder}; +use futures::future::join_all; use itertools::Itertools; use pumpkin_config::advanced_config; use pumpkin_data::{Block, chunk::ChunkStatus}; @@ -316,11 +317,12 @@ impl AnvilChunkData { Ok(chunk) } - fn from_chunk( + async fn from_chunk( chunk: &ChunkData, compression: Option, ) -> Result { let raw_bytes = chunk_to_bytes(chunk) + .await .map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))?; let compression = compression @@ -622,7 +624,7 @@ impl ChunkSerializer for AnvilChunkFile { let compression_type = self.chunks_data[index] .as_ref() .and_then(|chunk_data| chunk_data.serialized_data.compression); - let new_chunk_data = AnvilChunkData::from_chunk(chunk, compression_type)?; + let new_chunk_data = AnvilChunkData::from_chunk(chunk, compression_type).await?; let mut write_action = self.write_action.lock().await; if !advanced_config().chunk.write_in_place { @@ -816,7 +818,7 @@ impl ChunkSerializer for AnvilChunkFile { } } -pub fn chunk_to_bytes(chunk_data: &ChunkData) -> Result, ChunkSerializingError> { +pub async fn chunk_to_bytes(chunk_data: &ChunkData) -> Result, ChunkSerializingError> { let sections: Vec<_> = (0..chunk_data.section.sections.len() + 2) .map(|i| { let has_blocks = i >= 1 && i - 1 < chunk_data.section.sections.len(); @@ -887,15 +889,14 @@ pub fn chunk_to_bytes(chunk_data: &ChunkData) -> Result, ChunkSerializin }) .collect() }, - block_entities: chunk_data - .block_entities - .values() - .map(|block_entity| { + block_entities: join_all(chunk_data.block_entities.values().map( + |block_entity| async move { let mut nbt = NbtCompound::new(); - block_entity.write_internal(&mut nbt); + block_entity.write_internal(&mut nbt).await; nbt - }) - .collect(), + }, + )) + .await, // we have not implemented light engine light_correct: false, }; diff --git a/pumpkin-world/src/chunk/format/linear.rs b/pumpkin-world/src/chunk/format/linear.rs index 76cdcc513..7b91467ad 100644 --- a/pumpkin-world/src/chunk/format/linear.rs +++ b/pumpkin-world/src/chunk/format/linear.rs @@ -318,6 +318,7 @@ impl ChunkSerializer for LinearFile { async fn update_chunk(&mut self, chunk: &ChunkData) -> Result<(), ChunkWritingError> { let index = LinearFile::get_chunk_index(&chunk.position); let chunk_raw: Bytes = chunk_to_bytes(chunk) + .await .map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))? .into(); diff --git a/pumpkin-world/src/chunk/io/chunk_file_manager.rs b/pumpkin-world/src/chunk/io/chunk_file_manager.rs index 1b6b697f3..b4e3872c5 100644 --- a/pumpkin-world/src/chunk/io/chunk_file_manager.rs +++ b/pumpkin-world/src/chunk/io/chunk_file_manager.rs @@ -297,7 +297,7 @@ where let mut serializer = chunk_serializer.write().await; for chunk_lock in chunk_locks { let mut chunk = chunk_lock.write().await; - let chunk_is_dirty = chunk.dirty; + let chunk_is_dirty = chunk.dirty || chunk.block_entities.values().any(|block_entity| block_entity.is_dirty()); // Edge case: this chunk is loaded while we were saving, mark it as cleaned since we are // updating what we will write here chunk.dirty = false; diff --git a/pumpkin-world/src/inventory/inventory.rs b/pumpkin-world/src/inventory/inventory.rs new file mode 100644 index 000000000..73f5b410e --- /dev/null +++ b/pumpkin-world/src/inventory/inventory.rs @@ -0,0 +1,162 @@ +use std::{ + fmt::Debug, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use crate::item::ItemStack; +use async_trait::async_trait; +use pumpkin_data::item::Item; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use tokio::sync::{Mutex, OwnedMutexGuard}; + +// Inventory.java +#[async_trait] +pub trait Inventory: Send + Sync + Debug + Clearable { + fn size(&self) -> usize; + + async fn is_empty(&self) -> bool; + + async fn get_stack(&self, slot: usize) -> Arc>; + + async fn remove_stack(&self, slot: usize) -> ItemStack; + + async fn remove_stack_specific(&self, slot: usize, amount: u8) -> ItemStack; + + fn get_max_count_per_stack(&self) -> u8 { + 99 + } + + async fn set_stack(&self, slot: usize, stack: ItemStack); + + fn mark_dirty(&self) {} + + async fn write_data( + &self, + nbt: &mut pumpkin_nbt::compound::NbtCompound, + stacks: &[Arc>], + include_empty: bool, + ) { + let mut slots = Vec::new(); + + for (i, item) in stacks.iter().enumerate() { + let stack = item.lock().await; + if !stack.is_empty() { + let mut item_compound = NbtCompound::new(); + item_compound.put_byte("Slot", i as i8); + stack.write_item_stack(&mut item_compound); + slots.push(NbtTag::Compound(item_compound)); + } + } + + if !include_empty && slots.is_empty() { + return; + } + + nbt.put("Items", NbtTag::List(slots.into_boxed_slice())); + } + + fn read_data( + &self, + nbt: &pumpkin_nbt::compound::NbtCompound, + stacks: &[Arc>], + ) { + if let Some(inventory_list) = nbt.get_list("Items") { + for tag in inventory_list { + if let Some(item_compound) = tag.extract_compound() { + if let Some(slot_byte) = item_compound.get_byte("Slot") { + let slot = slot_byte as usize; + if slot < stacks.len() { + if let Some(item_stack) = ItemStack::read_item_stack(item_compound) { + // This won't error cause it's only called on initialization + *stacks[slot].try_lock().unwrap() = item_stack; + } + } + } + } + } + } + } + + /* + boolean canPlayerUse(PlayerEntity player); + + default void onOpen(PlayerEntity player) { + } + + default void onClose(PlayerEntity player) { + } + */ + + /// isValid is source + fn is_valid_slot_for(&self, _slot: usize, _stack: &ItemStack) -> bool { + true + } + + fn can_transfer_to( + &self, + _hopper_inventory: &dyn Inventory, + _slot: usize, + _stack: &ItemStack, + ) -> bool { + true + } + + async fn count(&self, item: &Item) -> u8 { + let mut count = 0; + + for i in 0..self.size() { + let slot = self.get_stack(i).await; + let stack = slot.lock().await; + if stack.get_item().id == item.id { + count += stack.item_count; + } + } + + count + } + + async fn contains_any_predicate( + &self, + predicate: &(dyn Fn(OwnedMutexGuard) -> bool + Sync), + ) -> bool { + for i in 0..self.size() { + let slot = self.get_stack(i).await; + let stack = slot.lock_owned().await; + if predicate(stack) { + return true; + } + } + + false + } + + async fn contains_any(&self, items: &[Item]) -> bool { + self.contains_any_predicate(&|stack| !stack.is_empty() && items.contains(stack.get_item())) + .await + } + + // TODO: canPlayerUse +} + +#[async_trait] +pub trait Clearable { + async fn clear(&self); +} + +pub struct ComparableInventory(pub Arc); + +impl PartialEq for ComparableInventory { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for ComparableInventory {} + +impl Hash for ComparableInventory { + fn hash(&self, state: &mut H) { + let ptr = Arc::as_ptr(&self.0); + ptr.hash(state); + } +} diff --git a/pumpkin-world/src/inventory/mod.rs b/pumpkin-world/src/inventory/mod.rs new file mode 100644 index 000000000..4a833c691 --- /dev/null +++ b/pumpkin-world/src/inventory/mod.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use tokio::sync::Mutex; + +use crate::item::ItemStack; + +#[allow(clippy::module_inception)] +mod inventory; + +pub use inventory::*; + +// These are some utility functions found in Inventories.java +pub async fn split_stack(stacks: &[Arc>], slot: usize, amount: u8) -> ItemStack { + let mut stack = stacks[slot].lock().await; + if slot < stacks.len() && !stack.is_empty() && amount > 0 { + stack.split(amount) + } else { + ItemStack::EMPTY + } +} diff --git a/pumpkin-world/src/item/mod.rs b/pumpkin-world/src/item/mod.rs index 010c2a948..b76812333 100644 --- a/pumpkin-world/src/item/mod.rs +++ b/pumpkin-world/src/item/mod.rs @@ -1,6 +1,7 @@ use pumpkin_data::item::Item; use pumpkin_data::tag::{RegistryKey, get_tag_values}; use pumpkin_nbt::compound::NbtCompound; +use std::hash::Hash; mod categories; @@ -14,26 +15,33 @@ pub enum Rarity { Epic, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Copy)] pub struct ItemStack { pub item_count: u8, - // TODO: Should this be a ref? all of our items are const - pub item: Item, + pub item: &'static Item, } +impl Hash for ItemStack { + fn hash(&self, state: &mut H) { + self.item_count.hash(state); + self.item.id.hash(state); + } +} + +/* impl PartialEq for ItemStack { fn eq(&self, other: &Self) -> bool { self.item.id == other.item.id } -} +} */ impl ItemStack { pub const EMPTY: ItemStack = ItemStack { item_count: 0, - item: Item::AIR, + item: &Item::AIR, }; - pub fn new(item_count: u8, item: Item) -> Self { + pub fn new(item_count: u8, item: &'static Item) -> Self { Self { item_count, item } } @@ -45,10 +53,14 @@ impl ItemStack { if self.is_empty() { &Item::AIR } else { - &self.item + self.item } } + pub fn is_stackable(&self) -> bool { + self.get_max_stack_size() > 1 // TODO: && (!this.isDamageable() || !this.isDamaged()); + } + pub fn is_empty(&self) -> bool { self.item_count == 0 || self.item.id == Item::AIR.id } @@ -61,11 +73,15 @@ impl ItemStack { } pub fn copy_with_count(&self, count: u8) -> Self { - let mut stack = self.clone(); + let mut stack = *self; stack.item_count = count; stack } + pub fn set_count(&mut self, count: u8) { + self.item_count = count; + } + pub fn decrement(&mut self, amount: u8) { self.item_count = self.item_count.saturating_sub(amount); } @@ -78,6 +94,10 @@ impl ItemStack { self.item == other.item //TODO: && self.item.components == other.item.components } + pub fn are_equal(&self, other: &Self) -> bool { + self.item_count == other.item_count && self.are_items_and_components_equal(other) + } + /// Determines the mining speed for a block based on tool rules. /// Direct matches return immediately, tagged blocks are checked separately. /// If no match is found, returns the tool's default mining speed or `1.0`. diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 546270cf9..46cb932b5 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -9,6 +9,7 @@ pub mod data; pub mod dimension; pub mod entity; mod generation; +pub mod inventory; pub mod item; pub mod level; mod lock; diff --git a/pumpkin/src/block/blocks/barrel.rs b/pumpkin/src/block/blocks/barrel.rs new file mode 100644 index 000000000..2f77651a2 --- /dev/null +++ b/pumpkin/src/block/blocks/barrel.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use pumpkin_data::Block; +use pumpkin_data::item::Item; +use pumpkin_inventory::generic_container_screen_handler::create_generic_9x3; +use pumpkin_inventory::player::player_inventory::PlayerInventory; +use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerFactory}; +use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::text::TextComponent; +use pumpkin_world::BlockStateId; +use pumpkin_world::block::entities::barrel::BarrelBlockEntity; +use pumpkin_world::inventory::Inventory; +use tokio::sync::Mutex; + +use crate::world::World; +use crate::{ + block::{pumpkin_block::PumpkinBlock, registry::BlockActionResult}, + entity::player::Player, + server::Server, +}; + +struct BarrelScreenFactory(Arc); + +impl ScreenHandlerFactory for BarrelScreenFactory { + fn create_screen_handler( + &self, + sync_id: u8, + player_inventory: &Arc, + _player: &dyn InventoryPlayer, + ) -> Option>> { + #[allow(clippy::option_if_let_else)] + Some(Arc::new(Mutex::new(create_generic_9x3( + sync_id, + player_inventory, + self.0.clone(), + )))) + } + + fn get_display_name(&self) -> TextComponent { + TextComponent::translate("container.barrel", vec![]) + } +} + +#[pumpkin_block("minecraft:barrel")] +pub struct BarrelBlock; + +#[async_trait] +impl PumpkinBlock for BarrelBlock { + async fn normal_use( + &self, + _block: &Block, + player: &Player, + location: BlockPos, + _server: &Server, + world: &Arc, + ) { + if let Some(block_entity) = world.get_block_entity(&location).await { + if let Some(inventory) = block_entity.get_inventory() { + player + .open_handled_screen(&BarrelScreenFactory(inventory)) + .await; + } + } + } + + async fn use_with_item( + &self, + _block: &Block, + player: &Player, + location: BlockPos, + _item: &Item, + _server: &Server, + world: &Arc, + ) -> BlockActionResult { + if let Some(block_entity) = world.get_block_entity(&location).await { + if let Some(inventory) = block_entity.get_inventory() { + player + .open_handled_screen(&BarrelScreenFactory(inventory)) + .await; + } + } + BlockActionResult::Consume + } + + async fn placed( + &self, + world: &Arc, + _block: &Block, + _state_id: BlockStateId, + pos: &BlockPos, + _old_state_id: BlockStateId, + _notify: bool, + ) { + let barrel_block_entity = BarrelBlockEntity::new(*pos); + world.add_block_entity(Arc::new(barrel_block_entity)).await; + } + + async fn on_state_replaced( + &self, + world: &Arc, + _block: &Block, + location: BlockPos, + _old_state_id: BlockStateId, + _moved: bool, + ) { + world.remove_block_entity(&location).await; + } +} diff --git a/pumpkin/src/block/blocks/chest.rs b/pumpkin/src/block/blocks/chest.rs index ae8b5c8b6..d32fc289c 100644 --- a/pumpkin/src/block/blocks/chest.rs +++ b/pumpkin/src/block/blocks/chest.rs @@ -6,15 +6,9 @@ use pumpkin_data::block_properties::{ }; use pumpkin_data::entity::EntityPose; use pumpkin_data::item::Item; -use pumpkin_data::{Block, BlockState, block_properties::get_block}; -use pumpkin_data::{ - screen::WindowType, - sound::{Sound, SoundCategory}, -}; -use pumpkin_inventory::{ChestContainer, OpenContainer}; +use pumpkin_data::{Block, BlockState}; use pumpkin_macros::pumpkin_block; use pumpkin_protocol::server::play::SUseItemOn; -use pumpkin_protocol::{client::play::CBlockAction, codec::var_int::VarInt}; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; use pumpkin_world::block::BlockDirection; @@ -29,12 +23,6 @@ use crate::{ server::Server, }; -#[derive(PartialEq)] -pub enum ChestState { - IsOpened, - IsClosed, -} - #[pumpkin_block("minecraft:chest")] pub struct ChestBlock; @@ -116,29 +104,25 @@ impl PumpkinBlock for ChestBlock { async fn use_with_item( &self, - block: &Block, - player: &Player, + _block: &Block, + _player: &Player, _location: BlockPos, _item: &Item, - server: &Server, + _server: &Server, _world: &Arc, ) -> BlockActionResult { - self.open_chest_block(block, player, _location, server) - .await; BlockActionResult::Consume } async fn broken( &self, block: &Block, - player: &Arc, + _player: &Arc, block_pos: BlockPos, - server: &Server, + _server: &Server, world: Arc, state: BlockState, ) { - super::standard_on_broken_with_container(block, player, block_pos, server).await; - let chest_props = ChestLikeProperties::from_state_id(state.id, block); let connected_towards = match chest_props.r#type { ChestType::Single => return, @@ -167,95 +151,6 @@ impl PumpkinBlock for ChestBlock { .await; } } - - async fn normal_use( - &self, - block: &Block, - player: &Player, - _location: BlockPos, - server: &Server, - _world: &Arc, - ) { - self.open_chest_block(block, player, _location, server) - .await; - } - - async fn close( - &self, - _block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - container: &mut OpenContainer, - ) { - container.remove_player(player.entity_id()); - - self.play_chest_action(container, player, location, server, ChestState::IsClosed) - .await; - } -} - -impl ChestBlock { - pub async fn open_chest_block( - &self, - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - ) { - // TODO: shouldn't Chest and window type be constrained together to avoid errors? - super::standard_open_container::( - block, - player, - location, - server, - WindowType::Generic9x3, - ) - .await; - - if let Some(container_id) = server.get_container_id(location, block.clone()).await { - let open_containers = server.open_containers.read().await; - if let Some(container) = open_containers.get(&u64::from(container_id)) { - self.play_chest_action(container, player, location, server, ChestState::IsOpened) - .await; - } - } - } - - pub async fn play_chest_action( - &self, - container: &OpenContainer, - player: &Player, - location: BlockPos, - server: &Server, - state: ChestState, - ) { - let num_players = container.get_number_of_players() as u8; - if state == ChestState::IsClosed && num_players == 0 { - player - .world() - .await - .play_block_sound(Sound::BlockChestClose, SoundCategory::Blocks, location) - .await; - } else if state == ChestState::IsOpened && num_players == 1 { - player - .world() - .await - .play_block_sound(Sound::BlockChestOpen, SoundCategory::Blocks, location) - .await; - } - - if let Some(e) = get_block("minecraft:chest") { - server - .broadcast_packet_all(&CBlockAction::new( - location, - 1, - num_players, - VarInt(e.id.into()), - )) - .await; - } - } } async fn compute_chest_props( diff --git a/pumpkin/src/block/blocks/crafting_table.rs b/pumpkin/src/block/blocks/crafting_table.rs index 78fa5a87c..5ebc888ee 100644 --- a/pumpkin/src/block/blocks/crafting_table.rs +++ b/pumpkin/src/block/blocks/crafting_table.rs @@ -1,97 +1,9 @@ -use std::sync::Arc; - -use crate::block::registry::BlockActionResult; -use crate::entity::player::Player; -use crate::server::Server; -use crate::{block::pumpkin_block::PumpkinBlock, world::World}; +use crate::block::pumpkin_block::PumpkinBlock; use async_trait::async_trait; -use pumpkin_data::item::Item; -use pumpkin_data::screen::WindowType; -use pumpkin_data::{Block, BlockState}; -use pumpkin_inventory::{CraftingTable, OpenContainer}; use pumpkin_macros::pumpkin_block; -use pumpkin_util::math::position::BlockPos; #[pumpkin_block("minecraft:crafting_table")] pub struct CraftingTableBlock; #[async_trait] -impl PumpkinBlock for CraftingTableBlock { - async fn normal_use( - &self, - block: &Block, - player: &Player, - _location: BlockPos, - server: &Server, - _world: &Arc, - ) { - self.open_crafting_screen(block, player, _location, server) - .await; - } - - async fn use_with_item( - &self, - block: &Block, - player: &Player, - _location: BlockPos, - _item: &Item, - server: &Server, - _world: &Arc, - ) -> BlockActionResult { - self.open_crafting_screen(block, player, _location, server) - .await; - BlockActionResult::Consume - } - - async fn broken( - &self, - block: &Block, - player: &Arc, - location: BlockPos, - server: &Server, - _world: Arc, - _state: BlockState, - ) { - super::standard_on_broken_with_container(block, player, location, server).await; - } - async fn close( - &self, - _block: &Block, - player: &Player, - _location: BlockPos, - _server: &Server, - container: &mut OpenContainer, - ) { - let entity_id = player.entity_id(); - for player_id in container.all_player_ids() { - if entity_id == player_id { - container.clear_all_slots().await; - } - } - - container.remove_player(entity_id); - - // TODO: items should be re-added to player inventory or dropped depending on if they are in movement. - // TODO: unique containers should be implemented as a separate stack internally (optimizes large player servers for example) - // TODO: ephemeral containers (crafting tables) might need to be a separate data structure than stored (ender chest) - } -} - -impl CraftingTableBlock { - pub async fn open_crafting_screen( - &self, - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - ) { - super::standard_open_container_unique::( - block, - player, - location, - server, - WindowType::Crafting, - ) - .await; - } -} +impl PumpkinBlock for CraftingTableBlock {} diff --git a/pumpkin/src/block/blocks/furnace.rs b/pumpkin/src/block/blocks/furnace.rs index cbaf23ea4..5f0c22cbb 100644 --- a/pumpkin/src/block/blocks/furnace.rs +++ b/pumpkin/src/block/blocks/furnace.rs @@ -1,76 +1,10 @@ -use std::sync::Arc; - -use crate::entity::player::Player; -use crate::{block::registry::BlockActionResult, world::World}; use async_trait::async_trait; -use pumpkin_data::item::Item; -use pumpkin_data::screen::WindowType; -use pumpkin_data::{Block, BlockState}; -use pumpkin_inventory::Furnace; use pumpkin_macros::pumpkin_block; -use pumpkin_util::math::position::BlockPos; -use crate::{block::pumpkin_block::PumpkinBlock, server::Server}; +use crate::block::pumpkin_block::PumpkinBlock; #[pumpkin_block("minecraft:furnace")] pub struct FurnaceBlock; #[async_trait] -impl PumpkinBlock for FurnaceBlock { - async fn normal_use( - &self, - block: &Block, - player: &Player, - _location: BlockPos, - server: &Server, - _world: &Arc, - ) { - self.open_furnace_screen(block, player, _location, server) - .await; - } - - async fn use_with_item( - &self, - block: &Block, - player: &Player, - _location: BlockPos, - _item: &Item, - server: &Server, - _world: &Arc, - ) -> BlockActionResult { - self.open_furnace_screen(block, player, _location, server) - .await; - BlockActionResult::Consume - } - - async fn broken( - &self, - block: &Block, - player: &Arc, - location: BlockPos, - server: &Server, - _world: Arc, - _state: BlockState, - ) { - super::standard_on_broken_with_container(block, player, location, server).await; - } -} - -impl FurnaceBlock { - pub async fn open_furnace_screen( - &self, - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - ) { - super::standard_open_container::( - block, - player, - location, - server, - WindowType::Furnace, - ) - .await; - } -} +impl PumpkinBlock for FurnaceBlock {} diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index d3d2ef191..470dbaa74 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -1,10 +1,4 @@ -use pumpkin_data::Block; -use pumpkin_data::screen::WindowType; -use pumpkin_inventory::{Container, OpenContainer}; -use pumpkin_util::math::position::BlockPos; - -use crate::{entity::player::Player, server::Server}; - +pub mod barrel; pub mod bed; pub mod cactus; pub mod chest; @@ -29,111 +23,3 @@ pub mod sugar_cane; pub mod tnt; pub mod torches; pub mod walls; - -/// The standard destroy with container removes the player forcibly from the container, -/// drops items to the floor, and back to the player's inventory if the item stack is in movement. -pub async fn standard_on_broken_with_container( - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, -) { - // TODO: drop all items and back to players inventory if in motion - if let Some(all_container_ids) = server.get_all_container_ids(location, block.clone()).await { - let mut open_containers = server.open_containers.write().await; - for individual_id in all_container_ids { - if let Some(container) = open_containers.get_mut(&u64::from(individual_id)) { - container.clear_all_slots().await; - player.open_container.store(None); - close_all_in_container(player, container).await; - container.clear_all_players(); - } - } - } -} - -/// The standard open container creates a new container if a container of the same block -/// type does not exist at the selected block location. If a container of the same type exists, the player -/// is added to the currently connected players to that container. -pub async fn standard_open_container( - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - window_type: WindowType, -) { - let entity_id = player.entity_id(); - // If container exists, add player to container, otherwise create new container - if let Some(container_id) = server.get_container_id(location, block.clone()).await { - let mut open_containers = server.open_containers.write().await; - log::debug!("Using previous standard container id: {container_id}"); - if let Some(container) = open_containers.get_mut(&u64::from(container_id)) { - container.add_player(entity_id); - player.open_container.store(Some(container_id.into())); - } - } else { - let mut open_containers = server.open_containers.write().await; - let new_id = server.new_container_id(); - log::debug!("Creating new standard container id: {new_id}"); - let open_container = - OpenContainer::new_empty_container::(entity_id, Some(location), Some(block.clone())); - open_containers.insert(new_id.into(), open_container); - player.open_container.store(Some(new_id.into())); - } - player.open_container(server, window_type).await; -} - -pub async fn standard_open_container_unique( - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - window_type: WindowType, -) { - { - let entity_id = player.entity_id(); - let mut open_containers = server.open_containers.write().await; - let mut id_to_use = -1; - - // TODO: we can do better than brute force - for (id, container) in open_containers.iter() { - if let Some(a_block) = container.get_block() { - if a_block.id == block.id && container.all_player_ids().is_empty() { - id_to_use = *id as i64; - } - } - } - - if id_to_use == -1 { - let new_id = server.new_container_id(); - log::debug!("Creating new unique container id: {new_id}"); - let open_container = OpenContainer::new_empty_container::( - entity_id, - Some(location), - Some(block.clone()), - ); - - open_containers.insert(new_id.into(), open_container); - - player.open_container.store(Some(new_id.into())); - } else { - log::debug!("Using previous unique container id: {id_to_use}"); - if let Some(unique_container) = open_containers.get_mut(&(id_to_use as u64)) { - unique_container.set_location(Some(location)).await; - unique_container.add_player(entity_id); - player - .open_container - .store(Some(id_to_use.try_into().unwrap())); - } - } - } - player.open_container(server, window_type).await; -} - -pub async fn close_all_in_container(player: &Player, container: &OpenContainer) { - for id in container.all_player_ids() { - if let Some(remote_player) = player.world().await.get_player_by_id(id).await { - remote_player.close_container().await; - } - } -} diff --git a/pumpkin/src/block/loot.rs b/pumpkin/src/block/loot.rs index 37e59a8f5..38d735d3a 100644 --- a/pumpkin/src/block/loot.rs +++ b/pumpkin/src/block/loot.rs @@ -85,16 +85,16 @@ impl LootPoolEntryExt for LootPoolEntry { enchantment: _, formula: _, parameters: _, - } => todo!(), - LootFunctionTypes::CopyComponents { + } + | LootFunctionTypes::CopyComponents { source: _, include: _, - } => todo!(), - LootFunctionTypes::CopyState { + } + | LootFunctionTypes::CopyState { block: _, properties: _, - } => todo!(), - LootFunctionTypes::ExplosionDecay => { + } + | LootFunctionTypes::ExplosionDecay => { // TODO: shouldnt crash here but needs to be implemented someday } } diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index d4ac147b1..0e04e3ffa 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -1,3 +1,4 @@ +use blocks::barrel::BarrelBlock; use blocks::bed::BedBlock; use blocks::cactus::CactusBlock; use blocks::dirt_path::DirtPathBlock; @@ -103,6 +104,7 @@ pub fn default_registry() -> Arc { manager.register(RedstoneWireBlock); manager.register(RepeaterBlock); manager.register(TargetBlock); + manager.register(BarrelBlock); // Rails manager.register(RailBlock); @@ -152,8 +154,7 @@ async fn drop_stack(world: &Arc, pos: &BlockPos, stack: ItemStack) { ); let entity = world.create_entity(pos, EntityType::ITEM); - let item_entity = - Arc::new(ItemEntity::new(entity, stack.item.id, u32::from(stack.item_count)).await); + let item_entity = Arc::new(ItemEntity::new(entity, stack).await); world.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; } diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs index d514a2d8b..61323ca2c 100644 --- a/pumpkin/src/block/pumpkin_block.rs +++ b/pumpkin/src/block/pumpkin_block.rs @@ -5,7 +5,6 @@ use crate::world::{BlockFlags, World}; use async_trait::async_trait; use pumpkin_data::item::Item; use pumpkin_data::{Block, BlockState}; -use pumpkin_inventory::OpenContainer; use pumpkin_protocol::server::play::SUseItemOn; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; @@ -133,16 +132,6 @@ pub trait PumpkinBlock: Send + Sync { ) { } - async fn close( - &self, - _block: &Block, - _player: &Player, - _location: BlockPos, - _server: &Server, - _container: &mut OpenContainer, - ) { - } - async fn on_neighbor_update( &self, _world: &Arc, diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index 5f42a1964..11dfe63b6 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -5,7 +5,6 @@ use crate::world::{BlockFlags, World}; use pumpkin_data::fluid::Fluid; use pumpkin_data::item::Item; use pumpkin_data::{Block, BlockState}; -use pumpkin_inventory::OpenContainer; use pumpkin_protocol::server::play::SUseItemOn; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; @@ -233,22 +232,6 @@ impl BlockRegistry { } } - pub async fn close( - &self, - block: &Block, - player: &Player, - location: BlockPos, - server: &Server, - container: &mut OpenContainer, - ) { - let pumpkin_block = self.get_pumpkin_block(block); - if let Some(pumpkin_block) = pumpkin_block { - pumpkin_block - .close(block, player, location, server, container) - .await; - } - } - pub async fn on_state_replaced( &self, world: &Arc, diff --git a/pumpkin/src/command/args/resource/item.rs b/pumpkin/src/command/args/resource/item.rs index ee04fc27b..aa9d00995 100644 --- a/pumpkin/src/command/args/resource/item.rs +++ b/pumpkin/src/command/args/resource/item.rs @@ -54,7 +54,7 @@ impl DefaultNameArgConsumer for ItemArgumentConsumer { } impl<'a> FindArg<'a> for ItemArgumentConsumer { - type Data = (&'a str, Item); + type Data = (&'a str, &'static Item); fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { match args.get(name) { diff --git a/pumpkin/src/command/commands/clear.rs b/pumpkin/src/command/commands/clear.rs index 4a0ee7cfa..848394267 100644 --- a/pumpkin/src/command/commands/clear.rs +++ b/pumpkin/src/command/commands/clear.rs @@ -1,11 +1,11 @@ use std::sync::Arc; use async_trait::async_trait; -use pumpkin_inventory::Container; use pumpkin_util::text::TextComponent; use pumpkin_util::text::click::ClickEvent; use pumpkin_util::text::color::NamedColor; use pumpkin_util::text::hover::HoverEvent; +use pumpkin_world::inventory::Clearable; use crate::command::args::entities::EntitiesArgumentConsumer; use crate::command::args::{Arg, ConsumedArgs}; @@ -21,19 +21,11 @@ const DESCRIPTION: &str = "Clear yours or targets inventory."; const ARG_TARGET: &str = "target"; async fn clear_player(target: &Player) -> usize { - let mut inventory = target.inventory().lock().await; + let inventory = target.inventory(); - let slots = inventory.all_slots(); - let items_count = slots - .iter() - .filter_map(|slot| slot.as_ref().map(|slot| slot.item_count as usize)) - .sum(); - for slot in slots { - *slot = None; - } - drop(inventory); - target.set_container_content(None).await; - items_count + inventory.clear().await; + //target.set_container_content(None).await; TODO: Inv + 0 //TODO: Count items } fn clear_command_text_output(item_count: usize, targets: &[Arc]) -> TextComponent { diff --git a/pumpkin/src/command/commands/give.rs b/pumpkin/src/command/commands/give.rs index a20efb789..59f8ed7ff 100644 --- a/pumpkin/src/command/commands/give.rs +++ b/pumpkin/src/command/commands/give.rs @@ -3,6 +3,7 @@ use pumpkin_util::text::TextComponent; use pumpkin_util::text::click::ClickEvent; use pumpkin_util::text::color::{Color, NamedColor}; use pumpkin_util::text::hover::HoverEvent; +use pumpkin_world::item::ItemStack; use crate::command::args::bounded_num::{BoundedNumArgumentConsumer, NotInBounds}; use crate::command::args::players::PlayersArgumentConsumer; @@ -60,7 +61,11 @@ impl CommandExecutor for Executor { }; for target in targets { - target.give_items(item.clone(), item_count as u32).await; + let mut stack = ItemStack::new(item_count as u8, item); + target.inventory().insert_stack_anywhere(&mut stack).await; + if stack.is_empty() { + target.drop_item(stack).await; + } } let msg = if targets.len() == 1 { TextComponent::translate( diff --git a/pumpkin/src/data/player_server_data.rs b/pumpkin/src/data/player_server_data.rs index 5b7e7a7f8..b543af52a 100644 --- a/pumpkin/src/data/player_server_data.rs +++ b/pumpkin/src/data/player_server_data.rs @@ -3,6 +3,7 @@ use crate::{ server::Server, }; use crossbeam::atomic::AtomicCell; +use pumpkin_inventory::screen_handler::ScreenHandler; use pumpkin_nbt::compound::NbtCompound; use pumpkin_world::data::player_data::{PlayerDataError, PlayerDataStorage}; use std::sync::Arc; @@ -57,6 +58,14 @@ impl ServerPlayerData { /// /// A Result indicating success or the error that occurred. pub async fn handle_player_leave(&self, player: &Player) -> Result<(), PlayerDataError> { + player + .player_screen_handler + .lock() + .await + .on_closed(player) + .await; + player.on_handled_screen_closed().await; + let mut nbt = NbtCompound::new(); player.write_nbt(&mut nbt).await; diff --git a/pumpkin/src/entity/combat.rs b/pumpkin/src/entity/combat.rs index 80fd2c3f0..2aa72d6d2 100644 --- a/pumpkin/src/entity/combat.rs +++ b/pumpkin/src/entity/combat.rs @@ -4,7 +4,6 @@ use pumpkin_data::{ }; use pumpkin_protocol::{client::play::CEntityVelocity, codec::var_int::VarInt}; use pumpkin_util::math::vector3::Vector3; -use pumpkin_world::item::ItemStack; use crate::{ entity::{Entity, player::Player}, @@ -27,12 +26,7 @@ impl AttackType { let sprinting = entity.sprinting.load(std::sync::atomic::Ordering::Relaxed); let on_ground = entity.on_ground.load(std::sync::atomic::Ordering::Relaxed); let fall_distance = player.living_entity.fall_distance.load(); - let sword = player - .inventory() - .lock() - .await - .held_item() - .is_some_and(ItemStack::is_sword); + let sword = player.inventory().held_item().lock().await.is_sword(); let is_strong = attack_cooldown_progress > 0.9; if sprinting && is_strong { diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index ac28002ae..8e1eeb6ba 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -4,7 +4,7 @@ use std::sync::{ }; use async_trait::async_trait; -use pumpkin_data::{damage::DamageType, item::Item}; +use pumpkin_data::damage::DamageType; use pumpkin_protocol::{ client::play::{CTakeItemEntity, MetaDataType, Metadata}, codec::item_stack_seralizer::ItemStackSerializer, @@ -27,7 +27,7 @@ pub struct ItemEntity { } impl ItemEntity { - pub async fn new(entity: Entity, item_id: u16, count: u32) -> Self { + pub async fn new(entity: Entity, item_stack: ItemStack) -> Self { entity .set_velocity(Vector3::new( rand::random::() * 0.2 - 0.1, @@ -38,10 +38,7 @@ impl ItemEntity { entity.yaw.store(rand::random::() * 360.0); Self { entity, - item_stack: Mutex::new(ItemStack::new( - count as u8, - Item::from_id(item_id).expect("We passed a bad item id into ItemEntity"), - )), + item_stack: Mutex::new(item_stack), item_age: AtomicU32::new(0), pickup_delay: Mutex::new(10), // Vanilla pickup delay is 10 ticks } @@ -49,8 +46,7 @@ impl ItemEntity { pub async fn new_with_velocity( entity: Entity, - item_id: u16, - count: u32, + item_stack: ItemStack, velocity: Vector3, pickup_delay: u8, ) -> Self { @@ -58,21 +54,17 @@ impl ItemEntity { entity.yaw.store(rand::random::() * 360.0); Self { entity, - item_stack: Mutex::new(ItemStack::new( - count as u8, - Item::from_id(item_id).expect("We passed a bad item id into ItemEntity"), - )), + item_stack: Mutex::new(item_stack), item_age: AtomicU32::new(0), pickup_delay: Mutex::new(pickup_delay), // Vanilla pickup delay is 10 ticks } } - pub async fn send_meta_packet(&self) { self.entity .send_meta_data(&[Metadata::new( 8, MetaDataType::ItemStack, - &ItemStackSerializer::from(self.item_stack.lock().await.clone()), + &ItemStackSerializer::from(*self.item_stack.lock().await), )]) .await; } @@ -104,77 +96,30 @@ impl EntityBase for ItemEntity { *delay == 0 }; - if can_pickup { - let mut inv = player.inventory.lock().await; - let mut total_pick_up = 0; - let mut slot_updates = Vec::new(); - let remove_entity = { - let item_stack = self.item_stack.lock().await.clone(); - let mut stack_size = item_stack.item_count; - let max_stack = item_stack.item.components.max_stack_size; - while stack_size > 0 { - if let Some(slot) = inv.get_pickup_item_slot(item_stack.item.id) { - // Fill the inventory while there are items in the stack and space in the inventory - let maybe_stack = inv.get_slot(slot).unwrap(); + if can_pickup + && player + .inventory + .insert_stack_anywhere(&mut *self.item_stack.lock().await) + .await + { + player + .client + .enqueue_packet(&CTakeItemEntity::new( + self.entity.entity_id.into(), + player.entity_id().into(), + self.item_stack.lock().await.item_count.into(), + )) + .await; + player + .current_screen_handler + .lock() + .await + .lock() + .await + .send_content_updates() + .await; - if let Some(existing_stack) = maybe_stack { - // We have the item in this stack already - - // This is bounded to `u8::MAX` - let amount_to_fill = u32::from(max_stack - existing_stack.item_count); - // This is also bounded to `u8::MAX` since `amount_to_fill` is max `u8::MAX` - let amount_to_add = amount_to_fill.min(u32::from(stack_size)); - // Therefore this is safe - - // Update referenced stack so next call to `get_pickup_item_slot` is - // correct - existing_stack.item_count += amount_to_add as u8; - total_pick_up += amount_to_add; - - debug_assert!(amount_to_add > 0); - stack_size = stack_size.saturating_sub(amount_to_add as u8); - - slot_updates.push((slot, existing_stack.clone())); - } else { - // A new stack - - // This is bounded to `u8::MAX` - let amount_to_fill = u32::from(max_stack); - // This is also bounded to `u8::MAX` since `amount_to_fill` is max `u8::MAX` - let amount_to_add = amount_to_fill.min(u32::from(stack_size)); - total_pick_up += amount_to_add; - - debug_assert!(amount_to_add > 0); - stack_size = stack_size.saturating_sub(amount_to_add as u8); - - slot_updates.push((slot, self.item_stack.lock().await.clone())); - } - } else { - // We can't pick anything else up - break; - } - } - - stack_size == 0 - }; - - if total_pick_up > 0 { - player - .client - .enqueue_packet(&CTakeItemEntity::new( - self.entity.entity_id.into(), - player.entity_id().into(), - total_pick_up.try_into().unwrap(), - )) - .await; - } - - // TODO: Can we batch slot updates? - for (slot, stack) in slot_updates { - player.update_single_slot(&mut inv, slot, stack).await; - } - - if remove_entity { + if self.item_stack.lock().await.is_empty() { self.entity.remove().await; } else { // Update entity diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 01cddb3fc..c5c9cd72c 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use std::sync::atomic::{AtomicU8, Ordering::Relaxed}; use std::{collections::HashMap, sync::atomic::AtomicI32}; @@ -10,11 +11,13 @@ use pumpkin_config::advanced_config; use pumpkin_data::Block; use pumpkin_data::entity::{EffectType, EntityStatus}; use pumpkin_data::{damage::DamageType, sound::Sound}; +use pumpkin_inventory::entity_equipment::EntityEquipment; +use pumpkin_inventory::equipment_slot::EquipmentSlot; use pumpkin_nbt::tag::NbtTag; use pumpkin_protocol::client::play::{CHurtAnimation, CTakeItemEntity}; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::{ - client::play::{CDamageEvent, CSetEquipment, EquipmentSlot, MetaDataType, Metadata}, + client::play::{CDamageEvent, CSetEquipment, MetaDataType, Metadata}, codec::item_stack_seralizer::ItemStackSerializer, }; use pumpkin_util::math::vector3::Vector3; @@ -39,6 +42,7 @@ pub struct LivingEntity { /// The distance the entity has been falling. pub fall_distance: AtomicCell, pub active_effects: Mutex>, + pub entity_equipment: Arc>, } impl LivingEntity { pub fn new(entity: Entity) -> Self { @@ -52,13 +56,14 @@ impl LivingEntity { fall_distance: AtomicCell::new(0.0), death_time: AtomicU8::new(0), active_effects: Mutex::new(HashMap::new()), + entity_equipment: Arc::new(Mutex::new(EntityEquipment::new())), } } pub async fn send_equipment_changes(&self, equipment: &[(EquipmentSlot, ItemStack)]) { - let equipment: Vec<(EquipmentSlot, ItemStackSerializer)> = equipment + let equipment: Vec<(i8, ItemStackSerializer)> = equipment .iter() - .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) + .map(|(slot, stack)| (slot.discriminant(), ItemStackSerializer::from(*stack))) .collect(); self.entity .world @@ -349,6 +354,7 @@ impl NBTStorage for LivingEntity { async fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { self.entity.write_nbt(nbt).await; nbt.put("Health", NbtTag::Float(self.health.load())); + //TODO: write equipment // todo more... } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 1d61b2b40..20e784669 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -555,7 +555,9 @@ impl NBTStorage for Entity { pub trait NBTStorage: Send + Sync { async fn write_nbt(&self, nbt: &mut NbtCompound); - async fn read_nbt(&mut self, nbt: &mut NbtCompound); + async fn read_nbt(&mut self, _nbt: &mut NbtCompound) {} + + async fn read_nbt_non_mut(&self, _nbt: &mut NbtCompound) {} } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 811ed85a1..a1b91728f 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -36,6 +36,7 @@ use crate::{ use crate::{error::PumpkinError, net::GameProfile}; use async_trait::async_trait; use crossbeam::atomic::AtomicCell; +use log::warn; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::{ BlockState, @@ -45,15 +46,20 @@ use pumpkin_data::{ particle::Particle, sound::{Sound, SoundCategory}, }; -use pumpkin_inventory::player::{ - PlayerInventory, SLOT_BOOT, SLOT_CRAFT_INPUT_END, SLOT_CRAFT_INPUT_START, SLOT_HELM, - SLOT_HOTBAR_END, SLOT_INV_START, SLOT_OFFHAND, +use pumpkin_inventory::{ + player::{player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler}, + screen_handler::{ + InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour, ScreenHandlerFactory, + ScreenHandlerListener, + }, + sync_handler::SyncHandler, }; use pumpkin_macros::send_cancellable; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; use pumpkin_protocol::client::play::{ - CEntityPositionSync, CSetHeldItem, PlayerInfoFlags, PreviousMessage, + CCloseContainer, CEntityPositionSync, COpenScreen, CSetContainerContent, CSetContainerProperty, + CSetContainerSlot, CSetCursorItem, CSetPlayerInventory, PlayerInfoFlags, PreviousMessage, }; use pumpkin_protocol::{ IdOr, RawPacket, ServerPacket, @@ -83,7 +89,7 @@ use pumpkin_protocol::{ use pumpkin_protocol::{client::play::CUpdateTime, codec::var_int::VarInt}; use pumpkin_protocol::{ client::play::Metadata, - server::play::{SClickContainer, SKeepAlive}, + server::play::{SClickSlot, SKeepAlive}, }; use pumpkin_util::{ GameMode, @@ -97,6 +103,7 @@ use pumpkin_util::{ use pumpkin_world::entity::entity_data_flags::{ DATA_PLAYER_MAIN_HAND, DATA_PLAYER_MODE_CUSTOMISATION, }; +use pumpkin_world::inventory::Inventory; use pumpkin_world::{cylindrical_chunk_iterator::Cylindrical, item::ItemStack, level::SyncChunk}; use tokio::sync::RwLock; use tokio::{sync::Mutex, task::JoinHandle}; @@ -188,7 +195,7 @@ pub struct Player { /// The client connection associated with the player. pub client: Client, /// The player's inventory. - pub inventory: Mutex, + pub inventory: Arc, /// The player's configuration settings. Changes when the player changes their settings. pub config: RwLock, /// The player's current gamemode (e.g., Survival, Creative, Adventure). @@ -251,10 +258,28 @@ pub struct Player { pub has_played_before: AtomicBool, pub chat_session: Arc>, pub signature_cache: Mutex, + pub player_screen_handler: Arc>, + pub current_screen_handler: Mutex>>, + pub screen_handler_sync_id: AtomicU8, + pub screen_handler_listener: Arc, + pub screen_handler_sync_handler: Arc, } impl Player { pub async fn new(client: Client, world: Arc, gamemode: GameMode) -> Self { + struct ScreenListener; + + impl ScreenHandlerListener for ScreenListener { + fn on_slot_update( + &self, + _screen_handler: &ScreenHandlerBehaviour, + _slot: u8, + _stack: ItemStack, + ) { + //println!("Slot updated: {slot:?}, {stack:?}"); + } + } + let gameprofile = client.gameprofile.lock().await.clone().map_or_else( || { log::error!("Client {} has no game profile!", client.id); @@ -271,14 +296,22 @@ impl Player { let config = client.config.lock().await.clone().unwrap_or_default(); + let living_entity = LivingEntity::new(Entity::new( + player_uuid, + world, + Vector3::new(0.0, 0.0, 0.0), + EntityType::PLAYER, + matches!(gamemode, GameMode::Creative | GameMode::Spectator), + )); + + let inventory = Arc::new(PlayerInventory::new(living_entity.entity_equipment.clone())); + + let player_screen_handler = Arc::new(Mutex::new( + PlayerScreenHandler::new(&inventory, None, 0).await, + )); + Self { - living_entity: LivingEntity::new(Entity::new( - player_uuid, - world, - Vector3::new(0.0, 0.0, 0.0), - EntityType::PLAYER, - matches!(gamemode, GameMode::Creative | GameMode::Spectator), - )), + living_entity, config: RwLock::new(config), gameprofile, client, @@ -317,7 +350,8 @@ impl Player { AtomicCell::new(advanced_config().commands.default_op_level), |op| AtomicCell::new(op.level), ), - inventory: Mutex::new(PlayerInventory::new()), + inventory, + // TODO: enderChestInventory experience_level: AtomicI32::new(0), experience_progress: AtomicCell::new(0.0), experience_points: AtomicI32::new(0), @@ -330,6 +364,11 @@ impl Player { has_played_before: AtomicBool::new(false), chat_session: Arc::new(Mutex::new(ChatSession::default())), // Placeholder value until the player actually sets their session id signature_cache: Mutex::new(MessageCache::default()), + player_screen_handler: player_screen_handler.clone(), + current_screen_handler: Mutex::new(player_screen_handler), + screen_handler_sync_id: AtomicU8::new(0), + screen_handler_listener: Arc::new(ScreenListener {}), + screen_handler_sync_handler: Arc::new(SyncHandler::new()), } } @@ -346,7 +385,7 @@ impl Player { self.client.spawn_task(task) } - pub fn inventory(&self) -> &Mutex { + pub fn inventory(&self) -> &Arc { &self.inventory } @@ -397,8 +436,8 @@ impl Player { let attacker_entity = &self.living_entity.entity; let config = &advanced_config().pvp; - let inventory = self.inventory().lock().await; - let item_slot = inventory.held_item(); + let inventory = self.inventory(); + let item_stack = inventory.held_item(); let base_damage = 1.0; let base_attack_speed = 4.0; @@ -408,22 +447,19 @@ impl Player { let mut add_speed = 0.0; // Get the attack damage - if let Some(item_stack) = item_slot { - // TODO: this should be cached in memory - if let Some(modifiers) = item_stack.item.components.attribute_modifiers { - for item_mod in modifiers { - if item_mod.operation == Operation::AddValue { - if item_mod.id == "minecraft:base_attack_damage" { - add_damage = item_mod.amount; - } - if item_mod.id == "minecraft:base_attack_speed" { - add_speed = item_mod.amount; - } + // TODO: this should be cached in memory, we shouldn't just use default here either + if let Some(modifiers) = item_stack.lock().await.item.components.attribute_modifiers { + for item_mod in modifiers { + if item_mod.operation == Operation::AddValue { + if item_mod.id == "minecraft:base_attack_damage" { + add_damage = item_mod.amount; + } + if item_mod.id == "minecraft:base_attack_speed" { + add_speed = item_mod.amount; } } } } - drop(inventory); let attack_speed = base_attack_speed + add_speed; @@ -548,9 +584,22 @@ impl Player { } pub async fn tick(&self, server: &Server) { - if self.client.closed.load(Relaxed) { + self.current_screen_handler + .lock() + .await + .lock() + .await + .send_content_updates() + .await; + + if self + .client + .closed + .load(std::sync::atomic::Ordering::Relaxed) + { return; } + if self.packet_sequence.load(Relaxed) > -1 { self.client .enqueue_packet(&CAcknowledgeBlockChange::new( @@ -1181,19 +1230,19 @@ impl Player { !block.tool_required() || self .inventory + .held_item() .lock() .await - .held_item() - .map_or_else(|| false, |e| e.is_correct_for_drops(block_name)) + .is_correct_for_drops(block_name) } pub async fn get_mining_speed(&self, block_name: &str) -> f32 { let mut speed = self .inventory + .held_item() .lock() .await - .get_mining_speed(block_name) - .await; + .get_speed(block_name); // Haste if self.living_entity.has_effect(EffectType::Haste).await || self @@ -1257,7 +1306,7 @@ impl Player { .await; } - pub async fn drop_item(&self, item_id: u16, count: u32) { + pub async fn drop_item(&self, item_stack: ItemStack) { let entity = self.world().await.create_entity( self.living_entity.entity.pos.load() + Vector3::new(0.0, f64::from(EntityType::PLAYER.eye_height) - 0.3, 0.0), @@ -1281,18 +1330,33 @@ impl Player { // TODO: Merge stacks together let item_entity = - Arc::new(ItemEntity::new_with_velocity(entity, item_id, count, velocity, 40).await); + Arc::new(ItemEntity::new_with_velocity(entity, item_stack, velocity, 40).await); self.world().await.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; } pub async fn drop_held_item(&self, drop_stack: bool) { - let mut inv = self.inventory.lock().await; - if let Some(item_stack) = inv.held_item_mut() { + let binding = self.inventory.held_item(); + let mut item_stack = binding.lock().await; + + if !item_stack.is_empty() { let drop_amount = if drop_stack { item_stack.item_count } else { 1 }; - self.drop_item(item_stack.item.id, u32::from(drop_amount)) + self.drop_item(item_stack.copy_with_count(drop_amount)) .await; - inv.decrease_current_stack(drop_amount); + item_stack.decrement(drop_amount); + let selected_slot = self.inventory.get_selected_slot(); + let inv: Arc = self.inventory.clone(); + let binding = self.current_screen_handler.lock().await; + let mut screen_handler = binding.lock().await; + let slot_index = screen_handler + .get_slot_index(&inv, selected_slot as usize) + .await; + + if let Some(slot_index) = slot_index { + screen_handler + .set_received_stack(slot_index, *item_stack) + .await; + } } } @@ -1466,14 +1530,171 @@ impl Player { self.set_experience(new_level, progress, new_points).await; } - /// Send the player's inventory to the client. - pub async fn send_inventory(&self) { - self.set_container_content(None).await; + pub fn increment_screen_handler_sync_id(&self) { + let current_id = self.screen_handler_sync_id.load(Ordering::Relaxed); + self.screen_handler_sync_id + .store(current_id % 100 + 1, Ordering::Relaxed); + } + + pub async fn close_handled_screen(&self) { self.client - .send_packet_now(&CSetHeldItem::new( - self.inventory.lock().await.selected as i8, + .enqueue_packet(&CCloseContainer::new( + self.current_screen_handler + .lock() + .await + .lock() + .await + .sync_id() + .into(), )) .await; + self.on_handled_screen_closed().await; + } + + pub async fn on_handled_screen_closed(&self) { + self.current_screen_handler + .lock() + .await + .lock() + .await + .on_closed(self) + .await; + + let player_screen_handler: Arc> = + self.player_screen_handler.clone(); + let current_screen_handler: Arc> = + self.current_screen_handler.lock().await.clone(); + + if !Arc::ptr_eq(&player_screen_handler, ¤t_screen_handler) { + player_screen_handler + .lock() + .await + .copy_shared_slots(current_screen_handler) + .await; + } + + *self.current_screen_handler.lock().await = self.player_screen_handler.clone(); + } + + pub async fn on_screen_handler_opened(&self, screen_handler: Arc>) { + let mut screen_handler = screen_handler.lock().await; + + screen_handler + .add_listener(self.screen_handler_listener.clone()) + .await; + + screen_handler + .update_sync_handler(self.screen_handler_sync_handler.clone()) + .await; + } + + pub async fn open_handled_screen( + &self, + screen_handler_factory: &dyn ScreenHandlerFactory, + ) -> Option { + if !self + .current_screen_handler + .lock() + .await + .lock() + .await + .as_any() + .is::() + { + self.close_handled_screen().await; + } + + self.increment_screen_handler_sync_id(); + + if let Some(screen_handler) = screen_handler_factory.create_screen_handler( + self.screen_handler_sync_id.load(Ordering::Relaxed), + &self.inventory, + self, + ) { + let screen_handler_temp = screen_handler.lock().await; + self.client + .enqueue_packet(&COpenScreen::new( + screen_handler_temp.sync_id().into(), + (screen_handler_temp + .window_type() + .expect("Can't open PlayerScreenHandler") as i32) + .into(), + &screen_handler_factory.get_display_name(), + )) + .await; + drop(screen_handler_temp); + self.on_screen_handler_opened(screen_handler.clone()).await; + *self.current_screen_handler.lock().await = screen_handler; + Some(self.screen_handler_sync_id.load(Ordering::Relaxed)) + } else { + //TODO: Send message if spectator + + None + } + } + + pub async fn on_slot_click(&self, packet: SClickSlot) { + let screen_handler = self.current_screen_handler.lock().await; + let mut screen_handler = screen_handler.lock().await; + let behaviour = screen_handler.get_behaviour(); + + // behaviour is dropped here + if i32::from(behaviour.sync_id) != packet.sync_id.0 { + return; + } + + if self.gamemode.load() == GameMode::Spectator { + screen_handler.sync_state().await; + return; + } + + if !screen_handler.can_use(self) { + warn!( + "Player {} interacted with invalid menu {:?}", + self.gameprofile.name, + screen_handler.window_type() + ); + return; + } + + let slot = packet.slot; + + if !screen_handler.is_slot_valid(i32::from(slot)).await { + warn!( + "Player {} clicked invalid slot index: {}, available slots: {}", + self.gameprofile.name, + slot, + screen_handler.get_behaviour().slots.len() + ); + return; + } + + let not_in_sync = packet.revision.0 != (behaviour.revision as i32); + + screen_handler.disable_sync().await; + screen_handler + .on_slot_click( + i32::from(slot), + i32::from(packet.button), + packet.mode.clone(), + self, + ) + .await; + + for (key, value) in packet.array_of_changed_slots { + screen_handler.set_received_hash(key as usize, value).await; + } + + screen_handler + .set_received_cursor_hash(packet.carried_item) + .await; + screen_handler.enable_sync().await; + + if not_in_sync { + screen_handler.update_to_client().await; + } else { + screen_handler.send_content_updates().await; + } } } @@ -1481,7 +1702,7 @@ impl Player { impl NBTStorage for Player { async fn write_nbt(&self, nbt: &mut NbtCompound) { self.living_entity.write_nbt(nbt).await; - self.inventory.lock().await.write_nbt(nbt).await; + self.inventory.write_nbt(nbt).await; self.abilities.lock().await.write_nbt(nbt).await; @@ -1502,7 +1723,7 @@ impl NBTStorage for Player { async fn read_nbt(&mut self, nbt: &mut NbtCompound) { self.living_entity.read_nbt(nbt).await; - self.inventory.lock().await.read_nbt(nbt).await; + self.inventory.read_nbt_non_mut(nbt).await; self.abilities.lock().await.read_nbt(nbt).await; self.gamemode.store( @@ -1535,46 +1756,28 @@ impl NBTStorage for Player { impl NBTStorage for PlayerInventory { async fn write_nbt(&self, nbt: &mut NbtCompound) { // Save the selected slot (hotbar) - nbt.put_int("SelectedItemSlot", self.selected as i32); + nbt.put_int("SelectedItemSlot", i32::from(self.get_selected_slot())); // Create inventory list with the correct capacity (inventory size) - let mut vec: Vec = Vec::with_capacity(SLOT_OFFHAND); + let mut vec: Vec = Vec::new(); - // Helper function to add items to the vector - let mut add_item = |slot: usize, stack_ref: Option<&ItemStack>| { - if let Some(stack) = stack_ref { + for i in 0..self.main_inventory.len() { + let stack = self.main_inventory[i].lock().await; + if !stack.is_empty() { let mut item_compound = NbtCompound::new(); - item_compound.put_byte("Slot", slot as i8); + item_compound.put_byte("Slot", i as i8); stack.write_item_stack(&mut item_compound); vec.push(NbtTag::Compound(item_compound)); } - }; - - // Crafting input slots - for slot in SLOT_CRAFT_INPUT_START..=SLOT_CRAFT_INPUT_END { - add_item(slot, self.crafting_slots()[slot - SLOT_CRAFT_INPUT_START]); } - // Armor slots - for slot in SLOT_HELM..=SLOT_BOOT { - add_item(slot, self.armor_slots()[slot - SLOT_HELM]); - } - - // Main inventory slots (includes hotbar in the data structure) - for slot in SLOT_INV_START..=SLOT_HOTBAR_END { - add_item(slot, self.item_slots()[slot - SLOT_INV_START]); - } - - // Offhand - add_item(SLOT_OFFHAND, self.offhand_slot()); - // Save the inventory list nbt.put("Inventory", NbtTag::List(vec.into_boxed_slice())); } - async fn read_nbt(&mut self, nbt: &mut NbtCompound) { + async fn read_nbt_non_mut(&self, nbt: &mut NbtCompound) { // Read selected hotbar slot - self.selected = nbt.get_int("SelectedItemSlot").unwrap_or(0) as usize; + self.set_selected_slot(nbt.get_int("SelectedItemSlot").unwrap_or(0) as u8); // Process inventory list if let Some(inventory_list) = nbt.get_list("Inventory") { @@ -1583,7 +1786,7 @@ impl NBTStorage for PlayerInventory { if let Some(slot_byte) = item_compound.get_byte("Slot") { let slot = slot_byte as usize; if let Some(item_stack) = ItemStack::read_item_stack(item_compound) { - let _ = self.set_slot(slot, Some(item_stack), true); + self.set_stack(slot, item_stack).await; } } } @@ -1729,9 +1932,8 @@ impl Player { self.handle_play_ping_request(SPlayPingRequest::read(payload)?) .await; } - SClickContainer::PACKET_ID => { - self.handle_click_container(server, SClickContainer::read(payload)?) - .await?; + SClickSlot::PACKET_ID => { + self.on_slot_click(SClickSlot::read(payload)?).await; } SSetHeldItem::PACKET_ID => { self.handle_set_held_item(SSetHeldItem::read(payload)?) @@ -2053,3 +2255,34 @@ impl MessageCache { self.full_cache.push_front(signature.into()); // Since recipient saw this message it will be most recent in cache } } + +#[async_trait] +impl InventoryPlayer for Player { + async fn drop_item(&self, item: ItemStack, _retain_ownership: bool) { + self.drop_item(item).await; + } + + fn get_inventory(&self) -> Arc { + self.inventory.clone() + } + + async fn enqueue_inventory_packet(&self, packet: &CSetContainerContent) { + self.client.enqueue_packet(packet).await; + } + + async fn enqueue_slot_packet(&self, packet: &CSetContainerSlot) { + self.client.enqueue_packet(packet).await; + } + + async fn enqueue_cursor_packet(&self, packet: &CSetCursorItem) { + self.client.enqueue_packet(packet).await; + } + + async fn enqueue_property_packet(&self, packet: &CSetContainerProperty) { + self.client.enqueue_packet(packet).await; + } + + async fn enqueue_slot_set_packet(&self, packet: &CSetPlayerInventory) { + self.client.enqueue_packet(packet).await; + } +} diff --git a/pumpkin/src/item/items/bucket.rs b/pumpkin/src/item/items/bucket.rs index 5b388709a..f21bfbb94 100644 --- a/pumpkin/src/item/items/bucket.rs +++ b/pumpkin/src/item/items/bucket.rs @@ -5,13 +5,9 @@ use async_trait::async_trait; use pumpkin_data::Block; use pumpkin_data::fluid::Fluid; use pumpkin_data::item::Item; -use pumpkin_inventory::player::PlayerInventory; -use pumpkin_protocol::client::play::CSetContainerSlot; -use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_util::GameMode; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; -use pumpkin_world::item::ItemStack; use crate::item::pumpkin_item::{ItemMetadata, PumpkinItem}; use crate::world::{BlockFlags, World}; @@ -79,49 +75,14 @@ impl PumpkinItem for EmptyBucketItem { let (block_pos, _) = world.raytrace(start_pos, end_pos, checker).await; if let Some(pos) = block_pos { - let Ok(state_id) = world.get_block_state_id(&pos).await else { + let Ok(_state_id) = world.get_block_state_id(&pos).await else { return; }; world .set_block_state(&pos, Block::AIR.id, BlockFlags::NOTIFY_NEIGHBORS) .await; - let mut inventory = player.inventory().lock().await; - let selected = inventory.get_selected_slot(); - let item_type = if state_id == Block::WATER.default_state_id { - Item::WATER_BUCKET - } else { - Item::LAVA_BUCKET - }; - let item_stack = Some(ItemStack::new(1, item_type.clone())); - let slot_data = ItemStackSerializer::from(item_stack.clone()); - let game_mode = player.gamemode.load(); - if game_mode == GameMode::Creative { - let slot = inventory.get_pickup_item_slot(item_type.id); - if let Some(slot) = slot { - if let Err(err) = inventory.set_slot(slot, item_stack, false) { - log::error!("Failed to set slot: {err}"); - } else { - let dest_packet = CSetContainerSlot::new( - PlayerInventory::CONTAINER_ID, - inventory.state_id as i32, - slot as i16, - &slot_data, - ); - player.client.enqueue_packet(&dest_packet).await; - } - } - } else if let Err(err) = inventory.set_slot(selected, item_stack.clone(), false) { - log::error!("Failed to set slot: {err}"); - } else { - let dest_packet = CSetContainerSlot::new( - PlayerInventory::CONTAINER_ID, - inventory.state_id as i32, - selected as i16, - &slot_data, - ); - player.client.enqueue_packet(&dest_packet).await; - } + //TODO: Pickup in inv } } } @@ -162,21 +123,7 @@ impl PumpkinItem for FilledBucketItem { ) .await; if player.gamemode.load() != GameMode::Creative { - let mut inventory = player.inventory().lock().await; - let selected = inventory.get_selected_slot(); - let item = Some(ItemStack::new(1, Item::BUCKET)); - let slot_data = ItemStackSerializer::from(item.clone()); - if let Err(err) = inventory.set_slot(selected, item, false) { - log::error!("Failed to set slot: {err}"); - } else { - let dest_packet = CSetContainerSlot::new( - PlayerInventory::CONTAINER_ID, - inventory.state_id as i32, - selected as i16, - &slot_data, - ); - player.client.enqueue_packet(&dest_packet).await; - } + //TODO: Pickup in inv } } } diff --git a/pumpkin/src/item/items/hoe.rs b/pumpkin/src/item/items/hoe.rs index bc1b24bed..209e0a585 100644 --- a/pumpkin/src/item/items/hoe.rs +++ b/pumpkin/src/item/items/hoe.rs @@ -10,6 +10,7 @@ use pumpkin_data::item::Item; use pumpkin_data::tag::Tagable; use pumpkin_util::math::position::BlockPos; use pumpkin_world::block::BlockDirection; +use pumpkin_world::item::ItemStack; use std::sync::Arc; pub struct HoeItem; @@ -94,8 +95,9 @@ impl PumpkinItem for HoeItem { }; let entity = world.create_entity(location, EntityType::ITEM); // TODO: Merge stacks together - let item_entity = - Arc::new(ItemEntity::new(entity, Block::HANGING_ROOTS.item_id, 1).await); + let item_entity = Arc::new( + ItemEntity::new(entity, ItemStack::new(1, &Item::HANGING_ROOTS)).await, + ); world.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; } diff --git a/pumpkin/src/net/container.rs b/pumpkin/src/net/container.rs deleted file mode 100644 index fe2c0b48f..000000000 --- a/pumpkin/src/net/container.rs +++ /dev/null @@ -1,724 +0,0 @@ -use crate::entity::player::Player; -use crate::server::Server; -use pumpkin_data::item::Item; -use pumpkin_data::screen::WindowType; -use pumpkin_inventory::Container; -use pumpkin_inventory::container_click::{ - Click, ClickType, DropType, KeyClick, MouseClick, MouseDragState, MouseDragType, -}; -use pumpkin_inventory::drag_handler::DragHandler; -use pumpkin_inventory::player::{SLOT_BOOT, SLOT_CHEST, SLOT_HELM, SLOT_HOTBAR_START, SLOT_LEG}; -use pumpkin_inventory::window_property::{WindowProperty, WindowPropertyTrait}; -use pumpkin_inventory::{InventoryError, OptionallyCombinedContainer, container_click}; -use pumpkin_protocol::client::play::{ - CCloseContainer, COpenScreen, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, -}; -use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; -use pumpkin_protocol::codec::var_int::VarInt; -use pumpkin_protocol::server::play::SClickContainer; -use pumpkin_util::text::TextComponent; -use pumpkin_util::{GameMode, MutableSplitSlice}; -use pumpkin_world::item::ItemStack; -use std::sync::Arc; - -impl Player { - pub async fn open_container(&self, server: &Server, window_type: WindowType) { - let mut inventory = self.inventory().lock().await; - //inventory.state_id = 0; - inventory.increment_state_id(); - inventory.total_opened_containers += 1; - let mut container = self.get_open_container(server).await; - let mut container = match container.as_mut() { - Some(container) => Some(container.lock().await), - None => None, - }; - let window_title = container.as_ref().map_or_else( - || inventory.window_name(), - |container| container.window_name(), - ); - let title = TextComponent::text(window_title); - - self.client - .enqueue_packet(&COpenScreen::new( - inventory.total_opened_containers.into(), - VarInt(window_type as i32), - &title, - )) - .await; - drop(inventory); - self.set_container_content(container.as_deref_mut()).await; - } - - pub async fn set_container_content(&self, container: Option<&mut Box>) { - let mut inventory = self.inventory().lock().await; - - let total_opened_containers = inventory.total_opened_containers; - let id = if container.is_some() { - total_opened_containers - } else { - 0 - }; - - let container = OptionallyCombinedContainer::new(&mut inventory, container); - - let slots: Vec = container - .all_slots_ref() - .into_iter() - .map(|i| ItemStackSerializer::from(i.unwrap_or(&ItemStack::EMPTY).clone())) - .collect(); - - let carried_item = self.carried_item.lock().await; - let carried_item = carried_item.as_ref().map_or_else( - || ItemStackSerializer::from(ItemStack::EMPTY.clone()), - |item| ItemStackSerializer::from(item.clone()), - ); - - inventory.increment_state_id(); - let packet = CSetContainerContent::new( - id.into(), - (inventory.state_id).try_into().unwrap(), - &slots, - &carried_item, - ); - self.client.enqueue_packet(&packet).await; - } - - /// The official Minecraft client is weird, and will always just close *any* window that is opened when this gets sent - // TODO: is this just bc ids are not synced? - pub async fn close_container(&self) { - let mut inventory = self.inventory().lock().await; - inventory.total_opened_containers += 1; - self.client - .enqueue_packet(&CCloseContainer::new( - inventory.total_opened_containers.into(), - )) - .await; - } - - pub async fn set_container_property( - &mut self, - window_property: WindowProperty, - ) { - let (id, value) = window_property.into_tuple(); - self.client - .enqueue_packet(&CSetContainerProperty::new( - self.inventory().lock().await.total_opened_containers.into(), - id, - value, - )) - .await; - } - - 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().cloned(), - 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 = ItemStackSerializer::from(slot.cloned()); - drop(opened_container); - self.send_container_changes(server, slot_index, slot) - .await?; - } - } - } - Ok(()) - } - - pub async fn handle_decrease_item( - &self, - _server: &Server, - slot_index: i16, - 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 = ItemStackSerializer::from(item_stack.cloned()); - *state_id += 1; - let packet = CSetContainerSlot::new(0, *state_id as i32, slot_index, &slot); - self.client.enqueue_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) => { - if let container_click::Slot::Normal(slot) = click.slot { - let mut inventory = self.inventory().lock().await; - let mut container = - OptionallyCombinedContainer::new(&mut inventory, opened_container); - let slots = container.all_slots(); - - if let Some(item_stack) = slots[slot].as_mut() { - match drop_type { - DropType::FullStack => { - self.drop_item( - item_stack.item.id, - u32::from(item_stack.item_count), - ) - .await; - *slots[slot] = None; - } - DropType::SingleItem => { - self.drop_item(item_stack.item.id, 1).await; - item_stack.item_count -= 1; - if item_stack.item_count == 0 { - *slots[slot] = None; - } - } - } - } - } - Ok(()) - } - } - } - - 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); - let mut carried_item = self.carried_item.lock().await; - match slot { - container_click::Slot::Normal(slot) => { - container.handle_item_change(&mut carried_item, slot, mouse_click, taking_crafted) - } - container_click::Slot::OutsideInventory => { - if let Some(item_stack) = carried_item.as_mut() { - match mouse_click { - MouseClick::Left => { - self.drop_item(item_stack.item.id, u32::from(item_stack.item_count)) - .await; - *carried_item = None; - } - MouseClick::Right => { - self.drop_item(item_stack.item.id, 1).await; - item_stack.item_count -= 1; - if item_stack.item_count == 0 { - *carried_item = None; - } - } - } - } - Ok(()) - } - } - } - - /// TODO: Allow equiping/de equiping armor and allow taking items from crafting grid - 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 has_container = opened_container.is_some(); - let container_size = opened_container - .as_ref() - .map_or(0, |c| c.all_slots_ref().len()); - let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); - - match slot { - container_click::Slot::Normal(slot) => { - let mut all_slots = container.all_slots(); - let (item_stack, mut split_slice) = - MutableSplitSlice::extract_ith(&mut all_slots, slot); - let Some(clicked_item_stack) = item_stack else { - return Ok(()); - }; - - // Define the two inventories and determine which one contains the source slot - let (inv1_range, inv2_range) = if has_container { - // When container is open: - // Inv1 = Container slots (0 to container_size-1) - // Inv2 = Player inventory (container_size to end) - ((0..container_size), (container_size..split_slice.len())) - } else { - // When no container: - // Inv1 = Hotbar (36-45) - // Inv2 = Main inventory (9-36) - ((36..45), (9..36)) - }; - - // Determine which inventory we're moving from and to - let (source_inv, target_inv) = if inv1_range.contains(&slot) { - (&inv1_range, &inv2_range) - } else if inv2_range.contains(&slot) { - (&inv2_range, &inv1_range) - } else { - // When moving from top slots to inventory - (&(0..9), &(9..45)) - }; - - // If moving to hotbar, reverse the order to fill from right to left - let target_slots: Vec = - if has_container && source_inv.contains(&slot) && source_inv == &inv1_range { - target_inv.clone().rev().collect() - } else { - target_inv.clone().collect() - }; - - //Handle armor slots - if !has_container { - let temp_item_stack = ItemStack::new(1, clicked_item_stack.item.clone()); - if slot != SLOT_HELM - && temp_item_stack.is_helmet() - && split_slice[SLOT_HELM].is_none() - { - *split_slice[SLOT_HELM] = Some(temp_item_stack); - **item_stack = None; - return Ok(()); - } else if slot != SLOT_CHEST - && temp_item_stack.is_chestplate() - && split_slice[SLOT_CHEST].is_none() - { - *split_slice[SLOT_CHEST] = Some(temp_item_stack); - **item_stack = None; - return Ok(()); - } else if slot != SLOT_LEG - && temp_item_stack.is_leggings() - && split_slice[SLOT_LEG].is_none() - { - *split_slice[SLOT_LEG] = Some(temp_item_stack); - **item_stack = None; - return Ok(()); - } else if slot != SLOT_BOOT - && temp_item_stack.is_boots() - && split_slice[SLOT_BOOT].is_none() - { - *split_slice[SLOT_BOOT] = Some(temp_item_stack); - **item_stack = None; - return Ok(()); - } - } - - // First try to stack with existing items - let max_stack_size = clicked_item_stack.item.components.max_stack_size; - for target_idx in &target_slots { - if let Some(target_item) = split_slice[*target_idx].as_mut() { - if target_item.item.id == clicked_item_stack.item.id - && target_item.item_count < max_stack_size - { - let space_in_stack = max_stack_size - target_item.item_count; - let amount_to_add = clicked_item_stack.item_count.min(space_in_stack); - target_item.item_count += amount_to_add; - clicked_item_stack.item_count -= amount_to_add; - - if clicked_item_stack.item_count == 0 { - **item_stack = None; - return Ok(()); - } - } - } - } - - // Then try to place in empty slots - for target_idx in target_slots { - if split_slice[target_idx].is_none() - || split_slice[target_idx] - .as_ref() - .is_some_and(|item| item.item_count == 0) - { - std::mem::swap(split_slice[target_idx], *item_stack); - return Ok(()); - } - } - } - 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 as usize + SLOT_HOTBAR_START, - KeyClick::Offhand => 45, - }; - let mut inventory = self.inventory().lock().await; - let mut changing_item_slot = inventory.get_slot(changing_slot)?.clone(); - 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)? = 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) { - let mut carried_item = self.carried_item.lock().await; - *carried_item = Some(item.clone()); - } - 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 carried_item = self.carried_item.lock().await; - let Some(carried_item) = carried_item.as_mut() else { - return Ok(()); - }; - - // Iterate directly over container slots to modify them in place' - for slot in container.all_slots() { - if let Some(itemstack) = slot { - if itemstack.item.id == carried_item.item.id { - if itemstack.item_count + carried_item.item_count - <= carried_item.item.components.max_stack_size - { - carried_item.item_count += itemstack.item_count; - *slot = None; - } else { - let overflow = itemstack.item_count - - (carried_item.item.components.max_stack_size - - carried_item.item_count); - carried_item.item_count = carried_item.item.components.max_stack_size; - itemstack.item_count = overflow; - } - - if carried_item.item_count == carried_item.item.components.max_stack_size { - break; - } - } - } - } - 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.lock().await; - drag_handler - .apply_drag(&mut carried_item, &mut container, &container_id, player_id) - .await - } - } - } - - 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 - - self.living_entity - .entity - .world - .read() - .await - .players - .read() - .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() - } - - pub async fn send_container_changes( - &self, - server: &Server, - slot_index: usize, - slot: ItemStackSerializer<'_>, - ) -> 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.increment_state_id(); - let packet = CSetContainerSlot::new( - total_opened_containers as i8, - (inventory.state_id) as i32, - slot_index as i16, - &slot, - ); - player.client.enqueue_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, - } - } - - // TODO: Use this method when actually picking up items instead of just the command - async fn pickup_items(&self, item: Item, amount: u32) { - let mut amount_left = amount; - let max_stack = item.components.max_stack_size; - let mut inventory = self.inventory().lock().await; - - while let Some(slot) = inventory.get_pickup_item_slot(item.id) { - let item_stack = inventory - .get_slot(slot) - .expect("We just called a method that said this was a valid slot"); - - if let Some(item_stack) = item_stack { - let amount_to_add = max_stack - item_stack.item_count; - if let Some(new_amount_left) = amount_left.checked_sub(u32::from(amount_to_add)) { - item_stack.item_count = max_stack; - amount_left = new_amount_left; - } else { - // This is safe because amount left is less than amount_to_add which is a u8 - item_stack.item_count = max_stack - (amount_to_add - amount_left as u8); - // Return here because if we have less than the max amount left then the whole - // stack will be moved - return; - } - } else if let Some(new_amount_left) = amount_left.checked_sub(u32::from(max_stack)) { - *item_stack = Some(ItemStack { - item: item.clone(), - item_count: max_stack, - }); - amount_left = new_amount_left; - } else { - *item_stack = Some(ItemStack { - item: item.clone(), - // This is safe because amount left is less than max_stack which is a u8 - item_count: amount_left as u8, - }); - // Return here because if we have less than the max amount left then the whole - // stack will be moved - 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; - } -} diff --git a/pumpkin/src/net/mod.rs b/pumpkin/src/net/mod.rs index 63b8ef649..e0329f2e6 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -60,7 +60,6 @@ use thiserror::Error; use tokio_util::task::TaskTracker; use uuid::Uuid; pub mod authentication; -mod container; pub mod lan_broadcast; mod packet; mod proxy; diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index e6adcce24..b2edbc331 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -1,4 +1,8 @@ use pumpkin_data::block_properties::{BlockProperties, WaterLikeProperties}; +use pumpkin_data::item::Item; +use pumpkin_inventory::InventoryError; +use pumpkin_inventory::equipment_slot::EquipmentSlot; +use pumpkin_inventory::screen_handler::ScreenHandler; use rsa::pkcs1v15::{Signature as RsaPkcs1v15Signature, VerifyingKey}; use rsa::signature::Verifier; use sha1::Sha1; @@ -27,23 +31,19 @@ use crate::{ }; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::entity::{EntityType, entity_from_egg}; -use pumpkin_data::item::Item; use pumpkin_data::sound::Sound; use pumpkin_data::sound::SoundCategory; use pumpkin_data::{ Block, block_properties::{get_block_by_item, get_block_collision_shapes}, }; -use pumpkin_inventory::InventoryError; -use pumpkin_inventory::player::{ - PlayerInventory, SLOT_HOTBAR_END, SLOT_HOTBAR_START, SLOT_OFFHAND, -}; + +use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_macros::send_cancellable; use pumpkin_protocol::client::play::{ CBlockUpdate, CEntityPositionSync, COpenSignEditor, CPlayerInfoUpdate, CPlayerPosition, - CSetContainerSlot, CSetHeldItem, CSystemChatMessage, EquipmentSlot, InitChat, PlayerAction, + CSetSelectedSlot, CSystemChatMessage, InitChat, PlayerAction, }; -use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::server::play::{ FLAG_ON_GROUND, SChunkBatch, SCookieResponse as SPCookieResponse, SPlayerSession, SUpdateSign, @@ -84,7 +84,6 @@ pub enum BlockPlacingError { BlockOutOfReach, InvalidBlockFace, BlockOutOfWorld, - InventoryInvalid, InvalidGamemode, NoBaseBlock, } @@ -99,7 +98,7 @@ impl PumpkinError for BlockPlacingError { fn is_kick(&self) -> bool { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => false, - Self::InvalidBlockFace | Self::InventoryInvalid | Self::NoBaseBlock => true, + Self::InvalidBlockFace | Self::NoBaseBlock => true, } } @@ -107,7 +106,6 @@ impl PumpkinError for BlockPlacingError { match self { Self::BlockOutOfWorld | Self::InvalidGamemode | Self::NoBaseBlock => log::Level::Trace, Self::BlockOutOfReach | Self::InvalidBlockFace => log::Level::Warn, - Self::InventoryInvalid => log::Level::Error, } } @@ -115,7 +113,6 @@ impl PumpkinError for BlockPlacingError { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => None, Self::InvalidBlockFace => Some("Invalid block face".into()), - Self::InventoryInvalid => Some("Held item invalid".into()), Self::NoBaseBlock => Some("No base block".into()), } } @@ -561,27 +558,6 @@ impl Player { .store(ground.on_ground, std::sync::atomic::Ordering::Relaxed); } - pub async fn update_single_slot( - &self, - inventory: &mut PlayerInventory, - slot: usize, - stack: ItemStack, - ) { - inventory.increment_state_id(); - let slot_data = ItemStackSerializer::from(stack.clone()); - if let Err(err) = inventory.set_slot(slot, Some(stack), false) { - log::error!("Pick item set slot error: {err}"); - } else { - let dest_packet = CSetContainerSlot::new( - PlayerInventory::CONTAINER_ID, - inventory.state_id as i32, - slot as i16, - &slot_data, - ); - self.client.enqueue_packet(&dest_packet).await; - } - } - pub async fn handle_pick_item_from_block(&self, pick_item: SPickItemFromBlock) { if !self.can_interact_with_block_at(&pick_item.pos, 1.0) { return; @@ -597,69 +573,31 @@ impl Player { return; } - let mut inventory = self.inventory().lock().await; + let stack = ItemStack::new(1, Item::from_id(block.item_id).unwrap()); - let source_slot = inventory.get_slot_with_item(block.item_id); - let mut dest_slot = inventory.get_empty_hotbar_slot(); + let slot_with_stack = self.inventory().get_slot_with_stack(&stack).await; - let dest_slot_data = match inventory.get_slot(dest_slot + SLOT_HOTBAR_START) { - Ok(Some(stack)) => stack.clone(), - _ => ItemStack::new(0, Item::AIR), - }; - - // Early return if no source slot and not in creative mode - if source_slot.is_none() && self.gamemode.load() != GameMode::Creative { - return; - } - - match source_slot { - Some(slot_index) if (SLOT_HOTBAR_START..=SLOT_HOTBAR_END).contains(&slot_index) => { - // Case where item is in hotbar - dest_slot = slot_index - SLOT_HOTBAR_START; - } - Some(slot_index) => { - // Case where item is in inventory - - // Update destination slot - let source_slot_data = match inventory.get_slot(slot_index) { - Ok(Some(stack)) => stack.clone(), - _ => return, - }; - self.update_single_slot( - &mut inventory, - dest_slot + SLOT_HOTBAR_START, - source_slot_data, - ) - .await; - - // Update source slot - self.update_single_slot(&mut inventory, slot_index, dest_slot_data) + if slot_with_stack != -1 { + if PlayerInventory::is_valid_hotbar_index(slot_with_stack as usize) { + self.inventory.set_selected_slot(slot_with_stack as u8); + } else { + self.inventory + .swap_slot_with_hotbar(slot_with_stack as usize) .await; } - 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()); - self.update_single_slot(&mut inventory, dest_slot + SLOT_HOTBAR_START, item_stack) - .await; - - // Check if there is any empty slot in the player inventory - if let Some(slot_index) = inventory.get_empty_slot_no_order() { - inventory.increment_state_id(); - self.update_single_slot(&mut inventory, slot_index, dest_slot_data) - .await; - } - } - _ => return, + } else if self.gamemode.load() == GameMode::Creative { + self.inventory.swap_stack_with_hotbar(stack).await; } - // Update held item - inventory.set_selected(dest_slot); - let empty = &ItemStack::new(0, Item::AIR); - let stack = inventory.held_item().unwrap_or(empty); - let equipment = &[(EquipmentSlot::MainHand, stack.clone())]; - self.living_entity.send_equipment_changes(equipment).await; self.client - .enqueue_packet(&CSetHeldItem::new(dest_slot as i8)) + .enqueue_packet(&CSetSelectedSlot::new( + self.inventory.get_selected_slot() as i8 + )) + .await; + self.player_screen_handler + .lock() + .await + .send_content_updates() .await; } @@ -1163,17 +1101,17 @@ impl Player { let block = world.get_block(&location).await.unwrap(); let state = world.get_block_state(&location).await.unwrap(); - if let Some(held) = self.inventory.lock().await.held_item() { - if !server.item_registry.can_mine(&held.item, self) { - self.client - .enqueue_packet(&CBlockUpdate::new( - location, - VarInt(i32::from(state.id)), - )) - .await; - self.update_sequence(player_action.sequence.0); - return; - } + let inventory = self.inventory(); + let held = inventory.held_item(); + if !server.item_registry.can_mine(held.lock().await.item, self) { + self.client + .enqueue_packet(&CBlockUpdate::new( + location, + VarInt(i32::from(state.id)), + )) + .await; + self.update_sequence(player_action.sequence.0); + return; } // TODO: do validation @@ -1385,10 +1323,8 @@ impl Player { return Err(BlockPlacingError::InvalidBlockFace.into()); }; - let inventory = self.inventory().lock().await; - let slot_id = inventory.get_selected_slot(); - let held_item = inventory.held_item().cloned(); - drop(inventory); + let inventory = self.inventory(); + let held_item = inventory.held_item(); let entity = &self.living_entity.entity; let world = &entity.world.read().await; @@ -1401,8 +1337,7 @@ impl Player { .entity .sneaking .load(std::sync::atomic::Ordering::Relaxed); - - let Some(stack) = held_item else { + if held_item.lock().await.is_empty() { if !sneaking { // Using block with empty hand server @@ -1411,17 +1346,26 @@ impl Player { .await; } return Ok(()); - }; - + } if !sneaking { server .item_registry - .use_on_block(&stack.item, self, location, face, &block, server) + .use_on_block( + held_item.lock().await.item, + self, + location, + face, + &block, + server, + ) .await; self.update_sequence(use_item_on.sequence.0); + let item_stack = held_item.lock().await; + let item = item_stack.item; + drop(item_stack); let action_result = server .block_registry - .use_with_item(&block, self, location, &stack.item, server, world) + .use_with_item(&block, self, location, item, server, world) .await; match action_result { BlockActionResult::Continue => {} @@ -1432,14 +1376,14 @@ impl Player { } // Check if the item is a block, because not every item can be placed :D - if let Some(block) = get_block_by_item(stack.item.id) { + if let Some(block) = get_block_by_item(held_item.lock().await.item.id) { should_try_decrement = self .run_is_block_place(block, server, use_item_on, location, face) .await?; } // Check if the item is a spawn egg - if let Some(entity) = entity_from_egg(stack.item.id) { + if let Some(entity) = entity_from_egg(held_item.lock().await.item.id) { self.spawn_entity_from_egg(entity, location, face).await; should_try_decrement = true; } @@ -1448,20 +1392,7 @@ impl Player { // TODO: Config // Decrease block count if self.gamemode.load() != GameMode::Creative { - let mut inventory = self.inventory().lock().await; - - if !inventory.decrease_current_stack(1) { - return Err(BlockPlacingError::InventoryInvalid.into()); - } - // TODO: this should be by use item on not currently selected as they might be different - let _ = self - .handle_decrease_item( - server, - slot_id as i16, - inventory.held_item().cloned().as_ref(), - &mut inventory.state_id, - ) - .await; + held_item.lock().await.decrement(1); } } @@ -1484,19 +1415,14 @@ impl Player { world.add_block_entity(Arc::new(updated_sign)).await; } - pub async fn handle_use_item(&self, use_item: &SUseItem, server: &Server) { + pub async fn handle_use_item(&self, _use_item: &SUseItem, server: &Server) { if !self.has_client_loaded() { return; } - let held = { - let inventory = self.inventory().lock().await; - inventory.held_item().cloned() - }; - - if held.is_some() { - server.item_registry.on_use(&held.unwrap().item, self).await; - } - self.update_sequence(use_item.sequence.0); + let inventory = self.inventory(); + let binding = inventory.held_item(); + let held = binding.lock().await; + server.item_registry.on_use(held.item, self).await; } pub async fn handle_set_held_item(&self, held: SSetHeldItem) { @@ -1505,11 +1431,10 @@ impl Player { self.kick(TextComponent::text("Invalid held slot")).await; return; } - let mut inv = self.inventory().lock().await; - inv.set_selected(slot as usize); - let empty = &ItemStack::new(0, Item::AIR); - let stack = inv.held_item().unwrap_or(empty); - let equipment = &[(EquipmentSlot::MainHand, stack.clone())]; + let inv = self.inventory(); + inv.set_selected_slot(slot as u8); + let stack = *inv.held_item().lock().await; + let equipment = &[(EquipmentSlot::MAIN_HAND, stack)]; self.living_entity.send_equipment_changes(equipment).await; } @@ -1520,18 +1445,26 @@ impl Player { if self.gamemode.load() != GameMode::Creative { return Err(InventoryError::PermissionError); } - let valid_slot = packet.slot >= 0 && packet.slot as usize <= SLOT_OFFHAND; - // TODO: Handle error + let is_negative = packet.slot < 0; + let valid_slot = packet.slot >= 1 && packet.slot as usize <= 45; let item_stack = packet.clicked_item.to_stack(); - if valid_slot { - self.inventory() - .lock() + let is_legal = + item_stack.is_empty() || item_stack.item_count <= item_stack.get_max_stack_size(); + + if valid_slot && is_legal { + let mut player_screen_handler = self.player_screen_handler.lock().await; + player_screen_handler + .get_slot(packet.slot as usize) .await - .set_slot(packet.slot as usize, Some(item_stack), true)?; - } else { - // Item drop - self.drop_item(item_stack.item.id, u32::from(item_stack.item_count)) + .set_stack(item_stack) .await; + player_screen_handler + .set_received_stack(packet.slot as usize, item_stack) + .await; + player_screen_handler.send_content_updates().await; + } else if is_negative && is_legal { + // Item drop + self.drop_item(item_stack).await; } Ok(()) } @@ -1546,42 +1479,8 @@ impl Player { ); } - // TODO: - // In the future, this function will be used to keep track of if the client is in a valid state. - // However, this is not possible yet. - pub async fn handle_close_container(&self, server: &Server, _packet: SCloseContainer) { - // TODO: This should check if player sent this packet before - // let Some(_window_type) = WindowType::from_i32(packet.window_id.0) else { - // log::info!("Closed ID: {}", packet.window_id.0); - // self.kick(TextComponent::text("Invalid window ID")).await; - // return; - // }; - // window_id 0 represents both 9x1 Generic AND inventory here - let open_container = self.open_container.load(); - if let Some(id) = open_container { - let mut open_containers = server.open_containers.write().await; - if let Some(container) = open_containers.get_mut(&id) { - // If the container contains both a location and a type, run the `on_close` `block_manager` handler - if let Some(pos) = container.get_location() { - if let Some(block) = container.get_block() { - server - .block_registry - .close(&block, self, pos, server, container) //block, self, location, server) - .await; - } - } - // Remove the player from the container - container.remove_player(self.entity_id()); - - let mut inventory = self.inventory().lock().await; - if inventory.state_id >= 2 { - inventory.state_id -= 2; - } else { - inventory.state_id = 0; - } - } - self.open_container.store(None); - } + pub async fn handle_close_container(&self, _server: &Server, _packet: SCloseContainer) { + self.on_handled_screen_closed().await; } pub async fn handle_command_suggestion( diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 7c72e7909..99484cc0d 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -2,7 +2,6 @@ use crate::block::registry::BlockRegistry; use crate::command::commands::default_dispatcher; use crate::command::commands::defaultgamemode::DefaultGamemode; use crate::data::player_server_data::ServerPlayerData; -use crate::entity::EntityId; use crate::item::registry::ItemRegistry; use crate::net::EncryptionError; use crate::plugin::player::player_login::PlayerLoginEvent; @@ -15,20 +14,16 @@ use bytes::Bytes; use connection_cache::{CachedBranding, CachedStatus}; use key_store::KeyStore; use pumpkin_config::{BASIC_CONFIG, advanced_config}; -use pumpkin_data::Block; -use pumpkin_inventory::drag_handler::DragHandler; -use pumpkin_inventory::{Container, OpenContainer}; + use pumpkin_macros::send_cancellable; use pumpkin_protocol::client::login::CEncryptionRequest; use pumpkin_protocol::{ClientPacket, client::config::CPluginMessage}; use pumpkin_registry::{DimensionType, Registry}; -use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector2::Vector2; use pumpkin_util::text::TextComponent; use pumpkin_world::dimension::Dimension; use rand::prelude::SliceRandom; use rsa::RsaPublicKey; -use std::collections::HashMap; use std::net::IpAddr; use std::sync::atomic::{AtomicBool, AtomicU32}; use std::{ @@ -66,10 +61,6 @@ pub struct Server { pub dimensions: Vec, /// Caches game registries for efficient access. pub cached_registry: Vec, - /// Tracks open containers used for item interactions. - // TODO: should have per player open_containers - pub open_containers: RwLock>, - pub drag_handler: DragHandler, /// Assigns unique IDs to containers. container_id: AtomicU32, /// Manages authentication with an authentication server, if enabled. @@ -120,8 +111,6 @@ impl Server { Self { cached_registry: Registry::get_synced(), - open_containers: RwLock::new(HashMap::new()), - drag_handler: DragHandler::new(), container_id: 0.into(), worlds: RwLock::new(vec![Arc::new(world)]), dimensions: vec![ @@ -222,6 +211,7 @@ impl Server { send_cancellable! {{ PlayerLoginEvent::new(player.clone(), TextComponent::text("You have been kicked from the server")); 'after: { + player.screen_handler_sync_handler.store_player(player.clone()).await; if world .add_player(player.gameprofile.id, player.clone()) .await.is_ok() { @@ -264,63 +254,6 @@ impl Server { log::info!("Completed worlds"); } - pub async fn try_get_container( - &self, - player_id: EntityId, - container_id: u64, - ) -> Option>>> { - let open_containers = self.open_containers.read().await; - open_containers - .get(&container_id)? - .try_open(player_id) - .cloned() - } - - /// Returns the first id with a matching location and block type. If this is used with unique - /// blocks, the output will return a random result. - pub async fn get_container_id(&self, location: BlockPos, block: Block) -> Option { - let open_containers = self.open_containers.read().await; - // TODO: do better than brute force - for (id, container) in open_containers.iter() { - if container.is_location(location) { - if let Some(container_block) = container.get_block() { - if container_block.id == block.id { - log::debug!("Found container id: {id}"); - return Some(*id as u32); - } - } - } - } - - drop(open_containers); - - None - } - - pub async fn get_all_container_ids( - &self, - location: BlockPos, - block: Block, - ) -> Option> { - let open_containers = self.open_containers.read().await; - let mut matching_container_ids: Vec = vec![]; - // TODO: do better than brute force - for (id, container) in open_containers.iter() { - if container.is_location(location) { - if let Some(container_block) = container.get_block() { - if container_block.id == block.id { - log::debug!("Found matching container id: {id}"); - matching_container_ids.push(*id as u32); - } - } - } - } - - drop(open_containers); - - Some(matching_container_ids) - } - /// Broadcasts a packet to all players in all worlds. /// /// This function sends the specified packet to every connected player in every world managed by the server. diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 07481311e..bc78470b6 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -768,7 +768,9 @@ impl World { player.has_played_before.store(true, Ordering::Relaxed); player.send_mobs(self).await; - player.send_inventory().await; + player + .on_screen_handler_opened(player.player_screen_handler.clone()) + .await; } pub async fn send_world_info( diff --git a/typos.toml b/typos.toml index 8870be053..01d433e17 100644 --- a/typos.toml +++ b/typos.toml @@ -1,4 +1,6 @@ [files] -extend-exclude = [ - "assets/" -] \ No newline at end of file +extend-exclude = ["assets/"] + +[default.extend-words] +handled = "handled" +received = "received"