From 8cde02e625480e873c777feb324254ff258ed59d Mon Sep 17 00:00:00 2001 From: Edvin Bryntesson Date: Tue, 12 Nov 2024 12:57:14 +0100 Subject: [PATCH] Add Shaped and Unshaped crafting support, as well as deserializing the entire recipe format (#165) * add cleaner registry parsing * start adding recipes * add tags, and some crafting things * Fix echest command * add partly working crafting interface * add generated assets * fix container commands and clippy lints * fix issues with resolving recursive item tags for crafting * add flatten3x3 function * add shapeless crafting * add tests to and fix recipe flattening function * refactor recipe system a bit * add rayon to speed up initial loading of recipes * almost working after rebase * add deserialization for new crafting type "transmute" * make crafting work again * make shapeless recipes not require all permutations * fixed visual bug with when taking out ingredients for crafting * add support for new tags system --- pumpkin-inventory/Cargo.toml | 2 + pumpkin-inventory/src/crafting.rs | 100 +++ pumpkin-inventory/src/lib.rs | 69 +- pumpkin-inventory/src/open_container.rs | 77 ++- pumpkin-inventory/src/player.rs | 64 +- pumpkin-registry/Cargo.toml | 4 + pumpkin-registry/src/lib.rs | 6 + pumpkin-registry/src/recipe/mod.rs | 92 +++ pumpkin-registry/src/recipe/read.rs | 628 ++++++++++++++++++ pumpkin-registry/src/recipe/recipe_formats.rs | 94 +++ pumpkin-registry/src/tags.rs | 113 ++++ pumpkin/src/client/container.rs | 254 ++++--- pumpkin/src/client/player_packet.rs | 20 +- pumpkin/src/command/commands/cmd_craft.rs | 45 ++ pumpkin/src/command/commands/cmd_echest.rs | 4 +- pumpkin/src/command/commands/mod.rs | 1 + pumpkin/src/command/mod.rs | 5 +- pumpkin/src/entity/player.rs | 12 +- pumpkin/src/rcon/mod.rs | 4 +- pumpkin/src/server/mod.rs | 5 +- 20 files changed, 1458 insertions(+), 141 deletions(-) create mode 100644 pumpkin-inventory/src/crafting.rs create mode 100644 pumpkin-registry/src/recipe/mod.rs create mode 100644 pumpkin-registry/src/recipe/read.rs create mode 100644 pumpkin-registry/src/recipe/recipe_formats.rs create mode 100644 pumpkin-registry/src/tags.rs create mode 100644 pumpkin/src/command/commands/cmd_craft.rs diff --git a/pumpkin-inventory/Cargo.toml b/pumpkin-inventory/Cargo.toml index de5eb7d71..28beef942 100644 --- a/pumpkin-inventory/Cargo.toml +++ b/pumpkin-inventory/Cargo.toml @@ -6,9 +6,11 @@ edition.workspace = true [dependencies] # For items pumpkin-world = { path = "../pumpkin-world" } +pumpkin-registry = {path = "../pumpkin-registry"} pumpkin-macros = { path = "../pumpkin-macros" } log.workspace = true +rayon.workspace = true itertools.workspace = true crossbeam.workspace = true tokio.workspace = true diff --git a/pumpkin-inventory/src/crafting.rs b/pumpkin-inventory/src/crafting.rs new file mode 100644 index 000000000..61e2a3590 --- /dev/null +++ b/pumpkin-inventory/src/crafting.rs @@ -0,0 +1,100 @@ +use itertools::Itertools; +use pumpkin_registry::{ + flatten_3x3, get_tag_values, IngredientSlot, IngredientType, RecipeResult, TagCategory, RECIPES, +}; +use pumpkin_world::item::item_registry::get_item; +use pumpkin_world::item::ItemStack; +use rayon::prelude::*; + +#[inline(always)] +fn check_ingredient_type(ingredient_type: &IngredientType, input: ItemStack) -> bool { + match ingredient_type { + IngredientType::Tag(tag) => { + let items = match get_tag_values(TagCategory::Item, tag) { + Some(items) => items, + None => return false, + }; + items + .iter() + .any(|tag| check_ingredient_type(&tag.to_ingredient_type(), input)) + } + IngredientType::Item(item) => get_item(item).is_some_and(|item| item.id == input.item_id), + } +} + +pub fn check_if_matches_crafting(input: [[Option; 3]; 3]) -> Option { + 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_id: get_item(id).unwrap().id, + item_count: 1, + }), + RecipeResult::Many { id, count, .. } => Some(ItemStack { + item_id: get_item(id).unwrap().id, + item_count: *count, + }), + RecipeResult::Special => None, + })? +} + +fn ingredient_slot_check(recipe_item: &IngredientSlot, input: ItemStack) -> bool { + match recipe_item { + IngredientSlot::Single(ingredient) => check_ingredient_type(ingredient, input), + IngredientSlot::Many(ingredients) => ingredients + .iter() + .any(|ingredient| check_ingredient_type(ingredient, input)), + } +} +fn shapeless_crafting_match( + input: [[Option; 3]; 3], + pattern: &[[[Option; 3]; 3]], +) -> bool { + let mut pattern = pattern + .iter() + .flatten() + .flatten() + .flatten() + .cloned() + .collect_vec(); + 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() +} diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index c50af3fb6..c22a2158a 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -5,6 +5,7 @@ use pumpkin_macros::screen; use pumpkin_world::item::ItemStack; pub mod container_click; +mod crafting; pub mod drag_handler; mod error; mod open_container; @@ -12,7 +13,7 @@ pub mod player; pub mod window_property; pub use error::InventoryError; -pub use open_container::OpenContainer; +pub use open_container::*; /// https://wiki.vg/Inventory #[derive(Debug, FromPrimitive, Clone, Copy, Eq, PartialEq)] @@ -67,12 +68,26 @@ pub trait Container: Sync + Send { carried_item: &mut Option, slot: usize, mouse_click: MouseClick, + taking_crafted: bool, ) -> Result<(), InventoryError> { let mut all_slots = self.all_slots(); if slot > all_slots.len() { Err(InventoryError::InvalidSlot)? } + if taking_crafted { + match (all_slots[slot].as_mut(), carried_item.as_mut()) { + (Some(s1), Some(s2)) => { + if s1.item_id == s2.item_id { + handle_item_change(all_slots[slot], carried_item, mouse_click); + } + } + (Some(_), None) => handle_item_change(all_slots[slot], carried_item, mouse_click), + (None, None) | (None, Some(_)) => (), + } + return Ok(()); + } handle_item_change(carried_item, all_slots[slot], mouse_click); + Ok(()) } @@ -91,6 +106,26 @@ pub trait Container: Sync + Send { fn internal_pumpkin_id(&self) -> u64 { 0 } + + fn craft(&mut self) -> bool { + false + } + + fn crafting_output_slot(&self) -> Option { + None + } + + fn slot_in_crafting_input_slots(&self, _slot: &usize) -> bool { + false + } + + fn crafted_item_slot(&self) -> Option { + self.all_slots_ref() + .get(self.crafting_output_slot()?)? + .copied() + } + + fn recipe_used(&mut self) {} } pub fn handle_item_take( @@ -236,4 +271,36 @@ impl<'a> Container for OptionallyCombinedContainer<'a, 'a> { None => self.inventory.all_slots_ref(), } } + + fn craft(&mut self) -> bool { + match &mut self.container { + Some(container) => container.craft(), + None => self.inventory.craft(), + } + } + + fn crafting_output_slot(&self) -> Option { + match &self.container { + Some(container) => container.crafting_output_slot(), + None => self.inventory.crafting_output_slot(), + } + } + + fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { + match &self.container { + Some(container) => { + // We don't have to worry about length due to inventory crafting slots being inaccessible + // while inside container interfaces + container.slot_in_crafting_input_slots(slot) + } + None => self.inventory.slot_in_crafting_input_slots(slot), + } + } + + fn recipe_used(&mut self) { + match &mut self.container { + Some(container) => container.recipe_used(), + None => self.inventory.recipe_used(), + } + } } diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs index 1b97a69b9..a69695e7f 100644 --- a/pumpkin-inventory/src/open_container.rs +++ b/pumpkin-inventory/src/open_container.rs @@ -1,3 +1,4 @@ +use crate::crafting::check_if_matches_crafting; use crate::{Container, WindowType}; use pumpkin_world::item::ItemStack; use std::sync::Arc; @@ -35,10 +36,10 @@ impl OpenContainer { } } - pub fn empty(player_id: i32) -> Self { + pub fn new_empty_container(player_id: i32) -> Self { Self { players: vec![player_id], - container: Arc::new(Mutex::new(Box::new(Chest::new()))), + container: Arc::new(Mutex::new(Box::new(C::default()))), } } @@ -46,8 +47,8 @@ impl OpenContainer { self.players.clone() } } - -struct Chest([Option; 27]); +#[derive(Default)] +pub struct Chest([Option; 27]); impl Chest { pub fn new() -> Self { @@ -70,3 +71,71 @@ impl Container for Chest { self.0.iter().map(|slot| slot.as_ref()).collect() } } + +#[derive(Default)] +pub struct CraftingTable { + input: [[Option; 3]; 3], + output: Option, +} + +impl Container for CraftingTable { + fn window_type(&self) -> &'static WindowType { + &WindowType::CraftingTable + } + + fn window_name(&self) -> &'static str { + "Crafting Table" + } + fn all_slots(&mut self) -> Vec<&mut Option> { + let slots = vec![&mut self.output]; + let slots = slots + .into_iter() + .chain(self.input.iter_mut().flatten()) + .collect(); + slots + } + + fn all_slots_ref(&self) -> Vec> { + let slots = vec![self.output.as_ref()]; + let slots = slots + .into_iter() + .chain(self.input.iter().flatten().map(|i| i.as_ref())) + .collect(); + slots + } + + fn craft(&mut self) -> bool { + let old_output = self.output; + self.output = check_if_matches_crafting(self.input); + old_output != self.output + || self.input.iter().flatten().any(|s| s.is_some()) + || self.output.is_some() + } + + fn crafting_output_slot(&self) -> Option { + Some(0) + } + + fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { + (1..10).contains(slot) + } + + fn recipe_used(&mut self) { + self.input.iter_mut().flatten().for_each(|slot| { + if let Some(item) = slot { + if item.item_count > 1 { + item.item_count -= 1; + } else { + *slot = None; + } + } + }) + } + + fn all_combinable_slots(&self) -> Vec> { + self.input.iter().flatten().map(|s| s.as_ref()).collect() + } + fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option> { + self.input.iter_mut().flatten().collect() + } +} diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index 429498c25..8ac8274b7 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -1,9 +1,8 @@ -use std::slice::IterMut; -use std::sync::atomic::AtomicU32; - use crate::container_click::MouseClick; +use crate::crafting::check_if_matches_crafting; use crate::{handle_item_change, Container, InventoryError, WindowType}; use pumpkin_world::item::ItemStack; +use std::slice::IterMut; pub struct PlayerInventory { // Main Inventory + Hotbar @@ -14,7 +13,7 @@ pub struct PlayerInventory { offhand: Option, // current selected slot in hotbar selected: usize, - pub state_id: AtomicU32, + pub state_id: u32, // Notchian server wraps this value at 100, we can just keep it as a u8 that automatically wraps pub total_opened_containers: i32, } @@ -37,7 +36,7 @@ impl PlayerInventory { offhand: None, // TODO: What when player spawns in with an different index ? selected: 0, - state_id: AtomicU32::new(0), + state_id: 0, total_opened_containers: 2, } } @@ -54,29 +53,23 @@ impl PlayerInventory { /// Useful functionality for plugins in the future. pub fn set_slot( &mut self, - slot: u16, + slot: usize, item: Option, item_allowed_override: bool, ) -> Result<(), InventoryError> { - if !(0..=45).contains(&slot) { - return Err(InventoryError::InvalidSlot); - } - - match item_allowed_override { - true => { - *self.all_slots()[slot as usize] = item; + if item_allowed_override { + if !(0..=45).contains(&slot) { + Err(InventoryError::InvalidSlot)? } - false => { - let slot = slot as usize; - let slot_condition = self.slot_condition(slot)?; - if let Some(item) = item { - if slot_condition(&item) { - self.all_slots()[slot] = &mut Some(item); - } - } + *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)] @@ -168,19 +161,36 @@ impl Container for PlayerInventory { carried_slot: &mut Option, slot: usize, mouse_click: MouseClick, + invert: bool, ) -> Result<(), InventoryError> { let slot_condition = self.slot_condition(slot)?; let item_slot = self.get_slot(slot)?; if let Some(item) = carried_slot { if slot_condition(item) { + if invert { + handle_item_change(item_slot, carried_slot, mouse_click); + return Ok(()); + } handle_item_change(carried_slot, item_slot, mouse_click); } } else { + if invert { + handle_item_change(item_slot, carried_slot, mouse_click); + return Ok(()); + } handle_item_change(carried_slot, item_slot, mouse_click) } Ok(()) } + fn crafting_output_slot(&self) -> Option { + Some(0) + } + + fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool { + (1..=4).contains(slot) + } + fn all_slots(&mut self) -> Vec<&mut Option> { self.slots_mut() } @@ -196,4 +206,14 @@ impl Container for PlayerInventory { fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option> { self.items.iter_mut().collect() } + + fn craft(&mut self) -> bool { + let v1 = [self.crafting[0], self.crafting[1], None]; + let v2 = [self.crafting[2], self.crafting[3], None]; + let v3 = [None; 3]; + let together = [v1, v2, v3]; + + self.crafting_output = check_if_matches_crafting(together); + self.crafting.iter().any(|s| s.is_some()) + } } diff --git a/pumpkin-registry/Cargo.toml b/pumpkin-registry/Cargo.toml index 3cac258a2..ed7befdf4 100644 --- a/pumpkin-registry/Cargo.toml +++ b/pumpkin-registry/Cargo.toml @@ -10,8 +10,12 @@ pumpkin-core = { path = "../pumpkin-core" } serde.workspace = true serde_json.workspace = true +rayon.workspace = true + num-traits.workspace = true num-derive.workspace = true # nbt fastnbt = { git = "https://github.com/owengage/fastnbt.git" } + +itertools.workspace = true diff --git a/pumpkin-registry/src/lib.rs b/pumpkin-registry/src/lib.rs index d2576bc13..7c64093be 100644 --- a/pumpkin-registry/src/lib.rs +++ b/pumpkin-registry/src/lib.rs @@ -11,7 +11,11 @@ use instrument::Instrument; use jukebox_song::JukeboxSong; use paint::Painting; use pumpkin_protocol::client::config::RegistryEntry; +pub use recipe::{ + flatten_3x3, IngredientSlot, IngredientType, Recipe, RecipeResult, RecipeType, RECIPES, +}; use serde::{Deserialize, Serialize}; +pub use tags::{get_tag_values, TagCategory, TagType}; use trim_material::TrimMaterial; use trim_pattern::TrimPattern; use wolf::WolfVariant; @@ -25,6 +29,8 @@ mod enchantment; mod instrument; mod jukebox_song; mod paint; +mod recipe; +mod tags; mod trim_material; mod trim_pattern; mod wolf; diff --git a/pumpkin-registry/src/recipe/mod.rs b/pumpkin-registry/src/recipe/mod.rs new file mode 100644 index 000000000..271fe625d --- /dev/null +++ b/pumpkin-registry/src/recipe/mod.rs @@ -0,0 +1,92 @@ +mod read; +mod recipe_formats; + +pub use read::{ + ingredients::IngredientSlot, ingredients::IngredientType, Recipe, RecipeResult, RecipeType, +}; +use std::sync::LazyLock; +pub fn flatten_3x3(input: [[Option; 3]; 3]) -> [[Option; 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> = + LazyLock::new(|| serde_json::from_str(include_str!("../../../assets/recipes.json")).unwrap()); + +#[cfg(test)] +mod test { + use super::flatten_3x3; + use crate::RECIPES; + + #[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 deserialized properly + fn check_parsing() { + assert!(!RECIPES.is_empty()) + } +} diff --git a/pumpkin-registry/src/recipe/read.rs b/pumpkin-registry/src/recipe/read.rs new file mode 100644 index 000000000..2f3e08678 --- /dev/null +++ b/pumpkin-registry/src/recipe/read.rs @@ -0,0 +1,628 @@ +use crate::flatten_3x3; +use crate::recipe::read::ingredients::{IngredientSlot, Ingredients}; +use crate::recipe::read::SpecialCraftingType::{ + ArmorDye, BannerDuplicate, BookCloning, Firework, RepairItem, ShieldDecoration, + ShulkerboxColoring, SuspiciousStew, TippedArrow, +}; +use crate::recipe::recipe_formats::{ShapedCrafting, ShapelessCrafting}; +use serde::de::{Error, MapAccess, Visitor}; +use serde::{de, Deserialize, Deserializer}; +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 { + 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 serde::de::{Error, SeqAccess, Visitor}; + use serde::{de, Deserialize, Deserializer}; + use std::collections::HashMap; + use std::fmt::Formatter; + use std::hash::Hash; + + #[derive(Clone, PartialEq, Debug, Eq, Hash)] + pub enum IngredientType { + Item(String), + Tag(String), + } + + impl IngredientType { + pub fn to_all_types(&self, item_tags: &HashMap>) -> Vec { + match &self { + IngredientType::Tag(tag) => item_tags.get(tag).unwrap().clone(), + IngredientType::Item(s) => vec![s.to_string()], + } + } + } + + struct IngredientTypeVisitor; + impl<'de> Visitor<'de> for IngredientTypeVisitor { + type Value = IngredientType; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "valid item type") + } + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + match v.strip_prefix('#') { + Some(tag) => Ok(IngredientType::Tag(tag.to_string())), + None => Ok(IngredientType::Item(v.to_string())), + } + } + } + impl<'de> Deserialize<'de> for IngredientType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(IngredientTypeVisitor) + } + } + + #[derive(Clone, Debug, Eq, PartialEq, Hash)] + pub enum IngredientSlot { + Single(IngredientType), + Many(Vec), + } + + impl PartialEq for IngredientSlot { + fn eq(&self, other: &IngredientType) -> bool { + match self { + IngredientSlot::Single(ingredient) => other == ingredient, + IngredientSlot::Many(ingredients) => ingredients.contains(other), + } + } + } + + impl<'de> Deserialize<'de> for IngredientSlot { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SlotTypeVisitor; + impl<'de> Visitor<'de> for SlotTypeVisitor { + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "valid ingredient slot") + } + + type Value = IngredientSlot; + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(IngredientSlot::Single(IngredientTypeVisitor.visit_str(v)?)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut ingredients: Vec = vec![]; + while let Some(element) = seq.next_element()? { + ingredients.push(element) + } + if ingredients.len() == 1 { + Ok(IngredientSlot::Single(ingredients[0].clone())) + } else { + Ok(IngredientSlot::Many(ingredients)) + } + } + } + deserializer.deserialize_any(SlotTypeVisitor) + } + } + + pub struct Ingredients(pub Vec); + impl<'de> Deserialize<'de> for Ingredients { + fn deserialize(deserializer: D) -> Result + 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(self, mut seq: A) -> Result + 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, + }, + Single { + id: String, + // TODO + components: Option, + }, + 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(deserializer: D) -> Result + 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(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut id: Option<&str> = None; + let mut count: Option = None; + let mut components: Option = 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(self, v: &str) -> Result + 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); + +impl<'de> Deserialize<'de> for RecipeKeys { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct KeyVisitor; + impl<'de> Visitor<'de> for KeyVisitor { + type Value = HashMap; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "existing key inside recipe") + } + + fn visit_map(self, mut map: A) -> Result + 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: IngredientSlot = 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(deserializer: D) -> Result + 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(self, mut map: A) -> Result + 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 = None; + let mut pattern: Option> = None; + let mut result: Option = None; + let mut ingredients: Option = None; + let mut ingredient: Option = None; + let mut addition: Option = None; + let mut base: Option = None; + let mut template: Option = None; + let mut cookingtime: Option = None; + let mut experience: Option = None; + let mut show_notification: Option = None; + let mut transmute_input: Option = None; + let mut transmute_material: Option = 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, + field: &'static str, +) -> Result<(), Map::Error> { + match option { + Some(_) => Err(::Error::duplicate_field(field)), + None => { + *option = Some(map.next_value()?); + Ok(()) + } + } +} + +pub struct Recipe { + pub recipe_type: RecipeType, + pattern: Vec<[[Option; 3]; 3]>, + result: RecipeResult, +} + +impl Recipe { + pub fn pattern(&self) -> &[[[Option; 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; 3]; 3]> { + vec![[const { [const { None }; 3] }; 3]] + } + + fn result(self) -> RecipeResult { + self.result + } +} +impl From 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; 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, +} diff --git a/pumpkin-registry/src/recipe/recipe_formats.rs b/pumpkin-registry/src/recipe/recipe_formats.rs new file mode 100644 index 000000000..7c26376a0 --- /dev/null +++ b/pumpkin-registry/src/recipe/recipe_formats.rs @@ -0,0 +1,94 @@ +use super::super::recipe::RecipeType; +use super::read::{ + ingredients::IngredientSlot, CraftingType, RecipeKeys, RecipeResult, RecipeTrait, +}; +use itertools::Itertools; + +pub struct ShapedCrafting { + keys: RecipeKeys, + pattern: [[Option; 3]; 3], + output: RecipeResult, +} +impl RecipeKeys { + fn pattern_to_thing( + &self, + pattern: [[Option; 3]; 3], + ) -> [[Option; 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; 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; 3]; 3]> { + vec![self.keys.pattern_to_thing(self.pattern)] + } + + fn result(self) -> RecipeResult { + self.output + } +} + +pub struct ShapelessCrafting { + ingredients: Vec, + output: RecipeResult, +} + +impl ShapelessCrafting { + pub(crate) fn new(ingredients: Vec, 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 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; 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_vec() + } + + fn result(self) -> RecipeResult { + self.output + } +} diff --git a/pumpkin-registry/src/tags.rs b/pumpkin-registry/src/tags.rs new file mode 100644 index 000000000..0f356b6b8 --- /dev/null +++ b/pumpkin-registry/src/tags.rs @@ -0,0 +1,113 @@ +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; +use std::fmt::Formatter; +use std::sync::LazyLock; + +use crate::IngredientType; + +#[derive(Deserialize, Eq, PartialEq, Hash)] +pub enum TagCategory { + #[serde(rename = "minecraft:instrument")] + Instrument, + #[serde(rename = "minecraft:worldgen/biome")] + WorldGenBiome, + #[serde(rename = "minecraft:point_of_interest_type")] + PointOfInterest, + #[serde(rename = "minecraft:entity_type")] + Entity, + #[serde(rename = "minecraft:damage_type")] + DamageType, + #[serde(rename = "minecraft:banner_pattern")] + BannerPattern, + #[serde(rename = "minecraft:block")] + Block, + #[serde(rename = "minecraft:fluid")] + Fluid, + #[serde(rename = "minecraft:enchantment")] + Enchantment, + #[serde(rename = "minecraft:cat_variant")] + Cat, + #[serde(rename = "minecraft:painting_variant")] + Painting, + #[serde(rename = "minecraft:item")] + Item, + #[serde(rename = "minecraft:game_event")] + GameEvent, +} + +pub static TAGS: LazyLock>>> = + LazyLock::new(|| { + let mut map = HashMap::new(); + let tags_str = include_str!("../../assets/tags.json"); + let tags: Vec = + serde_json::from_str(tags_str).expect("Valid tag collections"); + for tag in tags { + map.insert(tag.name, tag.values); + } + map + }); + +pub fn get_tag_values(tag_category: TagCategory, tag: &str) -> Option<&Vec> { + TAGS.get(&tag_category) + .expect("Should deserialize all tag categories") + .get(tag) +} + +#[derive(Deserialize)] +pub struct TagCollection { + name: TagCategory, + #[serde(flatten)] + values: HashMap>, +} + +#[derive(Clone)] +pub enum TagType { + Item(String), + Tag(String), +} + +impl TagType { + pub fn to_ingredient_type(&self) -> IngredientType { + match self { + TagType::Tag(tag) => IngredientType::Tag(tag.to_string()), + TagType::Item(item) => IngredientType::Item(item.to_string()), + } + } +} + +impl<'de> Deserialize<'de> for TagType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TagVisitor; + impl<'de> Visitor<'de> for TagVisitor { + type Value = TagType; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "valid tag") + } + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + match v.strip_prefix('#') { + Some(v) => Ok(TagType::Tag(v.to_string())), + None => Ok(TagType::Item(v.to_string())), + } + } + } + deserializer.deserialize_str(TagVisitor) + } +} + +#[cfg(test)] +mod test { + use crate::tags::TAGS; + + #[test] + // This test assures that all tags that exist are loaded into the tags registry + fn load_tags() { + assert!(!TAGS.is_empty()) + } +} diff --git a/pumpkin/src/client/container.rs b/pumpkin/src/client/container.rs index 7c145c2e0..3a76aa13d 100644 --- a/pumpkin/src/client/container.rs +++ b/pumpkin/src/client/container.rs @@ -19,33 +19,31 @@ use pumpkin_protocol::VarInt; use pumpkin_world::item::ItemStack; use std::sync::Arc; -#[expect(unused)] - impl Player { pub async fn open_container(&self, server: &Server, window_type: WindowType) { - let inventory = self.inventory.lock().await; - inventory - .state_id - .store(0, std::sync::atomic::Ordering::Relaxed); - let total_opened_containers = inventory.total_opened_containers; - let container = self.get_open_container(server); - let container = container.as_ref().map(|container| container.lock()); - // TODO - let window_title = match container { - Some(container) => container.await.window_name(), - None => inventory.window_name(), + let mut inventory = self.inventory.lock().await; + inventory.state_id = 0; + inventory.total_opened_containers += 1; + let mut container = self.get_open_container(server).await; + let mut container = match container.as_mut() { + Some(container) => Some(container.lock().await), + None => None, }; + let window_title = container.as_ref().map_or_else( + || inventory.window_name(), + |container| container.window_name(), + ); let title = TextComponent::text(window_title); self.client .send_packet(&COpenScreen::new( - total_opened_containers.into(), + inventory.total_opened_containers.into(), VarInt(window_type as i32), title, )) .await; drop(inventory); - // self.set_container_content(container.as_deref_mut()); + self.set_container_content(container.as_deref_mut()).await; } pub async fn set_container_content(&self, container: Option<&mut Box>) { @@ -67,12 +65,10 @@ impl Player { .map_or_else(Slot::empty, std::convert::Into::into); // Gets the previous value - let i = inventory - .state_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + inventory.state_id += 1; let packet = CSetContainerContent::new( total_opened_containers.into(), - ((i + 1) as i32).into(), + (inventory.state_id as i32).into(), &slots, &carried_item, ); @@ -109,24 +105,24 @@ impl Player { server: &Arc, packet: SClickContainer, ) -> Result<(), InventoryError> { - let opened_container = self.get_open_container(server); - let opened_container = opened_container.as_ref().map(|container| container.lock()); + let opened_container = self.get_open_container(server).await; + let mut opened_container = match opened_container.as_ref() { + Some(container) => Some(container.lock().await), + None => None, + }; let drag_handler = &server.drag_handler; - let state_id = self - .inventory - .lock() - .await - .state_id - .load(std::sync::atomic::Ordering::Relaxed); + let state_id = self.inventory.lock().await.state_id; // This is just checking for regular desync, client hasn't done anything malicious if state_id != packet.state_id.0 as u32 { - // self.set_container_content(opened_container.as_deref_mut()); + self.set_container_content(opened_container.as_deref_mut()) + .await; return Ok(()); } if opened_container.is_some() { - if packet.window_id.0 != self.inventory.lock().await.total_opened_containers { + let total_containers = self.inventory.lock().await.total_opened_containers; + if packet.window_id.0 != total_containers { return Err(InventoryError::ClosedContainerInteract(self.entity_id())); } } else if packet.window_id.0 != 0 { @@ -134,7 +130,6 @@ impl Player { } let click = Click::new( - // TODO: This is very bad packet .mode .0 @@ -143,60 +138,54 @@ impl Player { packet.button, packet.slot, )?; + let (crafted_item, crafted_item_slot) = { + let mut inventory = self.inventory.lock().await; + let combined = + OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut()); + ( + combined.crafted_item_slot(), + combined.crafting_output_slot(), + ) + }; + let crafted_is_picked = crafted_item.is_some() + && match click.slot { + container_click::Slot::Normal(slot) => { + crafted_item_slot.is_some_and(|crafted_slot| crafted_slot == slot) + } + container_click::Slot::OutsideInventory => false, + }; let mut update_whole_container = false; - match click.click_type { - ClickType::MouseClick(mouse_click) => { - // self.mouse_click(opened_container.as_deref_mut(), mouse_click, click.slot).await - todo!() + let click_slot = click.slot; + self.match_click_behaviour( + opened_container.as_deref_mut(), + click, + drag_handler, + &mut update_whole_container, + crafted_is_picked, + ) + .await?; + // Checks for if crafted item has been taken + { + let mut inventory = self.inventory.lock().await; + let mut combined = + OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut()); + if combined.crafted_item_slot().is_none() && crafted_item.is_some() { + combined.recipe_used(); } - ClickType::ShiftClick => { - // self.shift_mouse_click(opened_container.as_deref_mut(), click.slot).await - todo!() + if combined.craft() { + drop(inventory); + self.set_container_content(opened_container.as_deref_mut()) + .await; } - ClickType::KeyClick(key_click) => { - todo!() - // container_click::Slot::Normal(slot) => { - // self.number_button_pressed(opened_container.as_deref_mut(), key_click, slot).await - // } - // container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket), - } - ClickType::CreativePickItem => { - // if let container_click::Slot::Normal(slot) = click.slot { - // self.creative_pick_item(opened_container.as_deref_mut(), slot).await - // } else { - // Err(InventoryError::InvalidPacket) - // } - todo!() - } - 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) - // } - todo!() - } - ClickType::MouseDrag { drag_state } => { - if drag_state == MouseDragState::End { - update_whole_container = true; - } - todo!() - // self.mouse_drag(drag_handler, opened_container.as_deref_mut(), drag_state) - } - ClickType::DropType(_drop_type) => { - log::debug!("todo"); - Ok(()) - } - }?; - if let Some(opened_container) = opened_container { + } + + 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 { + } else if let container_click::Slot::Normal(slot_index) = click_slot { let mut inventory = self.inventory.lock().await; - let mut opened_container = opened_container.await; let combined_container = OptionallyCombinedContainer::new(&mut inventory, Some(&mut opened_container)); if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) { @@ -210,19 +199,87 @@ impl Player { Ok(()) } + async fn match_click_behaviour( + &self, + opened_container: Option<&mut Box>, + click: Click, + drag_handler: &DragHandler, + update_whole_container: &mut bool, + using_crafting_slot: bool, + ) -> Result<(), InventoryError> { + match click.click_type { + ClickType::MouseClick(mouse_click) => { + self.mouse_click( + opened_container, + mouse_click, + click.slot, + using_crafting_slot, + ) + .await + } + ClickType::ShiftClick => { + self.shift_mouse_click(opened_container, click.slot, using_crafting_slot) + .await + } + ClickType::KeyClick(key_click) => match click.slot { + container_click::Slot::Normal(slot) => { + self.number_button_pressed( + opened_container, + key_click, + slot, + using_crafting_slot, + ) + .await + } + container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket), + }, + ClickType::CreativePickItem => { + if let container_click::Slot::Normal(slot) = click.slot { + self.creative_pick_item(opened_container, slot).await + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::DoubleClick => { + *update_whole_container = true; + if let container_click::Slot::Normal(slot) = click.slot { + self.double_click(opened_container, slot).await + } else { + Err(InventoryError::InvalidPacket) + } + } + ClickType::MouseDrag { drag_state } => { + if drag_state == MouseDragState::End { + *update_whole_container = true; + } + self.mouse_drag(drag_handler, opened_container, drag_state) + .await + } + ClickType::DropType(_drop_type) => { + log::debug!("todo"); + Ok(()) + } + } + } + async fn mouse_click( &self, opened_container: Option<&mut Box>, mouse_click: MouseClick, slot: container_click::Slot, + taking_crafted: bool, ) -> Result<(), InventoryError> { let mut inventory = self.inventory.lock().await; let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); - match slot { container_click::Slot::Normal(slot) => { let mut carried_item = self.carried_item.load(); - let res = container.handle_item_change(&mut carried_item, slot, mouse_click); + let res = container.handle_item_change( + &mut carried_item, + slot, + mouse_click, + taking_crafted, + ); self.carried_item.store(carried_item); res } @@ -234,6 +291,7 @@ impl Player { &self, opened_container: Option<&mut Box>, slot: container_click::Slot, + taking_crafted: bool, ) -> Result<(), InventoryError> { let mut inventory = self.inventory.lock().await; let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); @@ -267,7 +325,12 @@ impl Player { }; if let Some(slot) = slots { let mut item_slot = container.all_slots()[slot].map(|i| i); - container.handle_item_change(&mut item_slot, slot, MouseClick::Left)?; + container.handle_item_change( + &mut item_slot, + slot, + MouseClick::Left, + taking_crafted, + )?; *container.all_slots()[slot] = item_slot; } } @@ -282,6 +345,7 @@ impl Player { opened_container: Option<&mut Box>, key_click: KeyClick, slot: usize, + taking_crafted: bool, ) -> Result<(), InventoryError> { let changing_slot = match key_click { KeyClick::Slot(slot) => slot, @@ -291,7 +355,12 @@ impl Player { let mut changing_item_slot = inventory.get_slot(changing_slot as usize)?.to_owned(); let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); - container.handle_item_change(&mut changing_item_slot, slot, MouseClick::Left)?; + container.handle_item_change( + &mut changing_item_slot, + slot, + MouseClick::Left, + taking_crafted, + )?; *inventory.get_slot(changing_slot as usize)? = changing_item_slot; Ok(()) } @@ -436,16 +505,14 @@ impl Player { slot: Slot, ) -> Result<(), InventoryError> { for player in self.get_current_players_in_container(server).await { - let inventory = player.inventory.lock().await; + let mut inventory = player.inventory.lock().await; let total_opened_containers = inventory.total_opened_containers; // Returns previous value - let i = inventory - .state_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + inventory.state_id += 1; let packet = CSetContainerSlot::new( total_opened_containers as i8, - (i + 1) as i32, + (inventory.state_id) as i32, slot_index, &slot, ); @@ -458,18 +525,23 @@ impl Player { let players = self.get_current_players_in_container(server).await; for player in players { - let container = player.get_open_container(server); - let container = container.as_ref().map(|v| v.lock()); - // player.set_container_content(container.as_deref_mut()); + let container = player.get_open_container(server).await; + let mut container = match container.as_ref() { + Some(container) => Some(container.lock().await), + None => None, + }; + player.set_container_content(container.as_deref_mut()).await; } Ok(()) } - pub fn get_open_container( + pub async fn get_open_container( &self, server: &Server, ) -> Option>>> { - // self.open_container .load().map_or_else(|| None, |id| server.try_get_container(self.entity_id(), id).await) - None + match self.open_container.load() { + Some(id) => server.try_get_container(self.entity_id(), id).await, + None => None, + } } } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 17b45d3b0..ee750883e 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -234,7 +234,11 @@ impl Player { .await; } - pub async fn handle_chat_command(self: &Arc, server: &Server, command: SChatCommand) { + pub async fn handle_chat_command( + self: &Arc, + server: &Arc, + command: SChatCommand, + ) { let dispatcher = server.command_dispatcher.clone(); dispatcher .handle_command( @@ -615,15 +619,15 @@ impl Player { if self.gamemode.load() != GameMode::Creative { return Err(InventoryError::PermissionError); } - let valid_slot = packet.slot >= 1 && packet.slot <= 45; + let valid_slot = packet.slot >= 0 && packet.slot <= 45; if valid_slot { self.inventory.lock().await.set_slot( - packet.slot as u16, + packet.slot as usize, packet.clicked_item.to_item(), true, )?; }; - // TODO: The Item was droped per drag and drop, + // TODO: The Item was dropped per drag and drop, Ok(()) } @@ -636,11 +640,9 @@ impl Player { return; }; // window_id 0 represents both 9x1 Generic AND inventory here - self.inventory - .lock() - .await - .state_id - .store(0, std::sync::atomic::Ordering::Relaxed); + let mut inventory = self.inventory.lock().await; + + inventory.state_id = 0; let open_container = self.open_container.load(); if let Some(id) = open_container { let mut open_containers = server.open_containers.write().await; diff --git a/pumpkin/src/command/commands/cmd_craft.rs b/pumpkin/src/command/commands/cmd_craft.rs new file mode 100644 index 000000000..4ce64450c --- /dev/null +++ b/pumpkin/src/command/commands/cmd_craft.rs @@ -0,0 +1,45 @@ +use async_trait::async_trait; +use pumpkin_inventory::{CraftingTable, OpenContainer, WindowType}; + +use crate::command::{ + args::ConsumedArgs, tree::CommandTree, CommandExecutor, CommandSender, InvalidTreeError, +}; +const NAMES: [&str; 1] = ["craft"]; + +const DESCRIPTION: &str = "Open a crafting table"; + +struct CraftingTableExecutor {} + +#[async_trait] +impl CommandExecutor for CraftingTableExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + server: &crate::server::Server, + _args: &ConsumedArgs<'a>, + ) -> Result<(), InvalidTreeError> { + if let Some(player) = sender.as_player() { + let entity_id = player.entity_id(); + player.open_container.store(Some(1)); + { + let mut open_containers = server.open_containers.write().await; + if let Some(ender_chest) = open_containers.get_mut(&1) { + ender_chest.add_player(entity_id); + } else { + let open_container = + OpenContainer::new_empty_container::(entity_id); + open_containers.insert(1, open_container); + } + } + player + .open_container(server, WindowType::CraftingTable) + .await; + } + + Ok(()) + } +} + +pub fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).execute(&CraftingTableExecutor {}) +} diff --git a/pumpkin/src/command/commands/cmd_echest.rs b/pumpkin/src/command/commands/cmd_echest.rs index 20c9d7c8d..e75500c49 100644 --- a/pumpkin/src/command/commands/cmd_echest.rs +++ b/pumpkin/src/command/commands/cmd_echest.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use pumpkin_inventory::OpenContainer; +use pumpkin_inventory::{Chest, OpenContainer}; use crate::command::{ args::ConsumedArgs, tree::CommandTree, CommandExecutor, CommandSender, InvalidTreeError, @@ -28,7 +28,7 @@ impl CommandExecutor for EchestExecutor { if let Some(ender_chest) = open_containers.get_mut(&0) { ender_chest.add_player(entity_id); } else { - let open_container = OpenContainer::empty(entity_id); + let open_container = OpenContainer::new_empty_container::(entity_id); open_containers.insert(0, open_container); } } diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 330e46d50..a69674e46 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -1,4 +1,5 @@ pub mod cmd_clear; +pub mod cmd_craft; pub mod cmd_echest; pub mod cmd_gamemode; pub mod cmd_give; diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 2eda4f282..555835379 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use args::ConsumedArgs; use async_trait::async_trait; use commands::{ - cmd_clear, cmd_echest, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list, - cmd_pumpkin, cmd_say, cmd_stop, cmd_teleport, cmd_worldborder, + cmd_clear, cmd_craft, cmd_echest, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, + cmd_list, cmd_pumpkin, cmd_say, cmd_stop, cmd_teleport, cmd_worldborder, }; use dispatcher::InvalidTreeError; use pumpkin_core::math::vector3::Vector3; @@ -78,6 +78,7 @@ pub fn default_dispatcher<'a>() -> Arc> { dispatcher.register(cmd_stop::init_command_tree()); dispatcher.register(cmd_help::init_command_tree()); dispatcher.register(cmd_echest::init_command_tree()); + dispatcher.register(cmd_craft::init_command_tree()); dispatcher.register(cmd_kill::init_command_tree()); dispatcher.register(cmd_kick::init_command_tree()); dispatcher.register(cmd_worldborder::init_command_tree()); diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index ddcbe53f4..b575f2315 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -43,7 +43,7 @@ use pumpkin_protocol::{ use tokio::sync::{Mutex, Notify}; use tokio::task::JoinHandle; -use pumpkin_protocol::server::play::SKeepAlive; +use pumpkin_protocol::server::play::{SClickContainer, SKeepAlive}; use pumpkin_world::{ cylindrical_chunk_iterator::Cylindrical, item::{item_registry::Item, ItemStack}, @@ -832,6 +832,10 @@ impl Player { self.handle_player_command(SPlayerCommand::read(bytebuf)?) .await; } + SClickContainer::PACKET_ID => { + self.handle_click_container(server, SClickContainer::read(bytebuf)?) + .await?; + } SSetHeldItem::PACKET_ID => { self.handle_set_held_item(SSetHeldItem::read(bytebuf)?) .await; @@ -865,12 +869,10 @@ impl Player { let slot = (&*inventory.get_slot(slot_index)?).into(); // Returns previous value - let i = inventory - .state_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + inventory.state_id += 1; let packet = CSetContainerSlot::new( PlayerInventory::CONTAINER_ID, - (i + 1) as i32, + inventory.state_id as i32, slot_index, &slot, ); diff --git a/pumpkin/src/rcon/mod.rs b/pumpkin/src/rcon/mod.rs index fa87d4348..20bc12b03 100644 --- a/pumpkin/src/rcon/mod.rs +++ b/pumpkin/src/rcon/mod.rs @@ -60,7 +60,7 @@ impl RCONClient { } /// Returns if client is closed or not - pub async fn handle(&mut self, server: &Server, password: &str) -> bool { + pub async fn handle(&mut self, server: &Arc, password: &str) -> bool { if !self.closed { match self.read_bytes().await { // Stream closed, so we can't reply, so we just close everything. @@ -80,7 +80,7 @@ impl RCONClient { self.closed } - async fn poll(&mut self, server: &Server, password: &str) -> Result<(), PacketError> { + async fn poll(&mut self, server: &Arc, password: &str) -> Result<(), PacketError> { let Some(packet) = self.receive_packet().await? else { return Ok(()); }; diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 231ea4714..491aefc2b 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -18,8 +18,7 @@ use std::{ }, time::Duration, }; -use tokio::sync::Mutex; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use crate::client::EncryptionError; use crate::{ @@ -49,8 +48,8 @@ pub struct Server { pub worlds: Vec>, /// Caches game registries for efficient access. pub cached_registry: Vec, - pub open_containers: RwLock>, /// Tracks open containers used for item interactions. + pub open_containers: RwLock>, pub drag_handler: DragHandler, /// Assigns unique IDs to entities. entity_id: AtomicI32,