Remove non-vanilla Slot and replace with ItemStackSeralizer (#726)

* replace slot with ItemStackSeralizer

* Fix ItemStack

* Fix pickup and remove faulty crafting (getting reworked in inv revamp)

* re-add component check

---------

Co-authored-by: kralverde <github@email.kralverde.dev>
This commit is contained in:
4lve
2025-04-12 11:28:36 +02:00
committed by GitHub
parent 77846fc7d0
commit 7180ea69ee
21 changed files with 238 additions and 1057 deletions

View File

@@ -1,103 +0,0 @@
use pumpkin_data::{
item::Item,
tag::{RegistryKey, get_tag_values},
};
use pumpkin_registry::{RECIPES, RecipeResult, flatten_3x3};
use pumpkin_util::registry::{RegistryEntryList, TagType};
use pumpkin_world::item::ItemStack;
use rayon::prelude::*;
#[inline(always)]
fn check_ingredient_type(ingredient_type: &TagType, input: &ItemStack) -> bool {
match ingredient_type {
TagType::Tag(tag) => {
let _items = match get_tag_values(RegistryKey::Item, tag) {
Some(items) => items,
None => return false,
};
// TODO
false
// items.iter().any(|tag| check_ingredient_type(&tag, input))
}
TagType::Item(item) => {
Item::from_registry_key(item).is_some_and(|item| item.id == input.item.id)
}
}
}
pub fn check_if_matches_crafting(input: [[Option<&ItemStack>; 3]; 3]) -> Option<ItemStack> {
let input = flatten_3x3(input);
RECIPES
.par_iter()
.find_any(|recipe| {
let patterns = recipe.pattern();
if patterns
.iter()
.flatten()
.flatten()
.all(|slot| slot.is_none())
{
false
} else if recipe.recipe_type.is_shapeless() {
shapeless_crafting_match(input, recipe.pattern())
} else {
patterns.par_iter().any(|pattern| {
pattern.iter().enumerate().all(|(i, row)| {
row.iter()
.enumerate()
.all(|(j, item)| match (item, input[i][j]) {
(Some(item), Some(input)) => ingredient_slot_check(item, input),
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
})
})
})
}
})
.map(|recipe| match recipe.result() {
RecipeResult::Single { id, .. } => Some(ItemStack {
item: Item::from_registry_key(id).unwrap(),
item_count: 1,
}),
RecipeResult::Many { id, count, .. } => Some(ItemStack {
item: Item::from_registry_key(id).unwrap(),
item_count: *count,
}),
RecipeResult::Special => None,
})?
}
fn ingredient_slot_check(recipe_item: &RegistryEntryList, input: &ItemStack) -> bool {
match recipe_item {
RegistryEntryList::Single(ingredient) => check_ingredient_type(ingredient, input),
RegistryEntryList::Many(ingredients) => ingredients
.iter()
.any(|ingredient| check_ingredient_type(ingredient, input)),
}
}
fn shapeless_crafting_match(
input: [[Option<&ItemStack>; 3]; 3],
pattern: &[[[Option<RegistryEntryList>; 3]; 3]],
) -> bool {
let mut pattern: Vec<RegistryEntryList> = pattern
.iter()
.flatten()
.flatten()
.flatten()
.cloned()
.collect();
for item in input.into_iter().flatten().flatten() {
if let Some(index) = pattern.iter().enumerate().find_map(|(i, recipe_item)| {
if ingredient_slot_check(recipe_item, item) {
Some(i)
} else {
None
}
}) {
pattern.remove(index);
} else {
return false;
}
}
pattern.is_empty()
}

View File

@@ -4,7 +4,6 @@ use pumpkin_data::screen::WindowType;
use pumpkin_world::item::ItemStack;
pub mod container_click;
mod crafting;
pub mod drag_handler;
mod error;
mod open_container;

View File

@@ -1,5 +1,4 @@
use crate::Container;
use crate::crafting::check_if_matches_crafting;
use pumpkin_data::block::Block;
use pumpkin_data::screen::WindowType;
use pumpkin_util::math::position::BlockPos;
@@ -162,7 +161,7 @@ impl Container for CraftingTable {
fn craft(&mut self) -> bool {
// TODO: Is there a better way to do this?
let check = [
let _check = [
[
self.input[0][0].as_ref(),
self.input[0][1].as_ref(),
@@ -180,7 +179,7 @@ impl Container for CraftingTable {
],
];
let new_output = check_if_matches_crafting(check);
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();

View File

@@ -1,5 +1,4 @@
use crate::container_click::MouseClick;
use crate::crafting::check_if_matches_crafting;
use crate::{Container, InventoryError, WindowType, handle_item_change};
use pumpkin_data::item::Item;
use pumpkin_world::item::ItemStack;
@@ -366,9 +365,9 @@ impl Container for PlayerInventory {
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];
let _together = [v1, v2, v3];
self.crafting_output = check_if_matches_crafting(together);
self.crafting_output = None; //check_if_matches_crafting(together);
self.crafting.iter().any(|s| s.is_some())
}

View File

@@ -1,5 +1,5 @@
use crate::VarInt;
use crate::codec::slot::Slot;
use crate::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_data::packet::clientbound::PLAY_CONTAINER_SET_CONTENT;
use pumpkin_macros::packet;
@@ -10,16 +10,16 @@ use serde::Serialize;
pub struct CSetContainerContent<'a> {
window_id: VarInt,
state_id: VarInt,
slot_data: &'a [Slot],
carried_item: &'a Slot,
slot_data: &'a [ItemStackSerializer<'a>],
carried_item: &'a ItemStackSerializer<'a>,
}
impl<'a> CSetContainerContent<'a> {
pub fn new(
window_id: VarInt,
state_id: VarInt,
slots: &'a [Slot],
carried_item: &'a Slot,
slots: &'a [ItemStackSerializer],
carried_item: &'a ItemStackSerializer,
) -> Self {
Self {
window_id,

View File

@@ -1,5 +1,5 @@
use crate::VarInt;
use crate::codec::slot::Slot;
use crate::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_data::packet::clientbound::PLAY_CONTAINER_SET_SLOT;
use pumpkin_macros::packet;
@@ -11,11 +11,16 @@ pub struct CSetContainerSlot<'a> {
window_id: i8,
state_id: VarInt,
slot: i16,
slot_data: &'a Slot,
slot_data: &'a ItemStackSerializer<'a>,
}
impl<'a> CSetContainerSlot<'a> {
pub fn new(window_id: i8, state_id: i32, slot: i16, slot_data: &'a Slot) -> Self {
pub fn new(
window_id: i8,
state_id: i32,
slot: i16,
slot_data: &'a ItemStackSerializer<'a>,
) -> Self {
Self {
window_id,
state_id: state_id.into(),

View File

@@ -7,17 +7,20 @@ use serde::Serialize;
use crate::{
ClientPacket,
codec::{slot::Slot, var_int::VarInt},
codec::{item_stack_seralizer::ItemStackSerializer, var_int::VarInt},
};
#[packet(PLAY_SET_EQUIPMENT)]
pub struct CSetEquipment {
entity_id: VarInt,
equipment: Vec<(EquipmentSlot, Slot)>,
equipment: Vec<(EquipmentSlot, ItemStackSerializer<'static>)>,
}
impl CSetEquipment {
pub fn new(entity_id: VarInt, equipment: Vec<(EquipmentSlot, Slot)>) -> Self {
pub fn new(
entity_id: VarInt,
equipment: Vec<(EquipmentSlot, ItemStackSerializer<'static>)>,
) -> Self {
Self {
entity_id,
equipment,

View File

@@ -0,0 +1,121 @@
use std::borrow::Cow;
use crate::VarInt;
use pumpkin_data::item::Item;
use pumpkin_world::item::ItemStack;
use serde::{
Deserialize, Serialize, Serializer,
de::{self, SeqAccess},
};
#[derive(Debug, Clone)]
pub struct ItemStackSerializer<'a>(pub Cow<'a, ItemStack>);
impl<'de> Deserialize<'de> for ItemStackSerializer<'static> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = ItemStackSerializer<'static>;
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let item_count = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("Failed to decode VarInt"))?;
let slot = if item_count.0 == 0 {
ItemStackSerializer(Cow::Borrowed(&ItemStack::EMPTY))
} else {
let item_id = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No item id VarInt!"))?;
let num_components_to_add = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No component add length VarInt!"))?;
let num_components_to_remove = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No component remove length VarInt!"))?;
if num_components_to_add.0 != 0 || num_components_to_remove.0 != 0 {
return Err(de::Error::custom(
"Slot components are currently unsupported",
));
}
let item_id: u16 = item_id
.0
.try_into()
.map_err(|_| de::Error::custom("Invalid item id!"))?;
ItemStackSerializer(Cow::Owned(ItemStack::new(
item_count.0 as u8,
Item::from_id(item_id).unwrap_or(Item::AIR),
)))
};
Ok(slot)
}
}
deserializer.deserialize_seq(Visitor)
}
}
impl Serialize for ItemStackSerializer<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.0.is_empty() {
VarInt(0).serialize(serializer)
} else {
// TODO: Components
#[derive(Serialize)]
struct NetworkRepr {
item_count: VarInt,
item_id: VarInt,
components_to_add: VarInt,
components_to_remove: VarInt,
}
NetworkRepr {
item_count: self.0.item_count.into(),
item_id: self.0.item.id.into(),
components_to_add: 0.into(),
components_to_remove: 0.into(),
}
.serialize(serializer)
}
}
}
impl ItemStackSerializer<'_> {
pub fn to_stack(self) -> ItemStack {
self.0.into_owned()
}
}
impl From<ItemStack> for ItemStackSerializer<'_> {
fn from(item: ItemStack) -> Self {
ItemStackSerializer(Cow::Owned(item))
}
}
impl From<Option<ItemStack>> for ItemStackSerializer<'_> {
fn from(item: Option<ItemStack>) -> Self {
match item {
Some(item) => ItemStackSerializer::from(item),
None => ItemStackSerializer(Cow::Borrowed(&ItemStack::EMPTY)),
}
}
}

View File

@@ -1,5 +1,5 @@
pub mod bit_set;
pub mod identifier;
pub mod slot;
pub mod item_stack_seralizer;
pub mod var_int;
pub mod var_long;

View File

@@ -1,170 +0,0 @@
use crate::VarInt;
use pumpkin_data::item::Item;
use pumpkin_world::item::ItemStack;
use serde::{
Deserialize, Serialize, Serializer,
de::{self, SeqAccess},
ser,
};
#[derive(Debug, Clone)]
pub enum Slot {
NoItem,
Item {
// This also handles items on the ground which can have >64 items
item_count: u32,
item_id: u16,
// TODO: Implement item components
},
}
impl<'de> Deserialize<'de> for Slot {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Slot;
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let item_count = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("Failed to decode VarInt"))?;
let slot = if item_count.0 == 0 {
Slot::NoItem
} else {
let item_id = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No item id VarInt!"))?;
let num_components_to_add = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No component add length VarInt!"))?;
let num_components_to_remove = seq
.next_element::<VarInt>()?
.ok_or(de::Error::custom("No component remove length VarInt!"))?;
if num_components_to_add.0 != 0 || num_components_to_remove.0 != 0 {
return Err(de::Error::custom(
"Slot components are currently unsupported",
));
}
let item_id: u16 = item_id
.0
.try_into()
.map_err(|_| de::Error::custom("Invalid item id!"))?;
Slot::Item {
// i32 can always be u32
item_count: item_count.0 as u32,
item_id,
}
};
Ok(slot)
}
}
deserializer.deserialize_seq(Visitor)
}
}
impl Serialize for Slot {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::NoItem => VarInt(0).serialize(serializer),
Self::Item {
item_count,
item_id,
} => {
// TODO: Components
#[derive(Serialize)]
struct NetworkRepr {
item_count: VarInt,
item_id: VarInt,
components_to_add: VarInt,
components_to_remove: VarInt,
}
let item_count: i32 = (*item_count)
.try_into()
.map_err(|_| ser::Error::custom("Item count overflows an i32!"))?;
NetworkRepr {
item_count: item_count.into(),
item_id: (*item_id).into(),
components_to_add: 0.into(),
components_to_remove: 0.into(),
}
.serialize(serializer)
}
}
}
}
impl Slot {
pub fn new(item_id: u16, count: u32) -> Self {
Self::Item {
item_count: count,
item_id,
}
}
pub fn to_stack(self) -> Result<Option<ItemStack>, &'static str> {
match self {
Self::NoItem => Ok(None),
Self::Item {
item_count,
item_id,
} => {
let item = Item::from_id(item_id).ok_or("Item id invalid")?;
if item_count > item.components.max_stack_size as u32 {
Err("Stack item count greater than allowed")
} else {
let stack = ItemStack {
item,
// This is checked above
item_count: item_count as u8,
};
Ok(Some(stack))
}
}
}
}
pub const fn empty() -> Self {
Self::NoItem
}
}
impl From<&ItemStack> for Slot {
fn from(item: &ItemStack) -> Self {
Slot::new(item.item.id, item.item_count as u32)
}
}
impl From<Option<&ItemStack>> for Slot {
fn from(item: Option<&ItemStack>) -> Self {
item.map(Slot::from).unwrap_or(Slot::empty())
}
}
// impl From<&Option<ItemStack>> for Slot {
// fn from(item: &Option<ItemStack>) -> Self {
// item.map(|stack| Self::from(&stack))
// .unwrap_or(Slot::empty())
// }
// }

View File

@@ -1,5 +1,5 @@
use crate::VarInt;
use crate::codec::slot::Slot;
use crate::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_data::packet::serverbound::PLAY_CONTAINER_CLICK;
use pumpkin_macros::packet;
use serde::de::SeqAccess;
@@ -14,8 +14,8 @@ pub struct SClickContainer {
pub button: i8,
pub mode: SlotActionType,
pub length_of_array: VarInt,
pub array_of_changed_slots: Vec<(i16, Slot)>,
pub carried_item: Slot,
pub array_of_changed_slots: Vec<(i16, ItemStackSerializer<'static>)>,
pub carried_item: ItemStackSerializer<'static>,
}
impl<'de> Deserialize<'de> for SClickContainer {
@@ -60,13 +60,13 @@ impl<'de> Deserialize<'de> for SClickContainer {
.next_element::<i16>()?
.ok_or(de::Error::custom("Unable to parse slot"))?;
let slot = seq
.next_element::<Slot>()?
.next_element::<ItemStackSerializer>()?
.ok_or(de::Error::custom("Unable to parse item"))?;
array_of_changed_slots.push((slot_number, slot));
}
let carried_item = seq
.next_element::<Slot>()?
.next_element::<ItemStackSerializer>()?
.ok_or(de::Error::custom("Failed to decode carried item"))?;
Ok(SClickContainer {

View File

@@ -1,11 +1,11 @@
use pumpkin_data::packet::serverbound::PLAY_SET_CREATIVE_MODE_SLOT;
use pumpkin_macros::packet;
use crate::codec::slot::Slot;
use crate::codec::item_stack_seralizer::ItemStackSerializer;
#[derive(serde::Deserialize, Debug)]
#[packet(PLAY_SET_CREATIVE_MODE_SLOT)]
pub struct SSetCreativeSlot {
pub slot: i16,
pub clicked_item: Slot,
pub clicked_item: ItemStackSerializer<'static>,
}

View File

@@ -16,7 +16,6 @@ use jukebox_song::JukeboxSong;
use paint::Painting;
use pig::PigVariant;
use pumpkin_protocol::{client::config::RegistryEntry, codec::identifier::Identifier};
pub use recipe::{RECIPES, Recipe, RecipeResult, RecipeType, flatten_3x3};
use serde::{Deserialize, Serialize};
use trim_material::TrimMaterial;
use trim_pattern::TrimPattern;
@@ -36,7 +35,6 @@ mod instrument;
mod jukebox_song;
mod paint;
mod pig;
mod recipe;
mod trim_material;
mod trim_pattern;
mod wolf;

View File

@@ -1,89 +0,0 @@
mod read;
mod recipe_formats;
pub use read::{Recipe, RecipeResult, RecipeType};
use std::sync::LazyLock;
pub fn flatten_3x3<T: Clone>(input: [[Option<T>; 3]; 3]) -> [[Option<T>; 3]; 3] {
let mut final_output = [const { [const { None }; 3] }; 3];
let mut row_alignment = 0;
let mut column_alignment = 2;
for (i, row) in input.iter().enumerate() {
let mut row_values = [false; 3];
for (i, item) in row.iter().enumerate().take(column_alignment + 1) {
if item.is_some() {
row_values[i] = true;
if i < column_alignment {
column_alignment = i;
}
}
}
if i == row_alignment && row_values.iter().all(|val| !val) {
row_alignment += 1;
}
}
for (i, row) in &mut final_output.iter_mut().enumerate() {
let input_row = input.get(i + row_alignment);
for (j, item) in row.iter_mut().enumerate() {
let val = input_row.and_then(|val| val.get(j + column_alignment));
*item = match val {
None => None,
Some(None) => None,
Some(Some(val)) => Some(val.clone()),
}
}
}
final_output
}
pub static RECIPES: LazyLock<Vec<Recipe>> =
LazyLock::new(|| serde_json::from_str(include_str!("../../../assets/recipes.json")).unwrap());
#[cfg(test)]
mod test {
use super::flatten_3x3;
#[test]
fn row_flatten() {
let input = [[None; 3], [None; 3], [Some(()), Some(()), Some(())]];
let out = [[Some(()), Some(()), Some(())], [None; 3], [None; 3]];
assert_eq!(flatten_3x3(input), out);
}
#[test]
fn column_flatten() {
let one_row_right = [None, None, Some(())];
let one_row_left = [Some(()), None, None];
let input = [one_row_right, one_row_right, one_row_right];
let output = [one_row_left, one_row_left, one_row_left];
assert_eq!(flatten_3x3(input), output)
}
#[test]
fn full_flatten() {
let input_1 = [[None; 3], [None; 3], [None, None, Some(())]];
let output_1 = [[Some(()), None, None], [None; 3], [None; 3]];
let input_2 = [
[None; 3],
[None, None, Some(())],
[None, Some(()), Some(())],
];
let output_2 = [
[None, Some(()), None],
[Some(()), Some(()), None],
[None; 3],
];
let input_3 = [[Some(()), None, None], [Some(()), None, None], [None; 3]];
assert_eq!(flatten_3x3(input_1), output_1);
assert_eq!(flatten_3x3(input_2), output_2);
assert_eq!(flatten_3x3(input_3), input_3);
}
// #[test]
// // This makes sure that all recipes are able to be deserialized properly.
// fn check_parsing() {
// assert!(!RECIPES.is_empty())
// }
}

View File

@@ -1,534 +0,0 @@
use crate::flatten_3x3;
use crate::recipe::read::SpecialCraftingType::{
ArmorDye, BannerDuplicate, BookCloning, Firework, RepairItem, ShieldDecoration,
ShulkerboxColoring, SuspiciousStew, TippedArrow,
};
use crate::recipe::read::ingredients::Ingredients;
use crate::recipe::recipe_formats::{ShapedCrafting, ShapelessCrafting};
use pumpkin_util::registry::RegistryEntryList;
use serde::de::{Error, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, de};
use std::collections::HashMap;
use std::fmt::Formatter;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RecipeType {
Blasting,
CampfireCooking,
Crafting(CraftingType),
Smelting,
Smithing(SmithingType),
Smoking,
StoneCutting,
}
impl RecipeType {
pub const fn is_shapeless(&self) -> bool {
// I have not checked exactly which ones require shape and which don't!
!matches!(self, Self::Crafting(CraftingType::Shaped))
}
}
impl FromStr for RecipeType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use CraftingType::*;
use FireworkCrafting::*;
use MapCrafting::*;
use RecipeType::*;
let s = s.trim_start_matches("minecraft:");
match s {
"blasting" => Ok(Blasting),
"campfire_cooking" => Ok(CampfireCooking),
"crafting_shaped" => Ok(Crafting(Shaped)),
"crafting_shapeless" => Ok(Crafting(Shapeless)),
"crafting_special_bookcloning" => Ok(Crafting(Special(BookCloning))),
"crafting_special_repairitem" => Ok(Crafting(Special(RepairItem))),
"crafting_special_armordye" => Ok(Crafting(Special(ArmorDye))),
"crafting_special_firework_rocket" => Ok(Crafting(Special(Firework(Rocket)))),
"crafting_special_firework_star" => Ok(Crafting(Special(Firework(Star)))),
"crafting_special_firework_star_fade" => Ok(Crafting(Special(Firework(StarFade)))),
"crafting_special_suspiciousstew" => Ok(Crafting(Special(SuspiciousStew))),
"crafting_special_mapextending" => {
Ok(Crafting(Special(SpecialCraftingType::Map(Extending))))
}
"crafting_special_mapcloning" => {
Ok(Crafting(Special(SpecialCraftingType::Map(Cloning))))
}
"crafting_special_shulkerboxcoloring" => Ok(Crafting(Special(ShulkerboxColoring))),
"crafting_special_bannerduplicate" => Ok(Crafting(Special(BannerDuplicate))),
"crafting_special_shielddecoration" => Ok(Crafting(Special(ShieldDecoration))),
"crafting_special_tippedarrow" => Ok(Crafting(Special(TippedArrow))),
"crafting_decorated_pot" => Ok(Crafting(DecoratedPot)),
"crafting_transmute" => Ok(Crafting(Transmute)),
"smelting" => Ok(Smelting),
"smithing" => Ok(Smithing(SmithingType::Normal)),
"smithing_trim" => Ok(Smithing(SmithingType::Trim)),
"smithing_transform" => Ok(Smithing(SmithingType::Transform)),
"smoking" => Ok(Smoking),
"stonecutting" => Ok(StoneCutting),
_ => Err(format!("Could not find recipe with id: \"{s}\"")),
}
}
}
pub mod ingredients {
use pumpkin_util::registry::RegistryEntryList;
use serde::de::{SeqAccess, Visitor};
use serde::{Deserialize, Deserializer};
use std::fmt::Formatter;
pub struct Ingredients(pub Vec<RegistryEntryList>);
impl<'de> Deserialize<'de> for Ingredients {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IngredientsVisitor;
impl<'de> Visitor<'de> for IngredientsVisitor {
type Value = Ingredients;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid ingredients")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut ingredients = vec![];
while let Some(element) = seq.next_element()? {
ingredients.push(element)
}
Ok(Ingredients(ingredients))
}
}
deserializer.deserialize_seq(IngredientsVisitor)
}
}
}
#[derive(Debug)]
pub enum RecipeResult {
Many {
count: u8,
id: String,
// TODO
components: Option<serde_json::Value>,
},
Single {
id: String,
// TODO
components: Option<serde_json::Value>,
},
Special,
}
impl RecipeResult {
pub fn id(&self) -> &str {
match self {
Self::Many { id, .. } | Self::Single { id, .. } => id,
Self::Special => "minecraft:air",
}
}
}
impl<'de> Deserialize<'de> for RecipeResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Fields {
Count,
Id,
Components,
}
struct ResultVisitor;
impl<'de> Visitor<'de> for ResultVisitor {
type Value = RecipeResult;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid recipe result")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut id: Option<&str> = None;
let mut count: Option<u8> = None;
let mut components: Option<serde_json::Value> = None;
while let Some(key) = map.next_key()? {
match key {
Fields::Id => visit_option(&mut map, &mut id, "id")?,
Fields::Count => visit_option(&mut map, &mut count, "count")?,
Fields::Components => {
visit_option(&mut map, &mut components, "components")?
}
}
}
let id = id
.ok_or_else(|| de::Error::missing_field("id"))?
.to_string();
if let Some(count) = count {
Ok(RecipeResult::Many {
id,
count,
components,
})
} else {
Ok(RecipeResult::Single { id, components })
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(RecipeResult::Single {
id: v.to_string(),
components: None,
})
}
}
// Evaluate putting type constraint on `RecipeResult`, because only Crafting Transmute can call visit_str
deserializer.deserialize_any(ResultVisitor)
}
}
pub struct RecipeKeys(pub(super) HashMap<char, RegistryEntryList>);
impl<'de> Deserialize<'de> for RecipeKeys {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct KeyVisitor;
impl<'de> Visitor<'de> for KeyVisitor {
type Value = HashMap<char, RegistryEntryList>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "existing key inside recipe")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut return_map = HashMap::new();
while let Some(next) = map.next_key()? {
let test: &str = next;
let c: char = test.chars().next().unwrap();
let ingredient_type: RegistryEntryList = map.next_value()?;
return_map.insert(c, ingredient_type);
}
Ok(return_map)
}
}
deserializer.deserialize_map(KeyVisitor).map(Self)
}
}
impl<'de> Deserialize<'de> for Recipe {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Fields {
Type,
Category,
Group,
Key,
Pattern,
Result,
Ingredients,
Ingredient,
// Only exists sometimes, at least on Shaped crafting
#[serde(rename = "show_notification")]
ShowNotification,
// Armor
Addition,
Base,
Template,
// Smelting
CookingTime,
Experience,
// Transmute
Input,
Material,
}
struct RecipeVisitor;
impl<'de> Visitor<'de> for RecipeVisitor {
type Value = Recipe;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid recipe")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut recipe_type: Option<&str> = None;
let mut category: Option<&str> = None;
let mut group: Option<&str> = None;
let mut keys: Option<RecipeKeys> = None;
let mut pattern: Option<Vec<&str>> = None;
let mut result: Option<RecipeResult> = None;
let mut ingredients: Option<Ingredients> = None;
let mut ingredient: Option<RegistryEntryList> = None;
let mut addition: Option<RegistryEntryList> = None;
let mut base: Option<RegistryEntryList> = None;
let mut template: Option<RegistryEntryList> = None;
let mut cookingtime: Option<u16> = None;
let mut experience: Option<f32> = None;
let mut show_notification: Option<bool> = None;
let mut transmute_input: Option<RegistryEntryList> = None;
let mut transmute_material: Option<RegistryEntryList> = None;
while let Some(key) = map.next_key()? {
(match key {
Fields::Type => visit_option(&mut map, &mut recipe_type, "type"),
Fields::Group => visit_option(&mut map, &mut group, "group"),
Fields::Category => visit_option(&mut map, &mut category, "group"),
Fields::Key => visit_option(&mut map, &mut keys, "key"),
Fields::Pattern => visit_option(&mut map, &mut pattern, "pattern"),
Fields::Result => visit_option(&mut map, &mut result, "result"),
Fields::Ingredients => {
visit_option(&mut map, &mut ingredients, "ingredients")
}
Fields::Ingredient => visit_option(&mut map, &mut ingredient, "ingredient"),
Fields::Addition => visit_option(&mut map, &mut addition, "addition"),
Fields::Base => visit_option(&mut map, &mut base, "base"),
Fields::Template => visit_option(&mut map, &mut template, "template"),
Fields::CookingTime => {
visit_option(&mut map, &mut cookingtime, "cookingtime")
}
Fields::Experience => visit_option(&mut map, &mut experience, "experience"),
Fields::ShowNotification => {
visit_option(&mut map, &mut show_notification, "show_notification")
}
Fields::Input => visit_option(&mut map, &mut transmute_input, "input"),
Fields::Material => {
visit_option(&mut map, &mut transmute_material, "material")
}
})?
}
let recipe_type: RecipeType = recipe_type
.ok_or_else(|| de::Error::missing_field("type"))?
.parse()
.unwrap();
let result = match recipe_type {
RecipeType::Crafting(CraftingType::Special(_))
| RecipeType::Crafting(CraftingType::DecoratedPot)
| RecipeType::Smithing(_) => RecipeResult::Special,
_ => result.ok_or_else(|| de::Error::missing_field("result"))?,
};
match recipe_type {
RecipeType::Crafting(CraftingType::Shaped) => {
let mut rows = [[None; 3], [None; 3], [None; 3]];
pattern
.ok_or_else(|| de::Error::missing_field("pattern"))?
.into_iter()
.map(|s| {
let mut chars = [None; 3];
s.chars()
.enumerate()
.for_each(|(i, char)| chars[i] = Some(char));
chars
})
.enumerate()
.for_each(|(i, row)| rows[i] = row);
let keys = keys.ok_or_else(|| de::Error::missing_field("keys"))?;
Ok(Recipe::from(ShapedCrafting::new(keys, rows, result)))
}
RecipeType::Crafting(CraftingType::Shapeless) => {
let ingredients =
ingredients.ok_or_else(|| de::Error::missing_field("ingredients"))?;
Ok(Recipe::from(ShapelessCrafting::new(ingredients.0, result)))
}
RecipeType::Crafting(CraftingType::Special(_)) => Ok(Recipe::from(Test {
recipe_type,
result,
})),
RecipeType::Crafting(CraftingType::DecoratedPot) => Ok(Recipe::from(Test {
recipe_type,
result,
})),
RecipeType::Crafting(CraftingType::Transmute) => {
let _input =
transmute_input.ok_or_else(|| de::Error::missing_field("input"))?;
// Maybe also has material
Ok(Recipe::from(Test {
recipe_type,
result,
}))
}
RecipeType::Smithing(_) => Ok(Recipe::from(Test {
recipe_type,
result: RecipeResult::Special,
})),
_ => Ok(Recipe::from(Test {
recipe_type,
result,
})),
}
}
}
const FIELDS: &[&str] = &[
"type",
"category",
"group",
"key",
"pattern",
"result",
"ingredients",
"ingredient",
"addition",
"base",
"template",
"cookingtime",
"experience",
"show_notification",
"input",
"material",
];
deserializer.deserialize_struct("Recipe", FIELDS, RecipeVisitor)
}
}
#[inline(always)]
fn visit_option<'de, T: Deserialize<'de>, Map: MapAccess<'de>>(
map: &mut Map,
option: &mut Option<T>,
field: &'static str,
) -> Result<(), Map::Error> {
match option {
Some(_) => Err(<Map as MapAccess>::Error::duplicate_field(field)),
None => {
*option = Some(map.next_value()?);
Ok(())
}
}
}
pub struct Recipe {
pub recipe_type: RecipeType,
pattern: Vec<[[Option<RegistryEntryList>; 3]; 3]>,
result: RecipeResult,
}
impl Recipe {
pub fn pattern(&self) -> &[[[Option<RegistryEntryList>; 3]; 3]] {
&self.pattern
}
pub fn result(&self) -> &RecipeResult {
&self.result
}
pub fn implemented(&self) -> bool {
match self.recipe_type {
RecipeType::Crafting(crafting_type) => {
matches!(
crafting_type,
CraftingType::Shapeless | CraftingType::Shaped
)
}
_ => false,
}
}
}
struct Test {
recipe_type: RecipeType,
result: RecipeResult,
}
impl RecipeTrait for Test {
fn recipe_type(&self) -> RecipeType {
self.recipe_type
}
fn pattern(&self) -> Vec<[[Option<RegistryEntryList>; 3]; 3]> {
vec![[const { [const { None }; 3] }; 3]]
}
fn result(self) -> RecipeResult {
self.result
}
}
impl<T: RecipeTrait> From<T> for Recipe {
fn from(recipe_type: T) -> Self {
recipe_type.to_recipe()
}
}
pub trait RecipeTrait: Sized {
fn recipe_type(&self) -> RecipeType;
fn pattern(&self) -> Vec<[[Option<RegistryEntryList>; 3]; 3]>;
fn result(self) -> RecipeResult;
fn to_recipe(self) -> Recipe {
Recipe {
recipe_type: self.recipe_type(),
pattern: self.pattern().into_iter().map(flatten_3x3).collect(),
result: self.result(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CraftingType {
Shapeless,
Shaped,
Special(SpecialCraftingType),
DecoratedPot,
Transmute,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpecialCraftingType {
BookCloning,
RepairItem,
ArmorDye,
Firework(FireworkCrafting),
SuspiciousStew,
ShulkerboxColoring,
Map(MapCrafting),
BannerDuplicate,
ShieldDecoration,
TippedArrow,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FireworkCrafting {
Rocket,
Star,
StarFade,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MapCrafting {
Extending,
Cloning,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SmithingType {
Normal,
Trim,
Transform,
}

View File

@@ -1,92 +0,0 @@
use pumpkin_util::registry::RegistryEntryList;
use super::super::recipe::RecipeType;
use super::read::{CraftingType, RecipeKeys, RecipeResult, RecipeTrait};
pub struct ShapedCrafting {
keys: RecipeKeys,
pattern: [[Option<char>; 3]; 3],
output: RecipeResult,
}
impl RecipeKeys {
fn pattern_to_thing(
&self,
pattern: [[Option<char>; 3]; 3],
) -> [[Option<RegistryEntryList>; 3]; 3] {
pattern
.map(|row| row.map(|maybe_char| maybe_char.and_then(|char| self.0.get(&char).cloned())))
}
}
impl ShapedCrafting {
pub fn new(keys: RecipeKeys, pattern: [[Option<char>; 3]; 3], output: RecipeResult) -> Self {
Self {
keys,
pattern,
output,
}
}
}
impl RecipeTrait for ShapedCrafting {
fn recipe_type(&self) -> RecipeType {
RecipeType::Crafting(CraftingType::Shaped)
}
fn pattern(&self) -> Vec<[[Option<RegistryEntryList>; 3]; 3]> {
vec![self.keys.pattern_to_thing(self.pattern)]
}
fn result(self) -> RecipeResult {
self.output
}
}
pub struct ShapelessCrafting {
ingredients: Vec<RegistryEntryList>,
output: RecipeResult,
}
impl ShapelessCrafting {
pub(crate) fn new(ingredients: Vec<RegistryEntryList>, output: RecipeResult) -> Self {
Self {
ingredients,
output,
}
}
}
impl RecipeTrait for ShapelessCrafting {
fn recipe_type(&self) -> RecipeType {
RecipeType::Crafting(CraftingType::Shapeless)
}
// Iterating over all permutations is cheaper than resolving and iterating over all tags when trying to check if a recipe
// is correct. Otherwise, we would have to backtrack and check for each item in the recipe input, which tags they are inside,
// and then sort those permutations.
fn pattern(&self) -> Vec<[[std::option::Option<RegistryEntryList>; 3]; 3]> {
vec![
self.ingredients.clone(), //.permutations(self.ingredients.len())
]
.into_iter()
.map(|thing| {
let mut v1 = [const { None }; 3];
let mut v2 = [const { None }; 3];
let mut v3 = [const { None }; 3];
for (i, thing) in thing.into_iter().enumerate() {
if i < 3 {
v1[i] = Some(thing)
} else if i < 6 {
v2[i - 3] = Some(thing)
} else {
v3[i - 6] = Some(thing)
}
}
[v1, v2, v3]
})
.collect()
}
fn result(self) -> RecipeResult {
self.output
}
}

View File

@@ -28,10 +28,56 @@ impl PartialEq for ItemStack {
}
impl ItemStack {
pub const EMPTY: ItemStack = ItemStack {
item_count: 0,
item: Item::AIR,
};
pub fn new(item_count: u8, item: Item) -> Self {
Self { item_count, item }
}
pub fn get_max_stack_size(&self) -> u8 {
self.item.components.max_stack_size
}
pub fn get_item(&self) -> &Item {
if self.is_empty() {
&Item::AIR
} else {
&self.item
}
}
pub fn is_empty(&self) -> bool {
self.item_count == 0 || self.item.id == Item::AIR.id
}
pub fn split(&mut self, amount: u8) -> Self {
let min = amount.min(self.item_count);
let stack = self.copy_with_count(min);
self.decrement(min);
stack
}
pub fn copy_with_count(&self, count: u8) -> Self {
let mut stack = self.clone();
stack.item_count = count;
stack
}
pub fn decrement(&mut self, amount: u8) {
self.item_count = self.item_count.saturating_sub(amount);
}
pub fn increment(&mut self, amount: u8) {
self.item_count = self.item_count.saturating_add(amount);
}
pub fn are_items_and_components_equal(&self, other: &Self) -> bool {
self.item == other.item //TODO: && self.item.components == other.item.components
}
/// 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`.

View File

@@ -4,7 +4,7 @@ use async_trait::async_trait;
use pumpkin_data::{damage::DamageType, item::Item};
use pumpkin_protocol::{
client::play::{CTakeItemEntity, MetaDataType, Metadata},
codec::slot::Slot,
codec::item_stack_seralizer::ItemStackSerializer,
};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
@@ -16,11 +16,10 @@ use super::{Entity, EntityBase, living::LivingEntity, player::Player};
pub struct ItemEntity {
entity: Entity,
item: Item,
item_age: AtomicU32,
// These cannot be atomic values because we mutate their state based on what they are; we run
// into the ABA problem
item_count: Mutex<u32>,
item_stack: Mutex<ItemStack>,
pickup_delay: Mutex<u8>,
}
@@ -36,16 +35,21 @@ impl ItemEntity {
entity.yaw.store(rand::random::<f32>() * 360.0);
Self {
entity,
item: Item::from_id(item_id).expect("We passed a bad item id into ItemEntity"),
item_stack: Mutex::new(ItemStack::new(
count as u8,
Item::from_id(item_id).expect("We passed a bad item id into ItemEntity"),
)),
item_age: AtomicU32::new(0),
item_count: Mutex::new(count),
pickup_delay: Mutex::new(10), // Vanilla pickup delay is 10 ticks
}
}
pub async fn send_meta_packet(&self) {
let slot = Slot::new(self.item.id, *self.item_count.lock().await);
self.entity
.send_meta_data(&[Metadata::new(8, MetaDataType::ItemStack, &slot)])
.send_meta_data(&[Metadata::new(
8,
MetaDataType::ItemStack,
&ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)])
.await;
}
}
@@ -81,10 +85,11 @@ impl EntityBase for ItemEntity {
let mut total_pick_up = 0;
let mut slot_updates = Vec::new();
let remove_entity = {
let mut stack_size = self.item_count.lock().await;
let max_stack = self.item.components.max_stack_size;
while *stack_size > 0 {
if let Some(slot) = inv.get_pickup_item_slot(self.item.id) {
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)
@@ -96,7 +101,7 @@ impl EntityBase for ItemEntity {
// 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(*stack_size);
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
@@ -105,7 +110,7 @@ impl EntityBase for ItemEntity {
total_pick_up += amount_to_add;
debug_assert!(amount_to_add > 0);
*stack_size -= amount_to_add;
stack_size = stack_size.saturating_sub(amount_to_add as u8);
slot_updates.push((slot, existing_stack.clone()));
} else {
@@ -114,20 +119,13 @@ impl EntityBase for ItemEntity {
// 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(*stack_size);
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 -= amount_to_add;
stack_size = stack_size.saturating_sub(amount_to_add as u8);
// Therefore this is safe
let item_stack = ItemStack::new(amount_to_add as u8, self.item.clone());
// Update referenced stack so next call to `get_pickup_item_slot` is
// correct
*maybe_stack = Some(item_stack.clone());
slot_updates.push((slot, item_stack));
slot_updates.push((slot, self.item_stack.lock().await.clone()));
}
} else {
// We can't pick anything else up
@@ -135,7 +133,7 @@ impl EntityBase for ItemEntity {
}
}
*stack_size == 0
stack_size == 0
};
if total_pick_up > 0 {

View File

@@ -12,7 +12,7 @@ use pumpkin_protocol::client::play::{CHurtAnimation, CTakeItemEntity};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::{
client::play::{CDamageEvent, CSetEquipment, EquipmentSlot, MetaDataType, Metadata},
codec::slot::Slot,
codec::item_stack_seralizer::ItemStackSerializer,
};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
@@ -55,9 +55,9 @@ impl LivingEntity {
}
pub async fn send_equipment_changes(&self, equipment: &[(EquipmentSlot, ItemStack)]) {
let equipment: Vec<(EquipmentSlot, Slot)> = equipment
let equipment: Vec<(EquipmentSlot, ItemStackSerializer)> = equipment
.iter()
.map(|(slot, stack)| (*slot, Slot::from(stack)))
.map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone())))
.collect();
self.entity
.world

View File

@@ -13,7 +13,7 @@ use pumpkin_inventory::{InventoryError, OptionallyCombinedContainer, container_c
use pumpkin_protocol::client::play::{
CCloseContainer, COpenScreen, CSetContainerContent, CSetContainerProperty, CSetContainerSlot,
};
use pumpkin_protocol::codec::slot::Slot;
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;
@@ -61,16 +61,17 @@ impl Player {
let container = OptionallyCombinedContainer::new(&mut inventory, container);
let slots: Vec<Slot> = container
let slots: Vec<ItemStackSerializer> = container
.all_slots_ref()
.into_iter()
.map(Slot::from)
.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(Slot::empty, std::convert::Into::into);
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(
@@ -192,7 +193,7 @@ impl Player {
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 = Slot::from(slot);
let slot = ItemStackSerializer::from(slot.cloned());
drop(opened_container);
self.send_container_changes(server, slot_index, slot)
.await?;
@@ -211,7 +212,7 @@ impl Player {
) -> 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 = Slot::from(item_stack);
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;
@@ -624,7 +625,7 @@ impl Player {
&self,
server: &Server,
slot_index: usize,
slot: Slot,
slot: ItemStackSerializer<'_>,
) -> Result<(), InventoryError> {
for player in self.get_current_players_in_container(server).await {
let mut inventory = player.inventory().lock().await;

View File

@@ -38,7 +38,7 @@ use pumpkin_protocol::client::play::{
CBlockEntityData, CBlockUpdate, COpenSignEditor, CPlayerInfoUpdate, CPlayerPosition,
CSetContainerSlot, CSetHeldItem, CSystemChatMessage, EquipmentSlot, InitChat, PlayerAction,
};
use pumpkin_protocol::codec::slot::Slot;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::server::play::{
SChunkBatch, SCookieResponse as SPCookieResponse, SPlayerSession, SUpdateSign,
@@ -554,7 +554,7 @@ impl Player {
stack: ItemStack,
) {
inventory.increment_state_id();
let slot_data = Slot::from(&stack);
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 {
@@ -1511,13 +1511,13 @@ impl Player {
}
let valid_slot = packet.slot >= 0 && packet.slot as usize <= SLOT_OFFHAND;
// TODO: Handle error
let item_stack = packet.clicked_item.to_stack().unwrap();
let item_stack = packet.clicked_item.to_stack();
if valid_slot {
self.inventory()
.lock()
.await
.set_slot(packet.slot as usize, item_stack, true)?;
} else if let Some(item_stack) = item_stack {
.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))
.await;