Skip to content

Commit

Permalink
Merge pull request #60 from Bryntet/master
Browse files Browse the repository at this point in the history
Add packet SClickContainer and abstractions
  • Loading branch information
Snowiiii authored Sep 8, 2024
2 parents 7b02dcf + 4ed84f2 commit 8b29d9e
Show file tree
Hide file tree
Showing 20 changed files with 1,335 additions and 152 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pumpkin-inventory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
147 changes: 147 additions & 0 deletions pumpkin-inventory/src/container_click.rs
Original file line number Diff line number Diff line change
@@ -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<Self, InventoryError> {
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<Self, InventoryError> {
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<Self, InventoryError> {
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<Self, InventoryError> {
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<Self, InventoryError> {
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<Self, InventoryError> {
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 },
}
175 changes: 175 additions & 0 deletions pumpkin-inventory/src/drag_handler.rs
Original file line number Diff line number Diff line change
@@ -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<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,
maybe_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 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<usize>,
}

impl Drag {
fn possibly_changing_slots<'a>(
&'a self,
slots: &'a [Option<ItemStack>],
carried_item_id: u32,
) -> impl Iterator<Item = usize> + '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),
}
})
}
}
33 changes: 33 additions & 0 deletions pumpkin-inventory/src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Loading

0 comments on commit 8b29d9e

Please sign in to comment.