diff --git a/Cargo.lock b/Cargo.lock index 14caa4d65..1c1cbac2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1862,6 +1862,7 @@ dependencies = [ "digest 0.11.0-pre.9", "hmac", "image", + "itertools 0.13.0", "log", "mio", "num-bigint", @@ -1921,9 +1922,11 @@ version = "0.1.0" name = "pumpkin-inventory" version = "0.1.0" dependencies = [ + "itertools 0.13.0", "num-derive", "num-traits", "pumpkin-world", + "thiserror", ] [[package]] diff --git a/pumpkin-inventory/Cargo.toml b/pumpkin-inventory/Cargo.toml index 08f11b9e3..3b6c8c972 100644 --- a/pumpkin-inventory/Cargo.toml +++ b/pumpkin-inventory/Cargo.toml @@ -9,5 +9,5 @@ pumpkin-world = { path = "../pumpkin-world"} num-traits = "0.2" num-derive = "0.4" - - +thiserror = "1.0.63" +itertools = "0.13.0" \ No newline at end of file diff --git a/pumpkin-inventory/src/container_click.rs b/pumpkin-inventory/src/container_click.rs new file mode 100644 index 000000000..5d1039a9a --- /dev/null +++ b/pumpkin-inventory/src/container_click.rs @@ -0,0 +1,147 @@ +use crate::InventoryError; +use pumpkin_world::item::ItemStack; + +pub struct Click { + pub slot: Slot, + pub click_type: ClickType, +} + +impl Click { + pub fn new(mode: u8, button: i8, slot: i16) -> Result { + match mode { + 0 => Self::new_normal_click(button, slot), + // Both buttons do the same here, so we omit it + 1 => Self::new_shift_click(slot), + 2 => Self::new_key_click(button, slot), + 3 => Ok(Self { + click_type: ClickType::CreativePickItem, + slot: Slot::Normal(slot.try_into().or(Err(InventoryError::InvalidSlot))?), + }), + 4 => Self::new_drop_item(button), + 5 => Self::new_drag_item(button, slot), + 6 => Ok(Self { + click_type: ClickType::DoubleClick, + slot: Slot::Normal(slot.try_into().or(Err(InventoryError::InvalidSlot))?), + }), + _ => Err(InventoryError::InvalidPacket), + } + } + + fn new_normal_click(button: i8, slot: i16) -> Result { + let slot = match slot { + -999 => Slot::OutsideInventory, + _ => { + let slot = slot.try_into().or(Err(InventoryError::InvalidSlot))?; + Slot::Normal(slot) + } + }; + let button = match button { + 0 => MouseClick::Left, + 1 => MouseClick::Right, + _ => Err(InventoryError::InvalidPacket)?, + }; + Ok(Self { + click_type: ClickType::MouseClick(button), + slot, + }) + } + + fn new_shift_click(slot: i16) -> Result { + Ok(Self { + slot: Slot::Normal(slot.try_into().or(Err(InventoryError::InvalidSlot))?), + click_type: ClickType::ShiftClick, + }) + } + + fn new_key_click(button: i8, slot: i16) -> Result { + let key = match button { + 0..9 => KeyClick::Slot(button.try_into().or(Err(InventoryError::InvalidSlot))?), + 40 => KeyClick::Offhand, + _ => Err(InventoryError::InvalidSlot)?, + }; + + Ok(Self { + click_type: ClickType::KeyClick(key), + slot: Slot::Normal(slot.try_into().or(Err(InventoryError::InvalidSlot))?), + }) + } + + fn new_drop_item(button: i8) -> Result { + let drop_type = match button { + 0 => DropType::SingleItem, + 1 => DropType::FullStack, + _ => Err(InventoryError::InvalidPacket)?, + }; + Ok(Self { + click_type: ClickType::DropType(drop_type), + slot: Slot::OutsideInventory, + }) + } + + fn new_drag_item(button: i8, slot: i16) -> Result { + let state = match button { + 0 => MouseDragState::Start(MouseDragType::Left), + 4 => MouseDragState::Start(MouseDragType::Right), + 8 => MouseDragState::Start(MouseDragType::Middle), + 1 | 5 | 9 => { + MouseDragState::AddSlot(slot.try_into().or(Err(InventoryError::InvalidSlot))?) + } + 2 | 6 | 10 => MouseDragState::End, + _ => Err(InventoryError::InvalidPacket)?, + }; + Ok(Self { + slot: match &state { + MouseDragState::AddSlot(slot) => Slot::Normal(*slot), + _ => Slot::OutsideInventory, + }, + click_type: ClickType::MouseDrag { drag_state: state }, + }) + } +} + +pub enum ClickType { + MouseClick(MouseClick), + ShiftClick, + KeyClick(KeyClick), + CreativePickItem, + DropType(DropType), + MouseDrag { drag_state: MouseDragState }, + DoubleClick, +} +#[derive(Debug, PartialEq, Eq)] +pub enum MouseClick { + Left, + Right, +} + +pub enum KeyClick { + Slot(u8), + Offhand, +} +#[derive(Copy, Clone)] +pub enum Slot { + Normal(usize), + OutsideInventory, +} + +pub enum DropType { + SingleItem, + FullStack, +} +#[derive(Debug, PartialEq)] +pub enum MouseDragType { + Left, + Right, + Middle, +} +#[derive(PartialEq)] +pub enum MouseDragState { + Start(MouseDragType), + AddSlot(usize), + End, +} + +pub enum ItemChange { + Remove { slot: usize }, + Add { slot: usize, item: ItemStack }, +} diff --git a/pumpkin-inventory/src/drag_handler.rs b/pumpkin-inventory/src/drag_handler.rs new file mode 100644 index 000000000..11d8b2f82 --- /dev/null +++ b/pumpkin-inventory/src/drag_handler.rs @@ -0,0 +1,175 @@ +use crate::container_click::MouseDragType; +use crate::{Container, InventoryError}; +use itertools::Itertools; +use num_traits::Euclid; +use pumpkin_world::item::ItemStack; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +#[derive(Debug, Default)] +pub struct DragHandler(RwLock>>>); + +impl DragHandler { + pub fn new() -> Self { + Self(RwLock::new(HashMap::new())) + } + pub fn new_drag( + &self, + container_id: u64, + player: i32, + drag_type: MouseDragType, + ) -> Result<(), InventoryError> { + let drag = Drag { + player, + drag_type, + slots: vec![], + }; + let mut drags = match self.0.write() { + Ok(drags) => drags, + Err(_) => Err(InventoryError::LockError)?, + }; + drags.insert(container_id, Arc::new(Mutex::new(drag))); + Ok(()) + } + + pub fn add_slot( + &self, + container_id: u64, + player: i32, + slot: usize, + ) -> Result<(), InventoryError> { + let drags = match self.0.read() { + Ok(drags) => drags, + Err(_) => Err(InventoryError::LockError)?, + }; + match drags.get(&container_id) { + Some(drag) => { + let mut drag = drag.lock().unwrap(); + if drag.player != player { + Err(InventoryError::MultiplePlayersDragging)? + } + if !drag.slots.contains(&slot) { + drag.slots.push(slot); + } + } + None => Err(InventoryError::OutOfOrderDragging)?, + } + Ok(()) + } + + pub fn apply_drag( + &self, + maybe_carried_item: &mut Option, + container: &mut T, + container_id: &u64, + player: i32, + ) -> Result<(), InventoryError> { + // Minecraft client does still send dragging packets when not carrying an item! + if maybe_carried_item.is_none() { + return Ok(()); + } + + let Ok(mut drags) = self.0.write() else { + Err(InventoryError::LockError)? + }; + let Some((_, drag)) = drags.remove_entry(container_id) else { + Err(InventoryError::OutOfOrderDragging)? + }; + let drag = drag.lock().unwrap(); + + if player != drag.player { + Err(InventoryError::MultiplePlayersDragging)? + } + let mut slots = container.all_slots(); + let slots_cloned = slots + .iter() + .map(|stack| stack.map(|item| item.to_owned())) + .collect_vec(); + let Some(carried_item) = maybe_carried_item else { + return Ok(()); + }; + match drag.drag_type { + // This is only valid in Creative GameMode. + // Checked in any function that uses this function. + MouseDragType::Middle => { + for slot in &drag.slots { + *slots[*slot] = *maybe_carried_item; + } + } + MouseDragType::Right => { + let mut single_item = *carried_item; + single_item.item_count = 1; + + let changing_slots = + drag.possibly_changing_slots(&slots_cloned, carried_item.item_id); + changing_slots.for_each(|slot| { + if carried_item.item_count != 0 { + carried_item.item_count -= 1; + if let Some(stack) = &mut slots[slot] { + // TODO: Check for stack max here + if stack.item_count + 1 < 64 { + stack.item_count += 1; + } else { + carried_item.item_count += 1; + } + } else { + *slots[slot] = Some(single_item) + } + } + }); + + if carried_item.item_count == 0 { + *maybe_carried_item = None + } + } + MouseDragType::Left => { + // TODO: Handle dragging a stack with greater amount than item allows as max unstackable + // In that specific case, follow MouseDragType::Right behaviours instead! + + let changing_slots = + drag.possibly_changing_slots(&slots_cloned, carried_item.item_id); + let amount_of_slots = changing_slots.clone().count(); + let (amount_per_slot, remainder) = + (carried_item.item_count as usize).div_rem_euclid(&amount_of_slots); + let mut item_in_each_slot = *carried_item; + item_in_each_slot.item_count = amount_per_slot as u8; + changing_slots.for_each(|slot| *slots[slot] = Some(item_in_each_slot)); + + if remainder > 0 { + carried_item.item_count = remainder as u8; + } else { + *maybe_carried_item = None + } + } + } + Ok(()) + } +} +#[derive(Debug)] +struct Drag { + player: i32, + drag_type: MouseDragType, + slots: Vec, +} + +impl Drag { + fn possibly_changing_slots<'a>( + &'a self, + slots: &'a [Option], + carried_item_id: u32, + ) -> impl Iterator + 'a + Clone { + self.slots.iter().filter_map(move |slot_index| { + let slot = &slots[*slot_index]; + + match slot { + Some(item_slot) => { + if item_slot.item_id == carried_item_id { + Some(*slot_index) + } else { + None + } + } + None => Some(*slot_index), + } + }) + } +} diff --git a/pumpkin-inventory/src/error.rs b/pumpkin-inventory/src/error.rs new file mode 100644 index 000000000..e74ece87e --- /dev/null +++ b/pumpkin-inventory/src/error.rs @@ -0,0 +1,33 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum InventoryError { + #[error("Unable to lock")] + LockError, + #[error("Invalid slot")] + InvalidSlot, + #[error("Player '{0}' tried to interact with a closed container")] + ClosedContainerInteract(i32), + #[error("Multiple players dragging in a container at once")] + MultiplePlayersDragging, + #[error("Out of order dragging")] + OutOfOrderDragging, + #[error("Invalid inventory packet")] + InvalidPacket, + #[error("Player does not have enough permissions")] + PermissionError, +} + +impl InventoryError { + pub fn should_kick(&self) -> bool { + match self { + InventoryError::InvalidSlot + | InventoryError::ClosedContainerInteract(..) + | InventoryError::InvalidPacket + | InventoryError::PermissionError => true, + InventoryError::LockError + | InventoryError::OutOfOrderDragging + | InventoryError::MultiplePlayersDragging => false, + } + } +} diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index dadb10cc6..e1e554fae 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -1,10 +1,20 @@ +use crate::container_click::MouseClick; +use crate::player::PlayerInventory; use num_derive::{FromPrimitive, ToPrimitive}; +use pumpkin_world::item::ItemStack; +pub mod container_click; +pub mod drag_handler; +mod error; +mod open_container; pub mod player; pub mod window_property; +pub use error::InventoryError; +pub use open_container::OpenContainer; + /// https://wiki.vg/Inventory -#[derive(Debug, ToPrimitive, FromPrimitive, Clone)] +#[derive(Debug, ToPrimitive, FromPrimitive, Clone, Copy, Eq, PartialEq)] pub enum WindowType { // not used Generic9x1, @@ -42,13 +52,186 @@ pub enum WindowType { CartographyTable, Stonecutter, } +pub struct ContainerStruct([Option; SLOTS]); + +// Container needs Sync + Send to be able to be in 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, + ) -> Result<(), InventoryError> { + let mut all_slots = self.all_slots(); + if slot > all_slots.len() { + Err(InventoryError::InvalidSlot)? + } + handle_item_change(carried_item, all_slots[slot], mouse_click); + Ok(()) + } + + fn all_slots(&mut self) -> Vec<&mut Option>; + + fn all_slots_ref(&self) -> Vec>; + + fn all_combinable_slots(&self) -> Vec> { + self.all_slots_ref() + } + + fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option> { + self.all_slots() + } + + fn internal_pumpkin_id(&self) -> u64 { + 0 + } +} + +pub fn handle_item_take( + carried_item: &mut Option, + item_slot: &mut Option, + mouse_click: MouseClick, +) { + let Some(item) = item_slot else { + return; + }; + let mut new_item = *item; + + match mouse_click { + MouseClick::Left => { + *item_slot = None; + } + MouseClick::Right => { + let half = item.item_count / 2; + item.item_count -= half; + new_item.item_count = half; + } + } + *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 { + let carried = *carried; + *carried_slot = Some(current.to_owned()); + *current_slot = Some(carried.to_owned()); + } + } + // Put held stack into empty slot + (None, Some(carried)) => match mouse_click { + MouseClick::Left => { + *current_slot = Some(carried.to_owned()); + *carried_slot = None; + } + MouseClick::Right => { + carried.item_count -= 1; + let mut new = *carried; + new.item_count = 1; + *current_slot = Some(new); + } + }, + // 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; + }; + + 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 <= 64 { + 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 - 64; + slot.item_count = 64; + 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 players 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) -> Vec<&mut Option> { + let slots = match &mut self.container { + Some(container) => { + let mut slots = container.all_slots(); + slots.extend(self.inventory.all_combinable_slots_mut()); + slots + } + None => self.inventory.all_slots(), + }; + slots + } -impl WindowType { - pub const fn default_title(&self) -> &'static str { - // TODO: Add titles here: - /*match self { - _ => "WINDOW TITLE", - }*/ - "WINDOW TITLE" + fn all_slots_ref(&self) -> Vec> { + match &self.container { + Some(container) => { + let mut slots = container.all_slots_ref(); + slots.extend(self.inventory.all_combinable_slots()); + slots + } + None => self.inventory.all_slots_ref(), + } } } diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs new file mode 100644 index 000000000..df052755f --- /dev/null +++ b/pumpkin-inventory/src/open_container.rs @@ -0,0 +1,72 @@ +use crate::{Container, WindowType}; +use pumpkin_world::item::ItemStack; +use std::sync::Mutex; + +pub struct OpenContainer { + players: Vec, + container: Mutex>, +} + +impl OpenContainer { + pub fn try_open(&self, player_id: i32) -> Option<&Mutex>> { + if !self.players.contains(&player_id) { + dbg!("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 empty(player_id: i32) -> Self { + Self { + players: vec![player_id], + container: Mutex::new(Box::new(Chest::new())), + } + } + + pub fn all_player_ids(&self) -> Vec { + self.players.clone() + } +} + +struct Chest([Option; 27]); + +impl Chest { + pub fn new() -> Self { + Self([None; 27]) + } +} +impl Container for Chest { + fn window_type(&self) -> &'static WindowType { + &WindowType::Generic9x3 + } + + fn window_name(&self) -> &'static str { + "Chest" + } + fn all_slots(&mut self) -> Vec<&mut Option> { + self.0.iter_mut().collect() + } + + fn all_slots_ref(&self) -> Vec> { + self.0.iter().map(|slot| slot.as_ref()).collect() + } +} diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index 3aa5963a7..d8e127b78 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -1,14 +1,19 @@ -use pumpkin_world::item::Item; +use crate::container_click::MouseClick; +use crate::{handle_item_change, Container, InventoryError, WindowType}; +use pumpkin_world::item::ItemStack; pub struct PlayerInventory { // Main Inventory + Hotbar - crafting: [Option; 4], - crafting_output: Option, - items: [Option; 36], - armor: [Option; 4], - offhand: Option, + crafting: [Option; 4], + crafting_output: Option, + items: [Option; 36], + armor: [Option; 4], + offhand: Option, // current selected slot in hotbar 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: u8, } impl Default for PlayerInventory { @@ -27,9 +32,10 @@ impl PlayerInventory { offhand: None, // TODO: What when player spawns in with an different index ? selected: 0, + state_id: 0, + total_opened_containers: 2, } } - /// Set the contents of an item in a slot /// /// ## Slot @@ -41,61 +47,69 @@ impl PlayerInventory { /// ## 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) { + pub fn set_slot( + &mut self, + slot: usize, + item: Option, + item_allowed_override: bool, + ) -> Result<(), InventoryError> { + if item_allowed_override { + if !(0..=45).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..=45).contains(&slot) { + return Err(InventoryError::InvalidSlot); + } + + Ok(Box::new(match slot { + 0..=4 | 9..=45 => |_| true, + 5 => |item: &ItemStack| item.is_helmet(), + 6 => |item: &ItemStack| item.is_chestplate(), + 7 => |item: &ItemStack| item.is_leggings(), + 8 => |item: &ItemStack| item.is_boots(), + _ => unreachable!(), + })) + } + pub fn get_slot(&mut self, slot: usize) -> Result<&mut Option, InventoryError> { match slot { 0 => { // TODO: Add crafting check here - self.crafting_output = item - } - 1..=4 => self.crafting[slot - 1] = item, - 5..=8 => { - match item { - None => self.armor[slot - 5] = None, - Some(item) => { - // TODO: Replace asserts with error handling - match slot - 5 { - 0 => { - assert!(item.is_helmet() || item_allowed_override); - self.armor[0] = Some(item); - } - 1 => { - assert!(item.is_chestplate() || item_allowed_override); - self.armor[1] = Some(item) - } - 2 => { - assert!(item.is_leggings() || item_allowed_override); - self.armor[2] = Some(item); - } - 3 => { - assert!(item.is_boots() || item_allowed_override); - self.armor[3] = Some(item) - } - _ => unreachable!(), - } - } - } - } - 9..=44 => { - self.items[slot - 9] = item; + Ok(&mut self.crafting_output) } - 45 => { - self.offhand = item; - } - _ => unreachable!(), + 1..=4 => Ok(&mut self.crafting[slot - 1]), + 5..=8 => Ok(&mut self.armor[slot - 5]), + 9..=44 => Ok(&mut self.items[slot - 9]), + 45 => Ok(&mut self.offhand), + _ => Err(InventoryError::InvalidSlot), } } - pub fn set_selected(&mut self, slot: usize) { assert!((0..9).contains(&slot)); self.selected = slot; } - pub fn held_item(&self) -> Option<&Item> { + pub fn held_item(&self) -> Option<&ItemStack> { debug_assert!((0..9).contains(&self.selected)); self.items[self.selected + 36 - 9].as_ref() } - pub fn slots(&self) -> Vec> { + pub fn slots(&self) -> Vec> { 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())); @@ -103,4 +117,58 @@ impl PlayerInventory { slots.push(self.offhand.as_ref()); slots } + + pub fn slots_mut(&mut self) -> Vec<&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 + } +} + +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, + ) -> Result<(), InventoryError> { + let slot_condition = self.slot_condition(slot)?; + let item_slot = self.get_slot(slot)?; + if let Some(item) = carried_slot { + if slot_condition(item) { + handle_item_change(carried_slot, item_slot, mouse_click); + } + } else { + handle_item_change(carried_slot, item_slot, mouse_click) + } + Ok(()) + } + + fn all_slots(&mut self) -> Vec<&mut Option> { + self.slots_mut() + } + + fn all_slots_ref(&self) -> Vec> { + self.slots() + } + + fn all_combinable_slots(&self) -> Vec> { + self.items.iter().map(|item| item.as_ref()).collect() + } + + fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option> { + self.items.iter_mut().collect() + } } diff --git a/pumpkin-protocol/src/server/play/mod.rs b/pumpkin-protocol/src/server/play/mod.rs index 68b7596d5..59a8fc3f7 100644 --- a/pumpkin-protocol/src/server/play/mod.rs +++ b/pumpkin-protocol/src/server/play/mod.rs @@ -1,5 +1,6 @@ mod s_chat_command; mod s_chat_message; +mod s_click_container; mod s_client_information; mod s_close_container; mod s_confirm_teleport; @@ -19,6 +20,7 @@ mod s_use_item_on; pub use s_chat_command::*; pub use s_chat_message::*; +pub use s_click_container::*; pub use s_client_information::*; pub use s_close_container::*; pub use s_confirm_teleport::*; diff --git a/pumpkin-protocol/src/server/play/s_click_container.rs b/pumpkin-protocol/src/server/play/s_click_container.rs new file mode 100644 index 000000000..fd5e56aac --- /dev/null +++ b/pumpkin-protocol/src/server/play/s_click_container.rs @@ -0,0 +1,86 @@ +use crate::slot::Slot; +use crate::VarInt; +use pumpkin_macros::packet; +use serde::de::SeqAccess; +use serde::{de, Deserialize}; + +#[derive(Debug)] +#[packet(0x0E)] +pub struct SClickContainer { + pub window_id: u8, + pub state_id: VarInt, + pub slot: i16, + pub button: i8, + pub mode: VarInt, + pub length_of_array: VarInt, + pub array_of_changed_slots: Vec<(i16, Slot)>, + pub carried_item: Slot, +} + +impl<'de> Deserialize<'de> for SClickContainer { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = SClickContainer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid VarInt encoded in a byte sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let window_id = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode u8"))?; + let state_id = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode VarInt"))?; + + let slot = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode i16"))?; + let button = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode i8"))?; + let mode = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode VarInt"))?; + let length_of_array = seq + .next_element::()? + .ok_or(de::Error::custom("Failed to decode VarInt"))?; + let mut array_of_changed_slots = vec![]; + for _ in 0..length_of_array.0 { + let slot_number = seq + .next_element::()? + .ok_or(de::Error::custom("Unable to parse slot"))?; + let slot = seq + .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::()? + .ok_or(de::Error::custom("Failed to decode carried item"))?; + + Ok(SClickContainer { + window_id, + state_id, + slot, + button, + mode, + length_of_array, + array_of_changed_slots, + carried_item, + }) + } + } + + deserializer.deserialize_seq(Visitor) + } +} diff --git a/pumpkin-protocol/src/slot.rs b/pumpkin-protocol/src/slot.rs index 58386bbe2..671e1d0c5 100644 --- a/pumpkin-protocol/src/slot.rs +++ b/pumpkin-protocol/src/slot.rs @@ -1,5 +1,5 @@ use crate::VarInt; -use pumpkin_world::item::Item; +use pumpkin_world::item::ItemStack; use serde::ser::SerializeSeq; use serde::{ de::{self, SeqAccess}, @@ -129,9 +129,9 @@ impl Serialize for Slot { } impl Slot { - pub fn to_item(self) -> Option { + pub fn to_item(self) -> Option { let item_id = self.item_id?.0.try_into().unwrap(); - Some(Item { + Some(ItemStack { item_id, item_count: self.item_count.0.try_into().unwrap(), }) @@ -149,8 +149,8 @@ impl Slot { } } -impl From<&Item> for Slot { - fn from(item: &Item) -> Self { +impl From<&ItemStack> for Slot { + fn from(item: &ItemStack) -> Self { Slot { item_count: item.item_count.into(), item_id: Some(item.item_id.into()), @@ -163,8 +163,8 @@ impl From<&Item> for Slot { } } -impl From> for Slot { - fn from(item: Option<&Item>) -> Self { +impl From> for Slot { + fn from(item: Option<&ItemStack>) -> Self { item.map(Slot::from).unwrap_or(Slot::empty()) } } diff --git a/pumpkin-world/src/item/item_categories.rs b/pumpkin-world/src/item/item_categories.rs index 6543f348a..c0f8967bc 100644 --- a/pumpkin-world/src/item/item_categories.rs +++ b/pumpkin-world/src/item/item_categories.rs @@ -1,6 +1,6 @@ -use crate::item::Item; +use crate::item::ItemStack; -impl Item { +impl ItemStack { pub fn is_helmet(&self) -> bool { [ // Leather diff --git a/pumpkin-world/src/item/mod.rs b/pumpkin-world/src/item/mod.rs index 65c9dbfe8..8625a9023 100644 --- a/pumpkin-world/src/item/mod.rs +++ b/pumpkin-world/src/item/mod.rs @@ -11,10 +11,16 @@ pub enum Rarity { Epic, } -#[derive(Clone, Copy)] -pub struct Item { - pub item_count: u32, +#[derive(Clone, Copy, Debug)] +pub struct ItemStack { + pub item_count: u8, // This ID is the numerical protocol ID, not the usual minecraft::block ID. pub item_id: u32, // TODO: Add Item Components } + +impl PartialEq for ItemStack { + fn eq(&self, other: &Self) -> bool { + self.item_id == other.item_id + } +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 974474b76..29fa203dc 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -19,6 +19,8 @@ pumpkin-entity = { path = "../pumpkin-entity"} pumpkin-protocol = { path = "../pumpkin-protocol"} pumpkin-registry = { path = "../pumpkin-registry"} +itertools = "0.13.0" + # config serde.workspace = true serde_json = "1.0" diff --git a/pumpkin/src/client/container.rs b/pumpkin/src/client/container.rs index fc9cf1059..69c1c5dd1 100644 --- a/pumpkin/src/client/container.rs +++ b/pumpkin/src/client/container.rs @@ -1,23 +1,30 @@ +use crate::entity::player::Player; +use crate::server::Server; +use itertools::Itertools; use pumpkin_core::text::TextComponent; +use pumpkin_core::GameMode; +use pumpkin_inventory::container_click::{ + Click, ClickType, KeyClick, MouseClick, MouseDragState, MouseDragType, +}; +use pumpkin_inventory::drag_handler::DragHandler; use pumpkin_inventory::window_property::{WindowProperty, WindowPropertyTrait}; -use pumpkin_inventory::WindowType; +use pumpkin_inventory::Container; +use pumpkin_inventory::{container_click, InventoryError, OptionallyCombinedContainer}; use pumpkin_protocol::client::play::{ CCloseContainer, COpenScreen, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, }; +use pumpkin_protocol::server::play::SClickContainer; use pumpkin_protocol::slot::Slot; -use pumpkin_world::item::Item; - -use crate::entity::player::Player; +use pumpkin_world::item::ItemStack; +use std::sync::{Arc, Mutex}; impl Player { - pub fn open_container( - &mut self, - window_type: WindowType, - minecraft_menu_id: &str, - window_title: Option<&str>, - items: Option>>, - carried_item: Option<&Item>, - ) { + pub fn open_container(&mut self, server: &mut Server, minecraft_menu_id: &str) { + self.inventory.state_id = 0; + let total_opened_containers = self.inventory.total_opened_containers; + let mut container = self + .get_open_container(server) + .map(|container| container.lock().unwrap()); let menu_protocol_id = (*pumpkin_world::global_registry::REGISTRY .get("minecraft:menu") .unwrap() @@ -27,78 +34,404 @@ impl Player { .get("protocol_id") .unwrap()) .into(); - let title = TextComponent::text(window_title.unwrap_or(window_type.default_title())); + let window_title = container + .as_ref() + .map(|container| container.window_name()) + .unwrap_or(self.inventory.window_name()); + let title = TextComponent::text(window_title); + self.client.send_packet(&COpenScreen::new( - (window_type.clone() as u8 + 1).into(), + total_opened_containers.into(), menu_protocol_id, title, )); - self.set_container_content(window_type, items, carried_item); + self.set_container_content(container.as_deref_mut()); } - pub fn set_container_content<'a>( - &mut self, - window_type: WindowType, - items: Option>>, - carried_item: Option<&'a Item>, - ) { - let slots: Vec = { - if let Some(mut items) = items { - items.extend(self.inventory.slots()); - items - } else { - self.inventory.slots() - } + pub fn set_container_content(&mut self, container: Option<&mut Box>) { + let total_opened_containers = self.inventory.total_opened_containers; + let container = OptionallyCombinedContainer::new(&mut self.inventory, container); + + let slots = container + .all_slots_ref() .into_iter() - .map(|item| { - if let Some(item) = item { - Slot::from(item) - } else { - Slot::empty() - } - }) - .collect() - }; + .map(Slot::from) + .collect_vec(); let carried_item = { - if let Some(item) = carried_item { + if let Some(item) = self.carried_item.as_ref() { item.into() } else { Slot::empty() } }; - let packet = - CSetContainerContent::new(window_type as u8 + 1, 0.into(), &slots, &carried_item); + self.inventory.state_id += 1; + let packet = CSetContainerContent::new( + total_opened_containers, + (self.inventory.state_id as i32).into(), + &slots, + &carried_item, + ); self.client.send_packet(&packet); } - pub fn set_container_slot( - &mut self, - window_type: WindowType, - slot: usize, - item: Option<&Item>, - ) { - self.client.send_packet(&CSetContainerSlot::new( - window_type as i8, - 0, - slot, - &item.into(), - )) - } - /// The official Minecraft client is weird, and will always just close *any* window that is opened when this gets sent - pub fn close_container(&mut self, window_type: WindowType) { - self.client - .send_packet(&CCloseContainer::new(window_type as u8)) + pub fn close_container(&mut self) { + self.inventory.total_opened_containers += 1; + self.client.send_packet(&CCloseContainer::new( + self.inventory.total_opened_containers, + )) } pub fn set_container_property( &mut self, - window_type: WindowType, window_property: WindowProperty, ) { let (id, value) = window_property.into_tuple(); - self.client - .send_packet(&CSetContainerProperty::new(window_type as u8, id, value)); + self.client.send_packet(&CSetContainerProperty::new( + self.inventory.total_opened_containers, + id, + value, + )); + } + + pub async fn handle_click_container( + &mut self, + server: &mut Server, + packet: SClickContainer, + ) -> Result<(), InventoryError> { + let mut opened_container = self + .get_open_container(server) + .map(|container| container.lock().unwrap()); + let drag_handler = &server.drag_handler; + + let state_id = self.inventory.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()); + return Ok(()); + } + + if opened_container.is_some() { + if packet.window_id != self.inventory.total_opened_containers { + return Err(InventoryError::ClosedContainerInteract(self.entity_id())); + } + } else if packet.window_id != 0 { + return Err(InventoryError::ClosedContainerInteract(self.entity_id())); + } + + let click = Click::new( + packet + .mode + .0 + .try_into() + .expect("Mode can only be between 0-6"), + packet.button, + packet.slot, + )?; + let mut update_whole_container = false; + + match click.click_type { + ClickType::MouseClick(mouse_click) => { + self.mouse_click(opened_container.as_deref_mut(), mouse_click, click.slot) + } + ClickType::ShiftClick => { + self.shift_mouse_click(opened_container.as_deref_mut(), click.slot) + } + ClickType::KeyClick(key_click) => match click.slot { + container_click::Slot::Normal(slot) => { + self.number_button_pressed(opened_container.as_deref_mut(), key_click, slot) + } + container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket), + }, + ClickType::CreativePickItem => { + if let container_click::Slot::Normal(slot) = click.slot { + self.creative_pick_item(opened_container.as_deref_mut(), slot) + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::DoubleClick => { + update_whole_container = true; + if let container_click::Slot::Normal(slot) = click.slot { + self.double_click(opened_container.as_deref_mut(), slot) + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::MouseDrag { drag_state } => { + if drag_state == MouseDragState::End { + update_whole_container = true; + } + self.mouse_drag(drag_handler, opened_container.as_deref_mut(), drag_state) + } + ClickType::DropType(_drop_type) => todo!(), + }?; + 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 combined_container = OptionallyCombinedContainer::new( + &mut self.inventory, + Some(&mut opened_container), + ); + if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) { + let slot = Slot::from(slot); + drop(opened_container); + self.send_container_changes(server, slot_index, slot) + .await?; + } + } + } + Ok(()) + } + + fn mouse_click( + &mut self, + opened_container: Option<&mut Box>, + mouse_click: MouseClick, + slot: container_click::Slot, + ) -> Result<(), InventoryError> { + let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + + match slot { + container_click::Slot::Normal(slot) => { + container.handle_item_change(&mut self.carried_item, slot, mouse_click) + } + container_click::Slot::OutsideInventory => Ok(()), + } + } + + fn shift_mouse_click( + &mut self, + opened_container: Option<&mut Box>, + slot: container_click::Slot, + ) -> Result<(), InventoryError> { + let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + + match slot { + container_click::Slot::Normal(slot) => { + let all_slots = container.all_slots(); + if let Some(item_in_pressed_slot) = all_slots[slot].to_owned() { + let slots = all_slots.into_iter().enumerate(); + // Hotbar + let find_condition = |(slot_number, slot): (usize, &mut Option)| { + // TODO: Check for max item count here + match slot { + Some(item) => { + if item.item_id == item_in_pressed_slot.item_id + && item.item_count != 64 + { + Some(slot_number) + } else { + None + } + } + None => Some(slot_number), + } + }; + + let slots = if slot > 35 { + slots.skip(9).find_map(find_condition) + } else { + slots.skip(36).rev().find_map(find_condition) + }; + if let Some(slot) = slots { + let mut item_slot = container.all_slots()[slot].map(|i| i.to_owned()); + container.handle_item_change(&mut item_slot, slot, MouseClick::Left)?; + *container.all_slots()[slot] = item_slot; + } + } + } + container_click::Slot::OutsideInventory => (), + }; + Ok(()) + } + + fn number_button_pressed( + &mut self, + opened_container: Option<&mut Box>, + key_click: KeyClick, + slot: usize, + ) -> Result<(), InventoryError> { + let changing_slot = match key_click { + KeyClick::Slot(slot) => slot, + KeyClick::Offhand => 45, + }; + let mut changing_item_slot = self.inventory.get_slot(changing_slot as usize)?.to_owned(); + let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + + container.handle_item_change(&mut changing_item_slot, slot, MouseClick::Left)?; + *self.inventory.get_slot(changing_slot as usize)? = changing_item_slot; + Ok(()) + } + + fn creative_pick_item( + &mut self, + opened_container: Option<&mut Box>, + slot: usize, + ) -> Result<(), InventoryError> { + if self.gamemode != GameMode::Creative { + return Err(InventoryError::PermissionError); + } + let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + if let Some(Some(item)) = container.all_slots().get_mut(slot) { + self.carried_item = Some(item.to_owned()) + } + Ok(()) + } + + fn double_click( + &mut self, + opened_container: Option<&mut Box>, + slot: usize, + ) -> Result<(), InventoryError> { + let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut slots = container.all_slots(); + + let Some(item) = slots.get_mut(slot) else { + return Ok(()); + }; + let Some(mut carried_item) = **item else { + return Ok(()); + }; + **item = None; + + for slot in slots.iter_mut().filter_map(|slot| slot.as_mut()) { + if slot.item_id == carried_item.item_id { + // TODO: Check for max stack size + if slot.item_count + carried_item.item_count <= 64 { + slot.item_count = 0; + carried_item.item_count = 64; + } else { + let to_remove = slot.item_count - (64 - carried_item.item_count); + slot.item_count -= to_remove; + carried_item.item_count += to_remove; + } + + if carried_item.item_count == 64 { + break; + } + } + } + self.carried_item = Some(carried_item); + Ok(()) + } + + fn mouse_drag( + &mut 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(|container| container.internal_pumpkin_id()) + .unwrap_or(player_id as u64); + match mouse_drag_state { + MouseDragState::Start(drag_type) => { + if drag_type == MouseDragType::Middle && self.gamemode != GameMode::Creative { + Err(InventoryError::PermissionError)? + } + drag_handler.new_drag(container_id, player_id, drag_type) + } + MouseDragState::AddSlot(slot) => drag_handler.add_slot(container_id, player_id, slot), + MouseDragState::End => { + let mut container = + OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + drag_handler.apply_drag( + &mut self.carried_item, + &mut container, + &container_id, + player_id, + ) + } + } + } + + async fn get_current_players_in_container( + &mut self, + server: &Server, + ) -> Vec>> { + let player_ids = server + .open_containers + .get(&self.open_container.unwrap()) + .unwrap() + .all_player_ids() + .into_iter() + .filter(|player_id| *player_id != self.entity_id()) + .collect_vec(); + let player_token = self.client.token; + + // 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 + + let world = self.world.lock().await; + let players = world + .current_players + .iter() + .filter_map(|(token, player)| { + if *token != player_token { + let entity_id = player.lock().unwrap().entity_id(); + if player_ids.contains(&entity_id) { + Some(player.clone()) + } else { + None + } + } else { + None + } + }) + .collect_vec(); + players + } + + async fn send_container_changes( + &mut self, + server: &Server, + slot_index: usize, + slot: Slot, + ) -> Result<(), InventoryError> { + for player in self.get_current_players_in_container(server).await { + let mut player = player.lock().unwrap(); + let total_opened_containers = player.inventory.total_opened_containers; + + player.inventory.state_id += 1; + let packet = CSetContainerSlot::new( + total_opened_containers as i8, + player.inventory.state_id as i32, + slot_index, + &slot, + ); + player.client.send_packet(&packet); + } + Ok(()) + } + + async fn send_whole_container_change(&mut self, server: &Server) -> Result<(), InventoryError> { + let players = self.get_current_players_in_container(server).await; + + for player in players { + let mut player = player.lock().unwrap(); + let mut container = player + .get_open_container(server) + .map(|container| container.lock().unwrap()); + player.set_container_content(container.as_deref_mut()); + drop(container) + } + Ok(()) + } + + pub fn get_open_container<'a>( + &self, + server: &'a Server, + ) -> Option<&'a Mutex>> { + if let Some(id) = self.open_container { + server.try_get_container(self.entity_id(), id) + } else { + None + } } } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 2bdc9cd16..1a2c872c8 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -14,7 +14,7 @@ use pumpkin_core::{ GameMode, }; use pumpkin_entity::EntityId; -use pumpkin_inventory::WindowType; +use pumpkin_inventory::{InventoryError, WindowType}; use pumpkin_protocol::server::play::{SCloseContainer, SSetPlayerGround, SUseItem}; use pumpkin_protocol::{ client::play::{ @@ -552,22 +552,30 @@ impl Player { self.inventory.set_selected(slot as usize); } - pub fn handle_set_creative_slot(&mut self, _server: &mut Server, packet: SSetCreativeSlot) { + pub fn handle_set_creative_slot( + &mut self, + _server: &mut Server, + packet: SSetCreativeSlot, + ) -> Result<(), InventoryError> { if self.gamemode != GameMode::Creative { - self.kick(TextComponent::text( - "Invalid action, you can only do that if you are in creative", - )); - return; + return Err(InventoryError::PermissionError); } self.inventory - .set_slot(packet.slot as usize, packet.clicked_item.to_item(), false); + .set_slot(packet.slot as usize, packet.clicked_item.to_item(), false) } // TODO: // This function will in the future be used to keep track of if the client is in a valid state. // But this is not possible yet - pub fn handle_close_container(&mut self, _server: &mut Server, packet: SCloseContainer) { + pub fn handle_close_container(&mut self, server: &mut Server, packet: SCloseContainer) { // window_id 0 represents both 9x1 Generic AND inventory here + self.inventory.state_id = 0; + if let Some(id) = self.open_container { + if let Some(container) = server.open_containers.get_mut(&id) { + container.remove_player(self.entity_id()) + } + self.open_container = None; + } let Some(_window_type) = WindowType::from_u8(packet.window_id) else { self.kick(TextComponent::text("Invalid window ID")); return; diff --git a/pumpkin/src/commands/cmd_echest.rs b/pumpkin/src/commands/cmd_echest.rs new file mode 100644 index 000000000..d082ad1d3 --- /dev/null +++ b/pumpkin/src/commands/cmd_echest.rs @@ -0,0 +1,29 @@ +use pumpkin_inventory::OpenContainer; + +use crate::commands::tree::CommandTree; + +const NAMES: [&str; 2] = ["echest", "enderchest"]; + +const DESCRIPTION: &str = + "Show your personal enderchest (this command is used for testing container behaviour)"; + +pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, server, _| { + if let Some(player) = sender.as_mut_player() { + let entity_id = player.entity_id(); + player.open_container = Some(0); + match server.open_containers.get_mut(&0) { + Some(ender_chest) => { + ender_chest.add_player(entity_id); + } + None => { + let open_container = OpenContainer::empty(entity_id); + server.open_containers.insert(0, open_container); + } + } + player.open_container(server, "minecraft:generic_9x3"); + } + + Ok(()) + }) +} diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 4600ffbe9..f03a65014 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -3,6 +3,7 @@ use pumpkin_core::text::TextComponent; use crate::commands::dispatcher::CommandDispatcher; use crate::entity::player::Player; mod arg_player; +mod cmd_echest; mod cmd_gamemode; mod cmd_help; mod cmd_pumpkin; @@ -68,6 +69,7 @@ pub fn default_dispatcher<'a>() -> CommandDispatcher<'a> { dispatcher.register(cmd_gamemode::init_command_tree()); dispatcher.register(cmd_stop::init_command_tree()); dispatcher.register(cmd_help::init_command_tree()); + dispatcher.register(cmd_echest::init_command_tree()); dispatcher } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index d743707b8..647333c83 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -16,14 +16,17 @@ use pumpkin_protocol::{ CSystemChatMessage, PlayerAction, }, server::play::{ - SChatCommand, SChatMessage, SClientInformationPlay, SConfirmTeleport, SInteract, - SPlayPingRequest, SPlayerAction, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, - SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem, - SUseItemOn, + SChatCommand, SChatMessage, SClickContainer, SClientInformationPlay, SConfirmTeleport, + SInteract, SPlayPingRequest, SPlayerAction, SPlayerCommand, SPlayerPosition, + SPlayerPositionRotation, SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, + SSwingArm, SUseItem, SUseItemOn, }, ConnectionState, RawPacket, ServerPacket, VarInt, }; +use pumpkin_protocol::server::play::SCloseContainer; +use pumpkin_world::item::ItemStack; + use crate::{ client::{authentication::GameProfile, Client, PlayerConfig}, server::Server, @@ -67,6 +70,9 @@ pub struct Player { pub food: i32, pub food_saturation: f32, pub inventory: PlayerInventory, + pub open_container: Option, + pub carried_item: Option, + /// send `send_abilties_update` when changed pub abilities: PlayerAbilities, pub last_position: Vector3, @@ -113,6 +119,8 @@ impl Player { food_saturation: 20.0, current_block_destroy_stage: 0, inventory: PlayerInventory::new(), + open_container: None, + carried_item: None, teleport_id_count: 0, abilities: PlayerAbilities::default(), gamemode, @@ -345,13 +353,24 @@ impl Player { Ok(()) } SSetCreativeSlot::PACKET_ID => { - self.handle_set_creative_slot(server, SSetCreativeSlot::read(bytebuf)?); + self.handle_set_creative_slot(server, SSetCreativeSlot::read(bytebuf)?) + .unwrap(); Ok(()) } SPlayPingRequest::PACKET_ID => { self.handle_play_ping_request(server, SPlayPingRequest::read(bytebuf)?); Ok(()) } + SClickContainer::PACKET_ID => { + self.handle_click_container(server, SClickContainer::read(bytebuf)?) + .await + .unwrap(); + Ok(()) + } + SCloseContainer::PACKET_ID => { + self.handle_close_container(server, SCloseContainer::read(bytebuf)?); + Ok(()) + } _ => { log::error!("Failed to handle player packet id {:#04x}", packet.id.0); Ok(()) diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 9f3721e1c..0216d7a62 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -1,13 +1,3 @@ -use std::{ - io::Cursor, - path::Path, - sync::{ - atomic::{AtomicI32, Ordering}, - Arc, Mutex, - }, - time::Duration, -}; - use base64::{engine::general_purpose, Engine}; use image::GenericImageView; use mio::Token; @@ -20,7 +10,19 @@ use pumpkin_protocol::{ CURRENT_MC_PROTOCOL, }; use pumpkin_world::dimension::Dimension; +use std::collections::HashMap; +use std::{ + io::Cursor, + path::Path, + sync::{ + atomic::{AtomicI32, Ordering}, + Arc, Mutex, + }, + time::Duration, +}; +use pumpkin_inventory::drag_handler::DragHandler; +use pumpkin_inventory::{Container, OpenContainer}; use pumpkin_registry::Registry; use rsa::{traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey}; @@ -54,6 +56,8 @@ pub struct Server { /// Cache the registry so we don't have to parse it every time a player joins pub cached_registry: Vec, + pub open_containers: HashMap, + pub drag_handler: DragHandler, entity_id: AtomicI32, /// Used for Authentication, None is Online mode is disabled @@ -100,6 +104,8 @@ impl Server { Self { plugin_loader, cached_registry: Registry::get_static(), + open_containers: HashMap::new(), + drag_handler: DragHandler::new(), // 0 is invalid entity_id: 2.into(), worlds: vec![Arc::new(tokio::sync::Mutex::new(world))], @@ -127,6 +133,7 @@ impl Server { // Basicly the default world // TODO: select default from config let world = self.worlds[0].clone(); + let player = Arc::new(Mutex::new(Player::new( client, world.clone(), @@ -137,6 +144,14 @@ impl Server { (player, world) } + pub fn try_get_container( + &self, + player_id: EntityId, + container_id: u64, + ) -> Option<&Mutex>> { + self.open_containers.get(&container_id)?.try_open(player_id) + } + /// Sends a Packet to all Players in all worlds pub fn broadcast_packet_all

(&self, except: &[Token], packet: &P) where