add drag and error handling for containers

This commit is contained in:
Edvin Bryntesson 2024-09-05 14:54:28 +02:00
parent 28292439de
commit bb6677dd19
No known key found for this signature in database
11 changed files with 367 additions and 134 deletions

2
Cargo.lock generated
View File

@ -1967,9 +1967,11 @@ dependencies = [
name = "pumpkin-inventory"
version = "0.1.0"
dependencies = [
"itertools 0.13.0",
"num-derive",
"num-traits",
"pumpkin-world",
"thiserror",
]
[[package]]

View File

@ -6,6 +6,7 @@ edition.workspace = true
[dependencies]
num-traits = "0.2"
num-derive = "0.4"
thiserror = "1.0.63"
itertools = "0.13.0"
# For items
pumpkin-world = { path = "../pumpkin-world"}

View File

@ -83,63 +83,21 @@ impl Click {
}
fn new_drag_item(button: i8, slot: i16) -> Self {
let (mouse_type, state, slot) = match button {
0 => (
MouseDragType::Left,
MouseDragState::Start,
Slot::OutsideInventory,
),
1 => (
MouseDragType::Left,
MouseDragState::AddSlot,
Slot::Normal(slot.try_into().unwrap()),
),
2 => (
MouseDragType::Left,
MouseDragState::End,
Slot::OutsideInventory,
),
4 => (
MouseDragType::Right,
MouseDragState::Start,
Slot::OutsideInventory,
),
5 => (
MouseDragType::Right,
MouseDragState::AddSlot,
Slot::Normal(slot.try_into().unwrap()),
),
6 => (
MouseDragType::Right,
MouseDragState::End,
Slot::OutsideInventory,
),
// ONLY FOR CREATIVE
8 => (
MouseDragType::Middle,
MouseDragState::Start,
Slot::OutsideInventory,
),
9 => (
MouseDragType::Middle,
MouseDragState::AddSlot,
Slot::Normal(slot.try_into().unwrap()),
),
10 => (
MouseDragType::Middle,
MouseDragState::End,
Slot::OutsideInventory,
),
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().unwrap()),
2 | 6 | 10 => MouseDragState::End,
// TODO: Error handling
_ => unreachable!(),
};
Self {
click_type: ClickType::MouseDrag {
drag_state: state,
drag_type: mouse_type,
slot: match &state {
MouseDragState::AddSlot(slot) => Slot::Normal(*slot),
_ => Slot::OutsideInventory,
},
slot,
click_type: ClickType::MouseDrag { drag_state: state },
}
}
}
@ -150,10 +108,7 @@ pub enum ClickType {
KeyClick(KeyClick),
CreativePickItem,
DropType(DropType),
MouseDrag {
drag_state: MouseDragState,
drag_type: MouseDragType,
},
MouseDrag { drag_state: MouseDragState },
DoubleClick,
}
#[derive(Debug, PartialEq, Eq)]
@ -176,7 +131,7 @@ pub enum DropType {
SingleItem,
FullStack,
}
#[derive(Debug)]
pub enum MouseDragType {
Left,
Right,
@ -184,8 +139,8 @@ pub enum MouseDragType {
}
pub enum MouseDragState {
Start,
AddSlot,
Start(MouseDragType),
AddSlot(usize),
End,
}

View File

@ -0,0 +1,178 @@
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::iter::{Enumerate, Filter, TakeWhile};
use std::sync::{Arc, Mutex, RwLock};
use std::vec::IntoIter;
#[derive(Debug, Default)]
pub struct DragHandler(RwLock<HashMap<u64, Arc<Mutex<Drag>>>>);
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<T: Container>(
&self,
carried_item: &mut Option<ItemStack>,
container: &mut T,
container_id: &u64,
player: i32,
) -> Result<(), InventoryError> {
// Minecraft client does still send dragging packets when not carrying an item!
if 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 mut 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();
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] = carried_item.clone();
}
}
MouseDragType::Right => {
let amount_of_items = carried_item.unwrap().item_count as usize;
let mut single_item = carried_item.clone().unwrap();
single_item.item_count = 1;
let changing_slots = drag.changing_slots(
amount_of_items,
&slots_cloned,
carried_item.as_ref().unwrap(),
);
let mut amount_removed = 0;
changing_slots.for_each(|slot| {
amount_removed += 1;
*slots[slot] = Some(single_item.clone())
});
let mut remaining = carried_item.unwrap();
if remaining.item_count == amount_removed {
*carried_item = None
} else {
remaining.item_count -= amount_removed;
*carried_item = Some(remaining)
}
}
MouseDragType::Left => {
let amount_of_items = carried_item.unwrap().item_count as usize;
// 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.changing_slots(
amount_of_items,
&slots_cloned,
carried_item.as_ref().unwrap(),
);
let amount_of_slots = changing_slots.clone().count();
let (amount_per_slot, remainder) =
(carried_item.unwrap().item_count as usize).div_rem_euclid(&amount_of_slots);
let mut item_in_each_slot = carried_item.unwrap().clone();
item_in_each_slot.item_count = amount_per_slot as u8;
changing_slots.for_each(|slot| *slots[slot] = Some(item_in_each_slot.clone()));
if remainder > 0 {
let mut remaining = carried_item.unwrap().clone();
remaining.item_count = remainder as u8;
*carried_item = Some(remaining)
} else {
*carried_item = None
}
}
}
Ok(())
}
}
#[derive(Debug)]
struct Drag {
player: i32,
drag_type: MouseDragType,
slots: Vec<usize>,
}
impl Drag {
fn changing_slots<'a>(
&'a self,
amount_of_items: usize,
slots: &'a [Option<ItemStack>],
carried_item: &'a ItemStack,
) -> impl Iterator<Item = usize> + 'a + Clone {
self.slots
.iter()
.enumerate()
.take_while(move |(slot_number, _)| *slot_number <= amount_of_items)
.filter_map(move |(_, slot)| match &slots[*slot] {
Some(item_slot) => {
if *item_slot == *carried_item {
Some(*slot)
} else {
None
}
}
None => Some(*slot),
})
}
}

View File

@ -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,
}
}
}

View File

@ -1,13 +1,16 @@
use crate::container_click::MouseClick;
use crate::container_click::{MouseClick, MouseDragState, MouseDragType};
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
@ -68,8 +71,13 @@ pub trait Container: Sync + Send {
carried_item: &mut Option<ItemStack>,
slot: usize,
mouse_click: MouseClick,
) {
handle_item_change(carried_item, self.all_slots()[slot], mouse_click)
) -> 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<ItemStack>>;
@ -98,6 +106,10 @@ pub trait Container: Sync + Send {
dbg!(slot, item);
});
}
fn internal_pumpkin_id(&self) -> u64 {
0
}
}
pub fn handle_item_take(

View File

@ -1,6 +1,7 @@
use crate::drag_handler::DragHandler;
use crate::{Container, WindowType};
use pumpkin_world::item::ItemStack;
use std::sync::{Mutex, MutexGuard};
use std::sync::{Arc, Mutex};
pub struct OpenContainer {
players: Vec<i32>,
@ -8,21 +9,12 @@ pub struct OpenContainer {
}
impl OpenContainer {
pub fn try_open(&self, player_id: i32) -> Option<MutexGuard<Box<dyn Container>>> {
pub fn try_open(&self, player_id: i32) -> Option<&Mutex<Box<dyn Container>>> {
if !self.players.contains(&player_id) {
dbg!("couldn't open container");
return None;
}
let container = self.container.lock().unwrap();
container
.all_slots_ref()
.iter()
.enumerate()
.for_each(|(slot, item)| {
if let Some(item) = item {
dbg!(slot, item);
}
});
let container = &self.container;
Some(container)
}

View File

@ -1,5 +1,6 @@
use crate::container_click::MouseClick;
use crate::{handle_item_change, Container, WindowType};
use crate::drag_handler::DragHandler;
use crate::{handle_item_change, Container, InventoryError, WindowType};
use pumpkin_world::item::ItemStack;
#[allow(dead_code)]
@ -92,17 +93,17 @@ impl PlayerInventory {
_ => unreachable!(),
}
}
pub fn get_slot(&mut self, slot: usize) -> &mut Option<ItemStack> {
pub fn get_slot(&mut self, slot: usize) -> Result<&mut Option<ItemStack>, InventoryError> {
match slot {
0 => {
// TODO: Add crafting check here
&mut self.crafting_output
Ok(&mut self.crafting_output)
}
1..=4 => &mut self.crafting[slot - 1],
5..=8 => &mut self.armor[slot - 5],
9..=44 => &mut self.items[slot - 9],
45 => &mut self.offhand,
_ => 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) {
@ -144,9 +145,11 @@ impl Container for PlayerInventory {
carried_slot: &mut Option<ItemStack>,
slot: usize,
mouse_click: MouseClick,
) {
let item_slot = self.get_slot(slot);
handle_item_change(carried_slot, item_slot, mouse_click)
) -> Result<(), InventoryError> {
let item_slot = self.get_slot(slot)?;
handle_item_change(carried_slot, item_slot, mouse_click);
Ok(())
}
fn all_slots(&mut self) -> Vec<&mut Option<ItemStack>> {

View File

@ -18,3 +18,9 @@ pub struct ItemStack {
pub item_id: u32,
// TODO: Add Item Components
}
impl PartialEq for ItemStack {
fn eq(&self, other: &Self) -> bool {
self.item_id == other.item_id
}
}

View File

@ -1,8 +1,12 @@
use crate::entity::player::Player;
use crate::server::Server;
use itertools::Itertools;
use pumpkin_core::text::TextComponent;
use pumpkin_inventory::container_click::{KeyClick, MouseClick};
use pumpkin_core::GameMode;
use pumpkin_inventory::container_click::{KeyClick, MouseClick, MouseDragState, MouseDragType};
use pumpkin_inventory::drag_handler::DragHandler;
use pumpkin_inventory::window_property::{WindowProperty, WindowPropertyTrait};
use pumpkin_inventory::{container_click, OptionallyCombinedContainer};
use pumpkin_inventory::{container_click, InventoryError, OptionallyCombinedContainer};
use pumpkin_inventory::{Container, WindowType};
use pumpkin_protocol::client::play::{
CCloseContainer, COpenScreen, CSetContainerContent, CSetContainerProperty, CSetContainerSlot,
@ -10,10 +14,7 @@ use pumpkin_protocol::client::play::{
use pumpkin_protocol::server::play::SClickContainer;
use pumpkin_protocol::slot::Slot;
use pumpkin_world::item::ItemStack;
use std::sync::MutexGuard;
use crate::entity::player::Player;
use crate::server::Server;
use std::sync::{Arc, Mutex, MutexGuard};
impl Player {
pub fn open_container(
@ -22,8 +23,11 @@ impl Player {
minecraft_menu_id: &str,
window_title: Option<&str>,
) {
let mut container = self.get_open_container(server);
let container = container.as_deref_mut();
self.inventory.reset_state_id();
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()
@ -40,14 +44,12 @@ impl Player {
.to_owned();
let title = TextComponent::text(window_title.unwrap_or(window_type.default_title()));
self.inventory.reset_state_id();
self.client.send_packet(&COpenScreen::new(
self.inventory.total_opened_containers.into(),
total_opened_containers.into(),
menu_protocol_id,
title,
));
self.set_container_content(container);
self.set_container_content(container.as_deref_mut());
}
pub fn set_container_content(&mut self, container: Option<&mut Box<dyn Container>>) {
@ -113,33 +115,35 @@ impl Player {
));
}
pub fn handle_click_container(&mut self, server: &mut Server, packet: SClickContainer) {
pub fn handle_click_container(
&mut self,
server: &mut Server,
packet: SClickContainer,
) -> Result<(), InventoryError> {
use container_click::*;
let mut opened_container = self.get_open_container(server);
let opened_container = opened_container.as_deref_mut();
let mut opened_container = self
.get_open_container(server)
.map(|container| container.lock().unwrap());
let mut opened_container = opened_container.as_deref_mut();
let drag_handler = &server.drag_handler;
let current_state_id = if let Some(container) = opened_container.as_ref() {
container.state_id()
} else {
self.inventory.state_id()
};
// This is just checking for regular desync, client hasn't done anything malicious
if current_state_id != packet.state_id.0 {
dbg!(current_state_id, packet.state_id.0);
//self.set_container_content(opened_container.as_deref_mut());
//return;
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 {
dbg!(
self.inventory.total_opened_containers,
opened_container.unwrap().window_type()
);
return;
return Err(InventoryError::ClosedContainerInteract(self.entity_id()));
}
} else if packet.window_id != 0 {
dbg!("weird");
return;
return Err(InventoryError::ClosedContainerInteract(self.entity_id()));
}
let click = Click::new(
@ -160,26 +164,27 @@ impl Player {
container_click::Slot::Normal(slot) => {
self.number_button_pressed(opened_container, key_click, slot)
}
container_click::Slot::OutsideInventory => {
unimplemented!("This is not a valid state")
}
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)
} else {
Err(InventoryError::InvalidPacket)
}
}
ClickType::DoubleClick => {
if let container_click::Slot::Normal(slot) = click.slot {
self.double_click(slot)
} else {
Err(InventoryError::InvalidPacket)
}
}
ClickType::MouseDrag {
drag_state: _,
drag_type: _,
} => (),
ClickType::DropType(_drop_type) => (),
};
ClickType::MouseDrag { drag_state } => {
self.mouse_drag(drag_handler, opened_container, drag_state)
}
ClickType::DropType(_drop_type) => todo!(),
}
}
fn mouse_click(
@ -187,22 +192,22 @@ impl Player {
opened_container: Option<&mut Box<dyn Container>>,
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 => (),
};
container_click::Slot::OutsideInventory => todo!(),
}
}
fn shift_mouse_click(
&mut self,
opened_container: Option<&mut Box<dyn Container>>,
slot: container_click::Slot,
) {
) -> Result<(), InventoryError> {
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
match slot {
@ -235,13 +240,14 @@ impl Player {
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.handle_item_change(&mut item_slot, slot, MouseClick::Left)?;
*container.all_slots()[slot] = item_slot;
}
}
}
container_click::Slot::OutsideInventory => (),
};
Ok(())
}
fn number_button_pressed(
@ -249,35 +255,74 @@ impl Player {
opened_container: Option<&mut Box<dyn Container>>,
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 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
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<dyn Container>>,
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, _slot: usize) {}
fn double_click(&mut self, _slot: usize) -> Result<(), InventoryError> {
Ok(())
}
fn mouse_drag(
&mut self,
drag_handler: &DragHandler,
opened_container: Option<&mut Box<dyn Container>>,
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) => {
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,
)?
}
}
Ok(())
}
pub fn get_open_container<'a>(
&self,
server: &'a Server,
) -> Option<MutexGuard<'a, Box<dyn Container>>> {
) -> Option<&'a Mutex<Box<dyn Container>>> {
if let Some(id) = self.open_container {
server.try_get_container(self.entity_id(), id)
} else {

View File

@ -22,6 +22,7 @@ use std::{
};
use crate::{client::Client, entity::player::Player, world::World};
use pumpkin_inventory::drag_handler::DragHandler;
use pumpkin_inventory::{Container, OpenContainer};
use pumpkin_registry::Registry;
use rsa::{traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey};
@ -48,6 +49,7 @@ pub struct Server {
pub cached_registry: Vec<Registry>,
pub open_containers: HashMap<u64, OpenContainer>,
pub drag_handler: DragHandler,
entity_id: AtomicI32,
/// Used for Authentication, None is Online mode is disabled
@ -94,6 +96,7 @@ impl Server {
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))],
@ -134,8 +137,11 @@ impl Server {
&self,
player_id: EntityId,
container_id: u64,
) -> Option<MutexGuard<'_, Box<dyn Container>>> {
self.open_containers.get(&container_id)?.try_open(player_id)
) -> Option<&Mutex<Box<dyn Container>>> {
self.open_containers
.get(&container_id)?
.try_open(player_id)
.map(|container| container)
}
/// Sends a Packet to all Players in all worlds