diff --git a/pumpkin-codegen/src/recipes.rs b/pumpkin-codegen/src/recipes.rs index dd3bbf1cb..597c7d65c 100644 --- a/pumpkin-codegen/src/recipes.rs +++ b/pumpkin-codegen/src/recipes.rs @@ -535,8 +535,9 @@ pub fn build() -> TokenStream { quote! { use crate::tag::Taggable; use crate::item::Item; + use serde::{Serialize, Deserialize}; - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub enum CraftingRecipeTypes { CraftingShaped { category: RecipeCategoryTypes, @@ -566,7 +567,7 @@ pub fn build() -> TokenStream { } #[allow(dead_code)] - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub struct CookingRecipe { /// Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore") pub recipe_id: &'static str, @@ -578,14 +579,14 @@ pub fn build() -> TokenStream { pub result: RecipeResultStruct, } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub enum CookingRecipeType { Blasting(CookingRecipe), Smelting(CookingRecipe), Smoking(CookingRecipe), CampfireCooking(CookingRecipe), } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub enum CookingRecipeKind { Blasting, Smelting, @@ -626,20 +627,20 @@ pub fn build() -> TokenStream { } } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub struct StonecutterRecipe { pub group: Option<&'static str>, pub ingredient: RecipeIngredientTypes, pub result: RecipeResultStruct, } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub struct RecipeResultStruct { pub id: &'static str, pub count: u8, } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub enum RecipeIngredientTypes { Simple(&'static str), Tagged(&'static str), @@ -664,7 +665,7 @@ pub fn build() -> TokenStream { } } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Serialize)] pub enum RecipeCategoryTypes { Equipment, Building, diff --git a/pumpkin-codegen/src/wit/java_packet.rs b/pumpkin-codegen/src/wit/java_packet.rs index 521330501..dc4dba89d 100644 --- a/pumpkin-codegen/src/wit/java_packet.rs +++ b/pumpkin-codegen/src/wit/java_packet.rs @@ -84,6 +84,23 @@ fn parse_packet_file(path: &Path, interface: &mut Interface, variant: &mut Varia if let Fields::Named(fields) = s.fields { for field in fields.named { + let type_name = match &field.ty { + syn::Type::Path(p) => p.path.segments.last().unwrap().ident.to_string(), + syn::Type::Reference(r) => match &*r.elem { + syn::Type::Slice(s) => match &*s.elem { + syn::Type::Path(p) => p.path.segments.last().unwrap().ident.to_string(), + _ => String::new(), + }, + syn::Type::Path(p) => p.path.segments.last().unwrap().ident.to_string(), + _ => String::new(), + }, + _ => String::new(), + }; + + if type_name == "DynamicRecipe" { + continue; + } + let field_name = field.ident.as_ref().unwrap().to_string().to_kebab_case(); let field_type = map_type(&field.ty); fields_list.push(Field::new(field_name, field_type)); diff --git a/pumpkin-codegen/src/wit/packet_mapping.rs b/pumpkin-codegen/src/wit/packet_mapping.rs index 767e34c7a..61e542ba6 100644 --- a/pumpkin-codegen/src/wit/packet_mapping.rs +++ b/pumpkin-codegen/src/wit/packet_mapping.rs @@ -125,6 +125,11 @@ fn parse_packet_file( let wit_field = field_name.to_snake_case(); let (type_ident, is_ref, is_slice) = get_type_info(&field.ty); + if type_ident == "DynamicRecipe" { + field_inits.push_str(&format!(" {}: &[],\n", field_name)); + continue; + } + if field_name == "type" { field_name = "r#type".to_string(); } diff --git a/pumpkin-data/src/generated/recipes.rs b/pumpkin-data/src/generated/recipes.rs index 01d02ab54..2e7ff6bac 100644 --- a/pumpkin-data/src/generated/recipes.rs +++ b/pumpkin-data/src/generated/recipes.rs @@ -1,7 +1,8 @@ /* This file is generated. Do not edit manually. */ use crate::item::Item; use crate::tag::Taggable; -#[derive(Clone, Debug)] +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Serialize)] pub enum CraftingRecipeTypes { CraftingShaped { category: RecipeCategoryTypes, @@ -30,7 +31,7 @@ pub enum CraftingRecipeTypes { CraftingSpecial, } #[allow(dead_code)] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct CookingRecipe { #[doc = r#" Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")"#] pub recipe_id: &'static str, @@ -41,14 +42,14 @@ pub struct CookingRecipe { pub experience: f32, pub result: RecipeResultStruct, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum CookingRecipeType { Blasting(CookingRecipe), Smelting(CookingRecipe), Smoking(CookingRecipe), CampfireCooking(CookingRecipe), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum CookingRecipeKind { Blasting, Smelting, @@ -85,18 +86,18 @@ impl CookingRecipeKind { } } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct StonecutterRecipe { pub group: Option<&'static str>, pub ingredient: RecipeIngredientTypes, pub result: RecipeResultStruct, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct RecipeResultStruct { pub id: &'static str, pub count: u8, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum RecipeIngredientTypes { Simple(&'static str), Tagged(&'static str), @@ -119,7 +120,7 @@ impl RecipeIngredientTypes { } } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum RecipeCategoryTypes { Equipment, Building, diff --git a/pumpkin-inventory/src/crafting/crafting_screen_handler.rs b/pumpkin-inventory/src/crafting/crafting_screen_handler.rs index 420b75bb8..142906186 100644 --- a/pumpkin-inventory/src/crafting/crafting_screen_handler.rs +++ b/pumpkin-inventory/src/crafting/crafting_screen_handler.rs @@ -19,6 +19,7 @@ use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::{AtomicU8, Ordering}; +use super::recipe_provider::{GenericRecipe, RecipeProvider}; use super::recipes::{RecipeFinderScreenHandler, RecipeInputInventory}; use crate::crafting::crafting_inventory::CraftingInventory; use crate::player::player_inventory::PlayerInventory; @@ -28,52 +29,42 @@ use crate::screen_handler::{ }; use crate::slot::{BoxFuture, NormalSlot, Slot}; -use crossbeam_utils::atomic::AtomicCell; use pumpkin_data::item_stack::ItemStack; -use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING, RecipeResultStruct}; +use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING}; use pumpkin_data::screen::WindowType; use pumpkin_data::tag; use pumpkin_data::tag::Taggable; +use pumpkin_protocol::codec::recipe::{DynamicRecipe, OwnedCraftingRecipe}; use pumpkin_world::inventory::Inventory; use tokio::sync::Mutex; /// The result slot in a crafting screen. -/// -/// This special slot displays the output of the current crafting recipe. -/// Unlike normal slots, it doesn't store items permanently - it calculates -/// the result dynamically based on the crafting grid contents. -/// -/// # Note -/// -/// This implementation differs from vanilla Minecraft. Instead of a separate -/// result inventory, we directly store the result in the slot. The slot -/// should never be modified directly - modifications to the crafting grid -/// automatically update the result. pub struct ResultSlot { /// The crafting inventory (grid) that provides recipe input. pub inventory: Arc, /// Protocol ID for this slot (assigned by screen handler). pub id: AtomicU8, /// The cached result item stack. - /// - /// Updated when the crafting grid changes and a recipe matches. pub result: Arc>, - /// Cached reference to the last matched recipe for quick re-matching. - recipe_cache: AtomicCell>, + /// Provider for dynamic recipes. + pub recipe_provider: Option>, +} + +pub struct RecipeResult { + pub item_id: String, + pub count: u8, } /// Checks if a recipe pattern is symmetrical horizontally. -/// -/// Used to try both orientations when matching shaped recipes. -fn is_symmetrical_horizontally(pattern: &'static [&'static str]) -> bool { +fn is_symmetrical_horizontally(pattern: &[&str]) -> bool { let width = pattern.first().map_or(0, |s| s.len()); for row in pattern { if row.len() != width { - return false; // All rows must have the same length + return false; } for j in 0..width / 2 { if row.chars().nth(j) != row.chars().nth(width - j - 1) { - return false; // Characters must match symmetrically + return false; } } } @@ -81,26 +72,23 @@ fn is_symmetrical_horizontally(pattern: &'static [&'static str]) -> bool { } /// Checks if a crafting recipe matches the current inventory state. -/// -/// Tries the recipe at all possible positions in the grid and handles -/// both orientations for asymmetrical recipes. #[expect(clippy::too_many_lines)] -async fn recipe_matches<'a>( - recipe: &'static CraftingRecipeTypes, +async fn recipe_matches( + recipe: GenericRecipe<'_>, input_height: usize, input_width: usize, top_x: usize, top_y: usize, count: usize, - inventory: &'a dyn RecipeInputInventory, -) -> Option<&'a RecipeResultStruct> { + inventory: &dyn RecipeInputInventory, +) -> Option { match recipe { - CraftingRecipeTypes::CraftingShaped { + GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingShaped { key, pattern, result, .. - } => { + }) => { if pattern.len() != input_height || pattern.first().unwrap().len() != input_width { return None; } @@ -118,11 +106,10 @@ async fn recipe_matches<'a>( let y_offset = top_y; let mut matched = true; - 'outer: for y in 0..pattern.len() { - for x in 0..pattern[y].len() { - let current_key = pattern[y].chars().nth(x).unwrap(); + 'outer: for (y, row_str) in pattern.iter().enumerate() { + for (x, current_key) in row_str.chars().enumerate() { let slot = inventory - .get_stack((y + y_offset) * inventory.get_height() + (x + x_offset)) + .get_stack((y + y_offset) * inventory.get_width() + (x + x_offset)) .await; if current_key == ' ' { if !slot.lock().await.is_empty() { @@ -146,13 +133,11 @@ async fn recipe_matches<'a>( } } - // Check for asymmetrical recipes if !matched && !is_symmetrical_horizontally(pattern) { matched = true; 'outer: for y in 0..pattern.len() { for x in 0..pattern[y].len() { let current_key = pattern[y].chars().nth(x).unwrap(); - let slot = inventory .get_stack( (y + y_offset) * inventory.get_height() @@ -166,14 +151,11 @@ async fn recipe_matches<'a>( } continue; } - let ingredient = key .iter() .find_map(|(k, v)| (*k == current_key).then_some(v)) .expect("Crafting recipe used invalid key"); - let slot = slot.lock().await; - if !ingredient.match_item(slot.item) { matched = false; break 'outer; @@ -182,75 +164,70 @@ async fn recipe_matches<'a>( } } - // TODO: Apply components - matched.then_some(result) + matched.then_some(RecipeResult { + item_id: result.id.to_string(), + count: result.count, + }) } - CraftingRecipeTypes::CraftingShapeless { + GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingShapeless { ingredients, result, .. - } => { + }) => { if count != ingredients.len() { return None; } - let mut ingredient_used = vec![false; ingredients.len()]; 'next_slot: for i in 0..inventory.size() { let slot = inventory.get_stack(i).await; let slot = slot.lock().await; - if slot.is_empty() { continue 'next_slot; } - for i in 0..ingredients.len() { if !ingredient_used[i] && ingredients[i].match_item(slot.item) { ingredient_used[i] = true; continue 'next_slot; } } - return None; } - - // TODO: Apply components - Some(result) + Some(RecipeResult { + item_id: result.id.to_string(), + count: result.count, + }) } - CraftingRecipeTypes::CraftingTransmute { + GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingTransmute { input, material, result, .. - } => { + }) => { if count != 2 { return None; } - 'item_stack: for i in 0..inventory.size() { let slot = inventory.get_stack(i).await; let slot = slot.lock().await; - if slot.is_empty() { continue 'item_stack; } - if !material.match_item(slot.item) && !input.match_item(slot.item) { return None; } } - - // TODO: Copy components - Some(result) + Some(RecipeResult { + item_id: result.id.to_string(), + count: result.count, + }) } - CraftingRecipeTypes::CraftingDecoratedPot { .. } => { + GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingDecoratedPot { .. }) => { if count != 4 || inventory.get_width() != 3 || inventory.get_height() != 3 { return None; } - for position in (1..=7).step_by(2) { let slot = inventory.get_stack(position).await; let slot = slot.lock().await; - if slot.is_empty() || !slot .item @@ -259,34 +236,106 @@ async fn recipe_matches<'a>( return None; } } - - // TODO: Handle side textures - Some(&RecipeResultStruct { - id: "minecraft:decorated_pot", + Some(RecipeResult { + item_id: "minecraft:decorated_pot".to_string(), count: 1, }) } - CraftingRecipeTypes::CraftingSpecial => None, + GenericRecipe::Dynamic(OwnedCraftingRecipe::Shaped { + pattern, + key, + result, + .. + }) => { + if pattern.len() != input_height || pattern.first().unwrap().len() != input_width { + return None; + } + if count + != pattern + .iter() + .map(|l| l.chars().filter(|c| *c != ' ').count()) + .sum::() + { + return None; + } + let x_offset = top_x; + let y_offset = top_y; + let mut matched = true; + 'outer: for (y, row_str) in pattern.iter().enumerate() { + for (x, current_key) in row_str.chars().enumerate() { + let slot = inventory + .get_stack((y + y_offset) * inventory.get_width() + (x + x_offset)) + .await; + if current_key == ' ' { + if !slot.lock().await.is_empty() { + matched = false; + break 'outer; + } + continue; + } + let ingredient = key + .iter() + .find(|(k, _)| *k == current_key) + .map(|(_, v)| v) + .expect("Crafting recipe used invalid key"); + let slot = slot.lock().await; + if !ingredient.match_item(slot.item) { + matched = false; + break 'outer; + } + } + } + matched.then_some(RecipeResult { + item_id: result.item_id.clone(), + count: result.count, + }) + } + GenericRecipe::Dynamic(OwnedCraftingRecipe::Shapeless { + ingredients, + result, + .. + }) => { + if count != ingredients.len() { + return None; + } + let mut ingredient_used = vec![false; ingredients.len()]; + 'next_slot: for i in 0..inventory.size() { + let slot = inventory.get_stack(i).await; + let slot = slot.lock().await; + if slot.is_empty() { + continue 'next_slot; + } + for i in 0..ingredients.len() { + if !ingredient_used[i] && ingredients[i].match_item(slot.item) { + ingredient_used[i] = true; + continue 'next_slot; + } + } + return None; + } + Some(RecipeResult { + item_id: result.item_id.clone(), + count: result.count, + }) + } + _ => None, } } impl ResultSlot { - //fn stat_crafted(&self, _crafted_amount: u8, _player: &dyn InventoryPlayer) {} - - /// Creates a new result slot for the given crafting inventory. - pub fn new(inventory: Arc) -> Self { + pub fn new( + inventory: Arc, + provider: Option>, + ) -> Self { Self { inventory, id: AtomicU8::new(0), result: Arc::new(Mutex::new(ItemStack::EMPTY.clone())), - recipe_cache: AtomicCell::new(None), + recipe_provider: provider, } } - /// Matches the recipe in the crafting inventory and returns the result. - /// - /// If no recipe matches, returns `None`. - async fn match_recipe(&self) -> Option<(&RecipeResultStruct, &'static CraftingRecipeTypes)> { + async fn match_recipe(&self) -> Option { let mut count: usize = 0; let inventory_width = self.inventory.get_width(); let mut top_x = 9; @@ -296,7 +345,6 @@ impl ResultSlot { for i in 0..self.inventory.size() { let x = i % inventory_width; let y = i / inventory_width; - let slot = self.inventory.get_stack(i).await; let slot = slot.lock().await; if !slot.is_empty() { @@ -307,31 +355,15 @@ impl ResultSlot { count += 1; } } - if count == 0 { return None; } let input_width = bottom_x + 1 - top_x; let input_height = bottom_y + 1 - top_y; - if let Some(cached_recipe) = self.recipe_cache.load() - && let Some(result) = recipe_matches( - cached_recipe, - input_height, - input_width, - top_x, - top_y, - count, - &*self.inventory, - ) - .await - { - return Some((result, cached_recipe)); - } - for recipe in RECIPES_CRAFTING { if let Some(result) = recipe_matches( - recipe, + GenericRecipe::Vanilla(recipe), input_height, input_width, top_x, @@ -341,20 +373,44 @@ impl ResultSlot { ) .await { - self.recipe_cache.store(Some(recipe)); - return Some((result, recipe)); + return Some(result); } } + if let Some(provider) = &self.recipe_provider { + let dynamic = provider.get_dynamic_recipes().await; + for recipe in &dynamic { + if let DynamicRecipe::Crafting(crafting) = recipe + && let Some(result) = recipe_matches( + GenericRecipe::Dynamic(crafting), + input_height, + input_width, + top_x, + top_y, + count, + &*self.inventory, + ) + .await + { + return Some(result); + } + } + } None } - /// Refills the output slot with the current recipe result. async fn refill_output(&self) -> ItemStack { - let result = self - .match_recipe() - .await - .map_or(ItemStack::EMPTY.clone(), |x| ItemStack::from(x.0)); + let result = if let Some(matched) = self.match_recipe().await { + let key = matched + .item_id + .strip_prefix("minecraft:") + .unwrap_or(&matched.item_id); + let item = pumpkin_data::item::Item::from_registry_key(key) + .unwrap_or(&pumpkin_data::item::Item::AIR); + ItemStack::new(matched.count, item) + } else { + ItemStack::EMPTY.clone() + }; *self.result.lock().await = result.clone(); result } @@ -364,26 +420,21 @@ impl Slot for ResultSlot { fn get_inventory(&self) -> Arc { self.inventory.clone() } - fn get_index(&self) -> usize { - 999 // this slot does not belong to any inventory + 999 } - fn set_id(&self, id: usize) { self.id.store(id as u8, Ordering::Relaxed); } - fn on_quick_move_crafted( &self, _stack: ItemStack, _stack_prev: ItemStack, ) -> BoxFuture<'_, ()> { Box::pin(async move { - // refill the result slot with the recipe result self.refill_output().await; }) } - fn on_take_item<'a>( &'a self, _player: &'a dyn InventoryPlayer, @@ -394,50 +445,39 @@ impl Slot for ResultSlot { let slot = self.inventory.get_stack(i).await; let mut stack = slot.lock().await; if !stack.is_empty() { - //TODO: Handle remaining items. stack.item_count -= 1; } } - // TODO - //self.stat_crafted(stack.item_count, player); self.mark_dirty().await; }) } - fn can_insert(&self, _stack: &ItemStack) -> BoxFuture<'_, bool> { Box::pin(async move { false }) } - fn get_stack(&self) -> BoxFuture<'_, Arc>> { Box::pin(async move { self.result.clone() }) } - fn get_cloned_stack(&self) -> BoxFuture<'_, ItemStack> { Box::pin(async move { self.result.lock().await.clone() }) } - fn has_stack(&self) -> BoxFuture<'_, bool> { Box::pin(async move { !self.result.lock().await.is_empty() }) } - fn set_stack(&self, _stack: ItemStack) -> BoxFuture<'_, ()> { Box::pin(async move { self.refill_output().await; }) } - fn set_stack_prev(&self, _stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> { Box::pin(async move { self.refill_output().await; }) } - fn mark_dirty(&self) -> BoxFuture<'_, ()> { Box::pin(async move { self.inventory.mark_dirty(); }) } - fn get_max_item_count(&self) -> BoxFuture<'_, u8> { Box::pin(async move { let mut count = u8::MAX; @@ -451,14 +491,10 @@ impl Slot for ResultSlot { count }) } - fn take_stack(&self, _amount: u8) -> BoxFuture<'_, ItemStack> { Box::pin(async move { if self.has_stack().await { - let stack = self.result.lock().await; - // Vanilla: net.minecraft.world.inventory.ResultContainer#removeItem - // Regardless of the amount, we always return the full stack - stack.clone() + self.result.lock().await.clone() } else { ItemStack::EMPTY.clone() } @@ -478,7 +514,6 @@ impl ScreenHandlerListener for ResultSlot { .contains(&(slot as usize)) { let result = self.refill_output().await; - let next_revision = screen_handler.next_revision(); if let Some(sync_handler) = screen_handler.sync_handler.as_ref() { sync_handler @@ -490,73 +525,52 @@ impl ScreenHandlerListener for ResultSlot { } } -/// Trait for crafting screen handlers. -/// -/// Provides common functionality for crafting UIs including slot setup -/// and recipe result management. -// AbstractCraftingScreenHandler.java pub trait CraftingScreenHandler: RecipeFinderScreenHandler + ScreenHandler { - /// Adds the result slot and crafting grid slots to the screen handler. fn add_recipe_slots<'a>( &'a mut self, crafing_inventory: Arc, + provider: Option>, ) -> Pin + Send + 'a>> { Box::pin(async move { - let result_slot = Arc::new(ResultSlot::new(crafing_inventory.clone())); + let result_slot = Arc::new(ResultSlot::new(crafing_inventory.clone(), provider)); self.add_slot(result_slot.clone()); - let width = crafing_inventory.get_width(); let height = crafing_inventory.get_height(); for i in 0..width { for j in 0..height { - // Assuming j + i * width is the correct slot index calculation let input_slot = NormalSlot::new(crafing_inventory.clone(), j + i * width); self.add_slot(Arc::new(input_slot)); } } - self.add_listener(result_slot).await; }) } } -/// Screen handler for the crafting table. -/// -/// The crafting table provides a 3x3 crafting grid and displays -/// the result of the current recipe configuration. -// CraftingMenu pub struct CraftingTableScreenHandler { - /// Core screen handler behavior (slots, sync ID, listeners). behaviour: ScreenHandlerBehaviour, - /// The 3x3 crafting grid inventory. crafting_inventory: Arc, } impl CraftingTableScreenHandler { - /// Creates a new crafting table screen handler. - /// - /// # Arguments - /// - `sync_id` - The sync ID for client-server matching - /// - `player_inventory` - The player's inventory - pub async fn new(sync_id: u8, player_inventory: &Arc) -> Self { + pub async fn new( + sync_id: u8, + player_inventory: &Arc, + provider: Option>, + ) -> Self { let crafting_inventory: Arc = Arc::new(CraftingInventory::new(3, 3)); - let mut crafting_table_handler = Self { behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Crafting)), crafting_inventory: crafting_inventory.clone(), }; - crafting_table_handler - .add_recipe_slots(crafting_inventory) + .add_recipe_slots(crafting_inventory, provider) .await; - - // Add player inventory slots let player_inventory: Arc = player_inventory.clone(); crafting_table_handler.add_player_slots(&player_inventory); - crafting_table_handler } } @@ -567,33 +581,22 @@ impl ScreenHandler for CraftingTableScreenHandler { fn as_any(&self) -> &dyn Any { self } - fn as_any_mut(&mut self) -> &mut dyn Any { self } - fn get_behaviour(&self) -> &ScreenHandlerBehaviour { &self.behaviour } - fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour { &mut self.behaviour } - fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> { Box::pin(async move { self.default_on_closed(player).await; - //TODO: this.craftingResultInventory.clear(); self.drop_inventory(player, self.crafting_inventory.clone()) .await; }) } - - /// Quick move logic for crafting table. - /// - /// - Result slot (0): Move to player inventory (10-46) - /// - Crafting grid (1-9): Move to player inventory - /// - Player inventory (10-46): Move to crafting grid first, then shuffle within inventory fn quick_move<'a>( &'a mut self, player: &'a dyn InventoryPlayer, @@ -601,74 +604,52 @@ impl ScreenHandler for CraftingTableScreenHandler { ) -> ItemStackFuture<'a> { Box::pin(async move { let slot = self.get_behaviour().slots[slot_index as usize].clone(); - if slot.has_stack().await { let slot_stack = slot.get_stack().await; let mut slot_stack = slot_stack.lock().await; let stack_prev = slot_stack.clone(); - if slot_index == 0 { - // From crafting result slot - move to player inventory (slots 10-46) if !self.insert_item(&mut slot_stack, 10, 46, true).await { return ItemStack::EMPTY.clone(); } } else if (1..=9).contains(&slot_index) { - // From crafting input slots - try to move to player inventory (slots 10-46) if !self.insert_item(&mut slot_stack, 10, 46, false).await { return ItemStack::EMPTY.clone(); } } else if (10..46).contains(&slot_index) { - // From player inventory - try to move to crafting input slots first (1-9) if !self.insert_item(&mut slot_stack, 1, 10, false).await { - // If that fails, try moving within player inventory if slot_index < 37 { - // From main inventory to hotbar if !self.insert_item(&mut slot_stack, 37, 46, false).await { return ItemStack::EMPTY.clone(); } - } else { - // From hotbar to main inventory - if !self.insert_item(&mut slot_stack, 10, 37, false).await { - return ItemStack::EMPTY.clone(); - } + } else if !self.insert_item(&mut slot_stack, 10, 37, false).await { + return ItemStack::EMPTY.clone(); } } - } else { - // Any other slot - try to move to player inventory - if !self.insert_item(&mut slot_stack, 10, 46, false).await { - return ItemStack::EMPTY.clone(); - } + } else if !self.insert_item(&mut slot_stack, 10, 46, false).await { + return ItemStack::EMPTY.clone(); } - let stack = slot_stack.clone(); - drop(slot_stack); // release the lock before calling other methods - + drop(slot_stack); if stack.is_empty() { slot.set_stack_prev(ItemStack::EMPTY.clone(), stack_prev.clone()) .await; } else { slot.mark_dirty().await; } - if stack.item_count == stack_prev.item_count { - // Nothing changed return ItemStack::EMPTY.clone(); } - slot.on_take_item(player, &stack).await; - if slot_index == 0 { slot.on_quick_move_crafted(stack.clone(), stack_prev.clone()) .await; - // For crafting result slot, drop any remaining items if !stack.is_empty() { player.drop_item(stack, false).await; } } - return stack_prev; } - ItemStack::EMPTY.clone() }) } diff --git a/pumpkin-inventory/src/crafting/mod.rs b/pumpkin-inventory/src/crafting/mod.rs index 4ff37f07d..0b8dae067 100644 --- a/pumpkin-inventory/src/crafting/mod.rs +++ b/pumpkin-inventory/src/crafting/mod.rs @@ -7,4 +7,5 @@ pub mod crafting_inventory; pub mod crafting_screen_handler; +pub mod recipe_provider; pub mod recipes; diff --git a/pumpkin-inventory/src/crafting/recipe_provider.rs b/pumpkin-inventory/src/crafting/recipe_provider.rs new file mode 100644 index 000000000..5a0224677 --- /dev/null +++ b/pumpkin-inventory/src/crafting/recipe_provider.rs @@ -0,0 +1,26 @@ +use crate::slot::BoxFuture; +use pumpkin_protocol::codec::recipe::DynamicRecipe; + +pub trait RecipeProvider: Send + Sync { + fn get_dynamic_recipes(&self) -> BoxFuture<'_, Vec>; +} + +pub enum GenericRecipe<'a> { + Vanilla(&'a pumpkin_data::recipes::CraftingRecipeTypes), + Dynamic(&'a pumpkin_protocol::codec::recipe::OwnedCraftingRecipe), +} + +pub enum IngredientRef<'a> { + Vanilla(&'a pumpkin_data::recipes::RecipeIngredientTypes), + Dynamic(&'a pumpkin_protocol::codec::recipe::OwnedRecipeIngredient), +} + +impl IngredientRef<'_> { + #[must_use] + pub fn match_item(&self, item: &pumpkin_data::item::Item) -> bool { + match self { + Self::Vanilla(v) => v.match_item(item), + Self::Dynamic(d) => d.match_item(item), + } + } +} diff --git a/pumpkin-inventory/src/player/player_screen_handler.rs b/pumpkin-inventory/src/player/player_screen_handler.rs index 0e34cbc22..a0bcb2963 100644 --- a/pumpkin-inventory/src/player/player_screen_handler.rs +++ b/pumpkin-inventory/src/player/player_screen_handler.rs @@ -81,6 +81,7 @@ impl PlayerScreenHandler { player_inventory: &Arc, window_type: Option, sync_id: u8, + provider: Option>, ) -> Self { let crafting_inventory: Arc = Arc::new(CraftingInventory::new(2, 2)); @@ -91,7 +92,7 @@ impl PlayerScreenHandler { }; player_screen_handler - .add_recipe_slots(crafting_inventory) + .add_recipe_slots(crafting_inventory, provider) .await; // Add armor slots (head, chest, legs, feet) diff --git a/pumpkin-plugin-wit/v0.1/recipe.wit b/pumpkin-plugin-wit/v0.1/recipe.wit new file mode 100644 index 000000000..8cbe61e26 --- /dev/null +++ b/pumpkin-plugin-wit/v0.1/recipe.wit @@ -0,0 +1,67 @@ +interface recipe { + use item-stack.{item-stack}; + + /// Represents an ingredient in a recipe. + variant ingredient { + /// A specific item. + item(string), + /// A tag group of items (e.g., "minecraft:logs"). + tag(string), + } + + /// Represents a shaped crafting recipe. + record shaped-recipe { + /// The pattern of the recipe (e.g., ["# #", " s ", "# #"]). + pattern: list, + /// Mapping of characters in the pattern to ingredients. + key: list>, + /// The resulting item stack. + output: item-stack, + /// The recipe group (optional). + group: option, + } + + /// Represents a shapeless crafting recipe. + record shapeless-recipe { + /// The list of ingredients required. + ingredients: list, + /// The resulting item stack. + output: item-stack, + /// The recipe group (optional). + group: option, + } + + /// Represents a cooking recipe (Smelting, Blasting, Smoking, Campfire). + record cooking-recipe { + /// The ingredient to cook. + ingredient: ingredient, + /// The resulting item stack. + output: item-stack, + /// Experience points awarded. + experience: f32, + /// Cooking time in ticks. + cooking-time: u32, + /// The recipe group (optional). + group: option, + } + + /// Types of cooking stations. + enum cooking-type { + smelting, + blasting, + smoking, + campfire, + } + + /// Common interface for registering recipes. + resource recipe-manager { + /// Registers a shaped crafting recipe. + register-shaped: func(id: string, recipe: shaped-recipe); + + /// Registers a shapeless crafting recipe. + register-shapeless: func(id: string, recipe: shapeless-recipe); + + /// Registers a cooking recipe. + register-cooking: func(id: string, station-type: cooking-type, recipe: cooking-recipe); + } +} diff --git a/pumpkin-plugin-wit/v0.1/server.wit b/pumpkin-plugin-wit/v0.1/server.wit index db3e9de67..80d6efd55 100644 --- a/pumpkin-plugin-wit/v0.1/server.wit +++ b/pumpkin-plugin-wit/v0.1/server.wit @@ -4,6 +4,7 @@ interface server { use %world.{%world}; use text.{text-component}; use uuid.{uuid}; + use recipe.{recipe-manager}; /// Represents the difficulty setting of a Minecraft server. enum difficulty { @@ -114,5 +115,8 @@ interface server { /// Returns the server's default gamemode. get-default-gamemode: func() -> game-mode; + + /// Returns the recipe manager to register custom recipes. + get-recipe-manager: func() -> recipe-manager; } } diff --git a/pumpkin-protocol/src/codec/mod.rs b/pumpkin-protocol/src/codec/mod.rs index 89460ba05..712272c80 100644 --- a/pumpkin-protocol/src/codec/mod.rs +++ b/pumpkin-protocol/src/codec/mod.rs @@ -5,6 +5,7 @@ pub mod item_stack_seralizer; pub mod little_endian; pub mod lp_vector_3d; pub mod optional_int; +pub mod recipe; mod u24_type; pub mod uuid; pub mod var_int; diff --git a/pumpkin-protocol/src/codec/recipe.rs b/pumpkin-protocol/src/codec/recipe.rs new file mode 100644 index 000000000..d38f3799a --- /dev/null +++ b/pumpkin-protocol/src/codec/recipe.rs @@ -0,0 +1,81 @@ +use pumpkin_data::recipes::RecipeCategoryTypes; +use serde::Serialize; + +use pumpkin_data::item::Item; +use pumpkin_data::tag::Taggable; + +#[derive(Clone, Debug, Serialize)] +pub enum OwnedRecipeIngredient { + Simple(String), + Tagged(String), + OneOf(Vec), +} + +impl OwnedRecipeIngredient { + #[must_use] + pub fn match_item(&self, item: &Item) -> bool { + match self { + Self::Simple(id) => { + let name = format!("minecraft:{}", item.registry_key); + name == *id + } + Self::Tagged(tag) => item + .is_tagged_with(tag) + .expect("Crafting recipe used invalid tag"), + Self::OneOf(ids) => { + let name = format!("minecraft:{}", item.registry_key); + ids.contains(&name) + } + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct OwnedRecipeResult { + pub item_id: String, + pub count: u8, + // TODO: Add components/enchantments if needed for the display result +} + +#[derive(Clone, Debug, Serialize)] +pub enum OwnedCraftingRecipe { + Shaped { + category: RecipeCategoryTypes, + group: Option, + show_notification: bool, + key: Vec<(char, OwnedRecipeIngredient)>, + pattern: Vec, + result: OwnedRecipeResult, + }, + Shapeless { + category: RecipeCategoryTypes, + group: Option, + ingredients: Vec, + result: OwnedRecipeResult, + }, +} + +#[derive(Clone, Debug, Serialize)] +pub struct OwnedCookingRecipe { + pub recipe_id: String, + pub category: RecipeCategoryTypes, + pub group: Option, + pub ingredient: OwnedRecipeIngredient, + pub cooking_time: i32, + pub experience: f32, + pub result: OwnedRecipeResult, +} + +#[derive(Clone, Debug, Serialize)] +pub enum OwnedCookingRecipeType { + Blasting(OwnedCookingRecipe), + Smelting(OwnedCookingRecipe), + Smoking(OwnedCookingRecipe), + CampfireCooking(OwnedCookingRecipe), +} + +#[derive(Clone, Debug, Serialize)] +pub enum DynamicRecipe { + Crafting(OwnedCraftingRecipe), + Cooking(OwnedCookingRecipeType), +} diff --git a/pumpkin-protocol/src/java/client/play/recipe_book_add.rs b/pumpkin-protocol/src/java/client/play/recipe_book_add.rs index 9ef3d4e56..8ace3a4c3 100644 --- a/pumpkin-protocol/src/java/client/play/recipe_book_add.rs +++ b/pumpkin-protocol/src/java/client/play/recipe_book_add.rs @@ -49,17 +49,23 @@ const CATEGORY_BLAST_FURNACE_MISC: i32 = 8; const CATEGORY_SMOKER_FOOD: i32 = 9; const CATEGORY_CAMPFIRE: i32 = 12; +use crate::codec::recipe::DynamicRecipe; + /// Clientbound packet that adds recipes to the client's recipe book. /// `replace = true` means the client replaces its current recipe list. #[java_packet(PLAY_RECIPE_BOOK_ADD)] -pub struct CRecipeBookAdd { +pub struct CRecipeBookAdd<'a> { pub replace: bool, + pub dynamic_recipes: &'a [DynamicRecipe], } -impl CRecipeBookAdd { +impl<'a> CRecipeBookAdd<'a> { #[must_use] - pub const fn new(replace: bool) -> Self { - Self { replace } + pub const fn new(replace: bool, dynamic_recipes: &'a [DynamicRecipe]) -> Self { + Self { + replace, + dynamic_recipes, + } } } @@ -255,20 +261,6 @@ fn write_optional_var_int(write: &mut impl Write, value: Option) -> Result< write.write_var_int(&VarInt(encoded))?; Ok(()) } - -fn resolve_group_id( - group_ids: &mut HashMap<&'static str, i32>, - next_group_id: &mut i32, - group: Option<&'static str>, -) -> Option { - let key = group?; - Some(*group_ids.entry(key).or_insert_with(|| { - let id = *next_group_id; - *next_group_id += 1; - id - })) -} - const fn entry_flags(replace: bool, notification: bool, highlight: bool) -> u8 { if replace { return 0; @@ -463,7 +455,10 @@ fn write_entry( Ok(false) } -impl ClientPacket for CRecipeBookAdd { +use std::borrow::Cow; + +#[allow(clippy::too_many_lines)] +impl ClientPacket for CRecipeBookAdd<'_> { fn write_packet_data( &self, write: impl Write, @@ -494,13 +489,14 @@ impl ClientPacket for CRecipeBookAdd { ) }) .count(); - let total = crafting_count + RECIPES_COOKING.len(); + let dynamic_count = self.dynamic_recipes.len(); + let total = crafting_count + RECIPES_COOKING.len() + dynamic_count; // Entry count (VarInt) write.write_var_int(&VarInt(total as i32))?; let mut display_id: i32 = 0; - let mut group_ids: HashMap<&'static str, i32> = HashMap::new(); + let mut group_ids: HashMap, i32> = HashMap::new(); let mut next_group_id: i32 = 0; let highlight = !self.replace; @@ -511,13 +507,15 @@ impl ClientPacket for CRecipeBookAdd { group, show_notification, .. - } => (*group, *show_notification), + } => (group.map(Cow::Borrowed), *show_notification), CraftingRecipeTypes::CraftingShapeless { group, .. } - | CraftingRecipeTypes::CraftingTransmute { group, .. } => (*group, true), + | CraftingRecipeTypes::CraftingTransmute { group, .. } => { + (group.map(Cow::Borrowed), true) + } CraftingRecipeTypes::CraftingDecoratedPot { .. } | CraftingRecipeTypes::CraftingSpecial => (None, true), }; - let group_id = resolve_group_id(&mut group_ids, &mut next_group_id, group); + let group_id = resolve_group_id_owned(&mut group_ids, &mut next_group_id, group); let flags = entry_flags(self.replace, notification, highlight); let written = write_entry( &mut write, @@ -559,7 +557,11 @@ impl ClientPacket for CRecipeBookAdd { CookingRecipeType::Smoking(r) => (CATEGORY_SMOKER_FOOD, r.group), CookingRecipeType::CampfireCooking(r) => (CATEGORY_CAMPFIRE, r.group), }; - let group_id = resolve_group_id(&mut group_ids, &mut next_group_id, group); + let group_id = resolve_group_id_owned( + &mut group_ids, + &mut next_group_id, + group.map(Cow::Borrowed), + ); let flags = entry_flags(self.replace, true, highlight); write_entry( &mut write, @@ -578,8 +580,318 @@ impl ClientPacket for CRecipeBookAdd { display_id += 1; } + // Write dynamic recipes + for recipe in self.dynamic_recipes { + match recipe { + DynamicRecipe::Crafting(crafting) => { + let (group, flags) = match crafting { + crate::codec::recipe::OwnedCraftingRecipe::Shaped { + group, + show_notification, + .. + } => ( + group.as_deref().map(Cow::Borrowed), + entry_flags(self.replace, *show_notification, highlight), + ), + crate::codec::recipe::OwnedCraftingRecipe::Shapeless { group, .. } => ( + group.as_deref().map(Cow::Borrowed), + entry_flags(self.replace, true, highlight), + ), + }; + let group_id = + resolve_group_id_owned(&mut group_ids, &mut next_group_id, group); + write_dynamic_crafting_entry( + &mut write, + display_id, + *version, + group_id, + flags, + crafting_table, + crafting, + )?; + } + DynamicRecipe::Cooking(cooking) => { + let (book_category, group, owned_cooking) = match cooking { + crate::codec::recipe::OwnedCookingRecipeType::Smelting(r) => ( + match r.category { + RecipeCategoryTypes::Food => CATEGORY_FURNACE_FOOD, + RecipeCategoryTypes::Blocks => CATEGORY_FURNACE_BLOCKS, + _ => CATEGORY_FURNACE_MISC, + }, + r.group.as_deref().map(Cow::Borrowed), + r, + ), + crate::codec::recipe::OwnedCookingRecipeType::Blasting(r) => ( + match r.category { + RecipeCategoryTypes::Blocks => CATEGORY_BLAST_FURNACE_BLOCKS, + _ => CATEGORY_BLAST_FURNACE_MISC, + }, + r.group.as_deref().map(Cow::Borrowed), + r, + ), + crate::codec::recipe::OwnedCookingRecipeType::Smoking(r) => ( + CATEGORY_SMOKER_FOOD, + r.group.as_deref().map(Cow::Borrowed), + r, + ), + crate::codec::recipe::OwnedCookingRecipeType::CampfireCooking(r) => { + (CATEGORY_CAMPFIRE, r.group.as_deref().map(Cow::Borrowed), r) + } + }; + let station = match cooking { + crate::codec::recipe::OwnedCookingRecipeType::Smelting(_) => furnace, + crate::codec::recipe::OwnedCookingRecipeType::Blasting(_) => blast_furnace, + crate::codec::recipe::OwnedCookingRecipeType::Smoking(_) => smoker, + crate::codec::recipe::OwnedCookingRecipeType::CampfireCooking(_) => { + campfire + } + }; + + let group_id = + resolve_group_id_owned(&mut group_ids, &mut next_group_id, group); + let flags = entry_flags(self.replace, true, highlight); + write_dynamic_cooking_entry( + &mut write, + display_id, + *version, + group_id, + flags, + station, + owned_cooking, + book_category, + )?; + } + } + display_id += 1; + } + // replace flag write.write_bool(self.replace)?; Ok(()) } } + +fn resolve_group_id_owned<'a>( + group_ids: &mut HashMap, i32>, + next_group_id: &mut i32, + group: Option>, +) -> Option { + let key = group?; + Some(*group_ids.entry(key).or_insert_with(|| { + let id = *next_group_id; + *next_group_id += 1; + id + })) +} + +fn write_dynamic_ingredient_slot_display( + write: &mut impl Write, + ingredient: &crate::codec::recipe::OwnedRecipeIngredient, + version: JavaMinecraftVersion, +) -> Result<(), WritingError> { + match ingredient { + crate::codec::recipe::OwnedRecipeIngredient::Simple(id) => { + let key = id.strip_prefix("minecraft:").unwrap_or(id); + if let Some(item) = Item::from_registry_key(key) { + write_item_slot_display(write, item, version)?; + } else { + write_empty_slot_display(write)?; + } + } + crate::codec::recipe::OwnedRecipeIngredient::Tagged(_tag) => { + write_empty_slot_display(write)?; + } + crate::codec::recipe::OwnedRecipeIngredient::OneOf(ids) => { + let items: Vec<&Item> = ids + .iter() + .filter_map(|id| { + let key = id.strip_prefix("minecraft:").unwrap_or(id); + Item::from_registry_key(key) + }) + .collect(); + + if items.is_empty() { + write_empty_slot_display(write)?; + } else if items.len() == 1 { + write_item_slot_display(write, items[0], version)?; + } else { + write.write_var_int(&VarInt(slot_display_composite_type(version)))?; + write.write_var_int(&VarInt(items.len() as i32))?; + for item in &items { + write_item_slot_display(write, item, version)?; + } + } + } + } + Ok(()) +} + +fn write_dynamic_ingredient_holderset( + write: &mut impl Write, + ingredient: Option<&crate::codec::recipe::OwnedRecipeIngredient>, + version: JavaMinecraftVersion, +) -> Result<(), WritingError> { + match ingredient { + None => { + write.write_var_int(&VarInt(1))?; + } + Some(crate::codec::recipe::OwnedRecipeIngredient::Simple(id)) => { + let key = id.strip_prefix("minecraft:").unwrap_or(id); + if let Some(item) = Item::from_registry_key(key) { + write.write_var_int(&VarInt(2))?; + write.write_var_int(&VarInt(item_id_versioned(item, version)))?; + } else { + write.write_var_int(&VarInt(1))?; + } + } + Some(crate::codec::recipe::OwnedRecipeIngredient::Tagged(_tag)) => { + write.write_var_int(&VarInt(1))?; + } + Some(crate::codec::recipe::OwnedRecipeIngredient::OneOf(ids)) => { + let items: Vec = ids + .iter() + .filter_map(|id| { + let key = id.strip_prefix("minecraft:").unwrap_or(id); + Item::from_registry_key(key).map(|item| item_id_versioned(item, version)) + }) + .collect(); + write.write_var_int(&VarInt(items.len() as i32 + 1))?; + for id in &items { + write.write_var_int(&VarInt(*id))?; + } + } + } + Ok(()) +} + +fn write_dynamic_result_slot_display( + write: &mut impl Write, + result: &crate::codec::recipe::OwnedRecipeResult, + version: JavaMinecraftVersion, +) -> Result<(), WritingError> { + let key = result + .item_id + .strip_prefix("minecraft:") + .unwrap_or(&result.item_id); + if let Some(item) = Item::from_registry_key(key) { + write_item_stack_slot_display(write, item, result.count, version)?; + } else { + write_empty_slot_display(write)?; + } + Ok(()) +} + +fn write_dynamic_crafting_entry( + write: &mut impl Write, + display_id: i32, + version: JavaMinecraftVersion, + group_id: Option, + flags: u8, + crafting_table: &Item, + recipe: &crate::codec::recipe::OwnedCraftingRecipe, +) -> Result<(), WritingError> { + match recipe { + crate::codec::recipe::OwnedCraftingRecipe::Shaped { + category, + pattern, + key, + result, + .. + } => { + let height = pattern.len() as i32; + let width = pattern.first().map_or(0, String::len) as i32; + + write.write_var_int(&VarInt(display_id))?; + write.write_var_int(&VarInt(RECIPE_DISPLAY_SHAPED))?; + write.write_var_int(&VarInt(width))?; + write.write_var_int(&VarInt(height))?; + write.write_var_int(&VarInt(width * height))?; + for row in pattern { + for ch in row.chars() { + if ch == ' ' { + write_empty_slot_display(write)?; + } else if let Some((_, ingredient)) = key.iter().find(|(k, _)| *k == ch) { + write_dynamic_ingredient_slot_display(write, ingredient, version)?; + } else { + write_empty_slot_display(write)?; + } + } + } + write_dynamic_result_slot_display(write, result, version)?; + write_item_slot_display(write, crafting_table, version)?; + write_optional_var_int(write, group_id)?; + write.write_var_int(&VarInt(crafting_category(category)))?; + + write.write_bool(true)?; // present + let mut non_empty_slots = 0; + for row in pattern { + for ch in row.chars() { + if ch != ' ' { + non_empty_slots += 1; + } + } + } + write.write_var_int(&VarInt(non_empty_slots))?; + for row in pattern { + for ch in row.chars() { + if ch != ' ' { + let ing = key.iter().find(|(k, _)| *k == ch).map(|(_, i)| i); + write_dynamic_ingredient_holderset(write, ing, version)?; + } + } + } + write.write_u8(flags)?; + } + crate::codec::recipe::OwnedCraftingRecipe::Shapeless { + category, + ingredients, + result, + .. + } => { + write.write_var_int(&VarInt(display_id))?; + write.write_var_int(&VarInt(RECIPE_DISPLAY_SHAPELESS))?; + write.write_var_int(&VarInt(ingredients.len() as i32))?; + for ing in ingredients { + write_dynamic_ingredient_slot_display(write, ing, version)?; + } + write_dynamic_result_slot_display(write, result, version)?; + write_item_slot_display(write, crafting_table, version)?; + write_optional_var_int(write, group_id)?; + write.write_var_int(&VarInt(crafting_category(category)))?; + + write.write_bool(true)?; + write.write_var_int(&VarInt(ingredients.len() as i32))?; + for ing in ingredients { + write_dynamic_ingredient_holderset(write, Some(ing), version)?; + } + write.write_u8(flags)?; + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn write_dynamic_cooking_entry( + write: &mut impl Write, + display_id: i32, + version: JavaMinecraftVersion, + group_id: Option, + flags: u8, + station: &Item, + cooking: &crate::codec::recipe::OwnedCookingRecipe, + book_category: i32, +) -> Result<(), WritingError> { + write.write_var_int(&VarInt(display_id))?; + write.write_var_int(&VarInt(RECIPE_DISPLAY_FURNACE))?; + write_dynamic_ingredient_slot_display(write, &cooking.ingredient, version)?; + write_any_fuel_slot_display(write)?; + write_dynamic_result_slot_display(write, &cooking.result, version)?; + write_item_slot_display(write, station, version)?; + write.write_var_int(&VarInt(cooking.cooking_time))?; + write.write_f32_be(cooking.experience)?; + write_optional_var_int(write, group_id)?; + write.write_var_int(&VarInt(book_category))?; + write_dynamic_ingredient_holderset(write, Some(&cooking.ingredient), version)?; + write.write_u8(flags)?; + Ok(()) +} diff --git a/pumpkin/src/block/blocks/crafting_table.rs b/pumpkin/src/block/blocks/crafting_table.rs index 89faefcb5..f585053a6 100644 --- a/pumpkin/src/block/blocks/crafting_table.rs +++ b/pumpkin/src/block/blocks/crafting_table.rs @@ -19,7 +19,10 @@ impl BlockBehaviour for CraftingTableBlock { fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { Box::pin(async move { args.player - .open_handled_screen(&CraftingTableScreenFactory, Some(*args.position)) + .open_handled_screen( + &CraftingTableScreenFactory(args.server.recipe_manager.clone()), + Some(*args.position), + ) .await; BlockActionResult::Success @@ -27,7 +30,7 @@ impl BlockBehaviour for CraftingTableBlock { } } -struct CraftingTableScreenFactory; +struct CraftingTableScreenFactory(Arc); impl ScreenHandlerFactory for CraftingTableScreenFactory { fn create_screen_handler<'a>( @@ -37,7 +40,9 @@ impl ScreenHandlerFactory for CraftingTableScreenFactory { _player: &'a dyn InventoryPlayer, ) -> BoxFuture<'a, Option> { Box::pin(async move { - let handler = CraftingTableScreenHandler::new(sync_id, player_inventory).await; + let handler = + CraftingTableScreenHandler::new(sync_id, player_inventory, Some(self.0.clone())) + .await; let concrete_arc = Arc::new(Mutex::new(handler)); Some(concrete_arc as SharedScreenHandler) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 9a3e2e75f..cffb66458 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -536,7 +536,13 @@ impl Player { let ender_chest_inventory = Arc::new(EnderChestInventory::new()); let player_screen_handler = Arc::new(Mutex::new( - PlayerScreenHandler::new(&inventory, None, 0).await, + PlayerScreenHandler::new( + &inventory, + None, + 0, + Some(world.server.upgrade().unwrap().recipe_manager.clone()), + ) + .await, )); // Initialize abilities based on gamemode (like vanilla's GameMode.setAbilities()) diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index 0f2e97d16..d4a264a24 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -60,6 +60,7 @@ pub mod config; pub mod handshake; pub mod login; pub mod play; +pub mod recipe_helper; pub mod status; use crate::entity::player::Player; @@ -945,8 +946,8 @@ impl JavaClient { .await; } id if id == SPlaceRecipe::to_id(version) => { - self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?) - .await; + let packet = SPlaceRecipe::read(payload, &version)?; + self.handle_place_recipe(server, player, packet).await; } id if id == pumpkin_protocol::java::server::play::SCustomClickAction::to_id(version) => diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index e771321da..443973e54 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -926,80 +926,19 @@ impl JavaClient { } #[allow(clippy::too_many_lines)] - pub async fn handle_place_recipe(&self, player: &Arc, packet: SPlaceRecipe) { - use pumpkin_data::recipes::{ - CookingRecipeType, CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING, - RecipeIngredientTypes, + pub async fn handle_place_recipe( + &self, + server: &Arc, + player: &Arc, + packet: SPlaceRecipe, + ) { + use super::recipe_helper::{ + GenericIngredient, compute_biggest_craftable, take_n_ingredient, }; + use crate::server::recipe::DynamicRecipe; + use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING}; use pumpkin_data::screen::WindowType; - - // Take `amount` items matching `ingredient` from inventory, committing to one item type. - async fn take_n_ingredient( - inventory: &PlayerInventory, - ingredient: &RecipeIngredientTypes, - amount: u8, - ) -> ItemStack { - let mut remaining = amount; - let mut result: Option = None; - for slot in &inventory.main_inventory { - if remaining == 0 { - break; - } - let mut stack = slot.lock().await; - if stack.is_empty() || !ingredient.match_item(stack.item) { - continue; - } - // Commit to the first item type encountered. - if result.as_ref().is_some_and(|r| r.item.id != stack.item.id) { - continue; - } - let take = remaining.min(stack.item_count); - let taken = stack.split(take); - remaining -= taken.item_count; - match &mut result { - None => result = Some(taken), - Some(r) => r.increment(taken.item_count), - } - } - result.unwrap_or_else(|| ItemStack::EMPTY.clone()) - } - - // Compute the maximum number of times all ingredients can be supplied from inventory. - // Each ingredient slot commits to one item type (no mixing within a slot). - async fn compute_biggest_craftable( - ingredients: &[&RecipeIngredientTypes], - inventory: &PlayerInventory, - ) -> u8 { - // Aggregate inventory by item id -> (item, count). - let mut available: Vec<(&'static pumpkin_data::item::Item, u32)> = Vec::new(); - for slot in &inventory.main_inventory { - let stack = slot.lock().await; - if !stack.is_empty() { - if let Some(e) = available.iter_mut().find(|(i, _)| i.id == stack.item.id) { - e.1 += u32::from(stack.item_count); - } else { - available.push((stack.item, u32::from(stack.item_count))); - } - } - } - - // From 64 down to 1, find the highest amount where every slot can be satisfied. - 'outer: for amount in (1u32..=64).rev() { - let mut budget = available.clone(); - for ing in ingredients { - // Pick the first item type that satisfies this slot and has enough. - let Some(idx) = budget - .iter() - .position(|(item, count)| *count >= amount && ing.match_item(item)) - else { - continue 'outer; - }; - budget[idx].1 -= amount; - } - return amount as u8; - } - 0 - } + use pumpkin_inventory::crafting::recipe_provider::RecipeProvider; let target_id = packet.recipe_display_id.0 as usize; let use_max = packet.use_max_items; @@ -1015,6 +954,8 @@ impl JavaClient { ) }) .count(); + let cooking_display_count = RECIPES_COOKING.len(); + let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; let (grid_width, crafting_inv) = { let screen_handler_arc = player.current_screen_handler.lock().await.clone(); @@ -1027,6 +968,9 @@ impl JavaClient { (grid_width, handler.get_behaviour().slots[1].get_inventory()) }; + let grid_size = grid_width * grid_width; + let mut ingredient_slots: Vec>> = vec![None; grid_size]; + if target_id < crafting_display_count { // Crafting recipe let mut counter = 0usize; @@ -1044,10 +988,6 @@ impl JavaClient { }); let Some(recipe) = recipe else { return }; - let grid_size = grid_width * grid_width; - - // Map each grid position to its required ingredient (None = empty/unused). - let mut ingredient_slots: Vec> = vec![None; grid_size]; match recipe { CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => { for (row, row_str) in pattern.iter().enumerate() { @@ -1055,159 +995,147 @@ impl JavaClient { if ch != ' ' && let Some(ing) = key.iter().find_map(|(k, v)| (*k == ch).then_some(v)) + && row * grid_width + col < grid_size { - ingredient_slots[row * grid_width + col] = Some(ing); + ingredient_slots[row * grid_width + col] = + Some(GenericIngredient::Vanilla(ing)); } } } } CraftingRecipeTypes::CraftingShapeless { ingredients, .. } => { for (i, ing) in ingredients.iter().enumerate().take(grid_size) { - ingredient_slots[i] = Some(ing); + ingredient_slots[i] = Some(GenericIngredient::Vanilla(ing)); } } CraftingRecipeTypes::CraftingTransmute { input, material, .. } => { - ingredient_slots[0] = Some(input); - ingredient_slots[1] = Some(material); + if grid_size >= 2 { + ingredient_slots[0] = Some(GenericIngredient::Vanilla(input)); + ingredient_slots[1] = Some(GenericIngredient::Vanilla(material)); + } } _ => return, } + } else if target_id < crafting_display_count + cooking_display_count { + // TODO: cooking recipes + return; + } else { + let dynamic_id = target_id - crafting_display_count - cooking_display_count; + let Some(DynamicRecipe::Crafting(crafting)) = dynamic_recipes.get(dynamic_id) else { + return; + }; - // Check if this exact recipe is already placed (determines stacking vs fresh fill). - let recipe_matches = { - let mut ok = true; - for (idx, ing) in ingredient_slots.iter().enumerate() { + match crafting { + pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shaped { + pattern, + key, + .. + } => { + for (row, row_str) in pattern.iter().enumerate() { + for (col, ch) in row_str.chars().enumerate() { + if ch != ' ' + && let Some((_, ing)) = key.iter().find(|(k, _)| *k == ch) + && row * grid_width + col < grid_size + { + ingredient_slots[row * grid_width + col] = + Some(GenericIngredient::Dynamic(ing)); + } + } + } + } + + pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shapeless { + ingredients, + .. + } => { + for (i, ing) in ingredients.iter().enumerate().take(grid_size) { + ingredient_slots[i] = Some(GenericIngredient::Dynamic(ing)); + } + } + } + } + + // Check if this exact recipe is already placed (determines stacking vs fresh fill). + let recipe_matches = { + let mut ok = true; + for (idx, ing) in ingredient_slots.iter().enumerate() { + let slot_arc = crafting_inv.get_stack(idx).await; + let stack = slot_arc.lock().await; + match ing { + None => { + if !stack.is_empty() { + ok = false; + break; + } + } + Some(ingredient) => { + if stack.is_empty() || !ingredient.match_item(stack.item) { + ok = false; + break; + } + } + } + } + ok + }; + + // Read minimum count from occupied slots before clearing (needed for stacking). + let current_min = if recipe_matches && !use_max { + let mut min = u8::MAX; + for (idx, ing) in ingredient_slots.iter().enumerate() { + if ing.is_some() { let slot_arc = crafting_inv.get_stack(idx).await; let stack = slot_arc.lock().await; - match ing { - None => { - if !stack.is_empty() { - ok = false; - break; - } - } - Some(ingredient) => { - if stack.is_empty() || !ingredient.match_item(stack.item) { - ok = false; - break; - } - } + if !stack.is_empty() { + min = min.min(stack.item_count); } } - ok - }; - - // Read minimum count from occupied slots before clearing (needed for stacking). - let current_min = if recipe_matches && !use_max { - let mut min = u8::MAX; - for (idx, ing) in ingredient_slots.iter().enumerate() { - if ing.is_some() { - let slot_arc = crafting_inv.get_stack(idx).await; - let stack = slot_arc.lock().await; - if !stack.is_empty() { - min = min.min(stack.item_count); - } - } - } - if min == u8::MAX { 0 } else { min } - } else { - 0 - }; - - // Always clear the grid first, returning items to inventory. - for i in 0..grid_size { - let stack = crafting_inv.remove_stack(i).await; - if !stack.is_empty() { - player.inventory.offer(stack, false, player.as_ref()).await; - } - } - - // Determine how many of each ingredient to place per slot. - let active_ingredients: Vec<&RecipeIngredientTypes> = - ingredient_slots.iter().flatten().copied().collect(); - let amount_to_craft = if use_max { - compute_biggest_craftable(&active_ingredients, &player.inventory).await - } else if recipe_matches { - current_min.saturating_add(1) - } else { - 1 - }; - - if amount_to_craft == 0 { - let screen_handler_arc = player.current_screen_handler.lock().await.clone(); - screen_handler_arc.lock().await.send_content_updates().await; - return; - } - - // Fill each grid slot with exactly `amount_to_craft` matching items. - for (idx, ing) in ingredient_slots.iter().enumerate() { - let Some(ingredient) = ing else { continue }; - let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await; - if !taken.is_empty() { - crafting_inv.set_stack(idx, taken).await; - } } + if min == u8::MAX { 0 } else { min } } else { - // Cooking recipe - let cooking_index = target_id - crafting_display_count; - let Some(recipe) = RECIPES_COOKING.get(cooking_index) else { - return; - }; + 0 + }; - let furnace_inv = { - let screen_handler_arc = player.current_screen_handler.lock().await.clone(); - let handler = screen_handler_arc.lock().await; - match handler.window_type() { - Some(WindowType::Furnace | WindowType::BlastFurnace | WindowType::Smoker) => { - handler.get_behaviour().slots[0].get_inventory() - } - _ => return, - } - }; - - let ingredient = match recipe { - CookingRecipeType::Smelting(r) - | CookingRecipeType::Blasting(r) - | CookingRecipeType::Smoking(r) - | CookingRecipeType::CampfireCooking(r) => &r.ingredient, - }; - - // Check if ingredient already matches (for stacking). - let (recipe_matches, current_count) = { - let slot_arc = furnace_inv.get_stack(0).await; - let stack = slot_arc.lock().await; - let matches = !stack.is_empty() && ingredient.match_item(stack.item); - let count = if matches { stack.item_count } else { 0 }; - (matches, count) - }; - - // Always clear slot 0 first, returning item to inventory. - let old = furnace_inv.remove_stack(0).await; - if !old.is_empty() { - player.inventory.offer(old, false, player.as_ref()).await; + // Always clear the grid first, returning items to inventory. + for i in 0..grid_size { + let stack = crafting_inv.remove_stack(i).await; + if !stack.is_empty() { + player.inventory.offer(stack, false, player.as_ref()).await; } + } - let amount_to_craft = if use_max { - compute_biggest_craftable(&[ingredient], &player.inventory).await - } else if recipe_matches { - current_count.saturating_add(1) - } else { - 1 - }; + // Determine how many of each ingredient to place per slot. + let active_ingredients: Vec> = + ingredient_slots.iter().flatten().copied().collect(); + let amount_to_craft = if use_max { + compute_biggest_craftable(&active_ingredients, &player.inventory).await + } else if recipe_matches { + current_min.saturating_add(1) + } else { + 1 + }; - if amount_to_craft > 0 { - let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await; - if !taken.is_empty() { - furnace_inv.set_stack(0, taken).await; - } + if amount_to_craft == 0 { + let screen_handler_arc = player.current_screen_handler.lock().await.clone(); + screen_handler_arc.lock().await.send_content_updates().await; + return; + } + + // Fill each grid slot with exactly `amount_to_craft` matching items. + for (idx, ing) in ingredient_slots.iter().enumerate() { + let Some(ingredient) = ing else { continue }; + let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await; + if !taken.is_empty() { + *crafting_inv.get_stack(idx).await.lock().await = taken; } } let screen_handler_arc = player.current_screen_handler.lock().await.clone(); screen_handler_arc.lock().await.send_content_updates().await; } + pub async fn handle_swing_arm(&self, player: &Arc, swing_arm: SSwingArm) { player.update_last_action_time(); let Ok(hand) = Hand::try_from(swing_arm.hand.0) else { diff --git a/pumpkin/src/net/java/recipe_helper.rs b/pumpkin/src/net/java/recipe_helper.rs new file mode 100644 index 000000000..758dfac83 --- /dev/null +++ b/pumpkin/src/net/java/recipe_helper.rs @@ -0,0 +1,81 @@ +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::recipes::RecipeIngredientTypes; +use pumpkin_inventory::player::player_inventory::PlayerInventory; +use pumpkin_protocol::codec::recipe::OwnedRecipeIngredient; + +#[derive(Clone, Copy)] +pub enum GenericIngredient<'a> { + Vanilla(&'a RecipeIngredientTypes), + Dynamic(&'a OwnedRecipeIngredient), +} + +impl GenericIngredient<'_> { + #[must_use] + pub fn match_item(&self, item: &Item) -> bool { + match self { + Self::Vanilla(v) => v.match_item(item), + Self::Dynamic(d) => d.match_item(item), + } + } +} + +pub async fn take_n_ingredient( + inventory: &PlayerInventory, + ingredient: &GenericIngredient<'_>, + count: u8, +) -> ItemStack { + let mut taken = 0u8; + let mut result: Option = None; + + for slot in &inventory.main_inventory { + let mut stack = slot.lock().await; + if !stack.is_empty() && ingredient.match_item(stack.item) { + let to_take = (count - taken).min(stack.item_count); + let sub_stack = stack.split(to_take); + taken += sub_stack.item_count; + + match &mut result { + None => result = Some(sub_stack), + Some(r) => r.item_count += sub_stack.item_count, + } + + if taken >= count { + break; + } + } + } + result.unwrap_or_else(|| ItemStack::EMPTY.clone()) +} + +pub async fn compute_biggest_craftable( + ingredients: &[GenericIngredient<'_>], + inventory: &PlayerInventory, +) -> u8 { + let mut available: Vec<(&'static Item, u32)> = Vec::new(); + for slot in &inventory.main_inventory { + let stack = slot.lock().await; + if !stack.is_empty() { + if let Some(e) = available.iter_mut().find(|(i, _)| i.id == stack.item.id) { + e.1 += u32::from(stack.item_count); + } else { + available.push((stack.item, u32::from(stack.item_count))); + } + } + } + + 'outer: for amount in (1u32..=64).rev() { + let mut budget = available.clone(); + for ing in ingredients { + let Some(idx) = budget + .iter() + .position(|(item, count)| *count >= amount && ing.match_item(item)) + else { + continue 'outer; + }; + budget[idx].1 -= amount; + } + return amount as u8; + } + 0 +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index 8fa597792..2cefd5e76 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -28,7 +28,7 @@ use crate::{ api::gui::PluginGui, loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg}, }, - server::Server, + server::{RecipeManager, Server}, world::World, }; @@ -54,6 +54,7 @@ pub type CommandSenderResource = WasmResource; pub type ConsumedArgsResource = WasmResource; pub type CommandNodeResource = WasmResource; pub type ItemStackResource = WasmResource>>; +pub type RecipeManagerResource = WasmResource>; pub type OwnedConsumedArgs = HashMap; @@ -229,6 +230,16 @@ impl PluginHostState { let resource = self.resource_table.push(ItemStackResource { provider })?; Ok(wasmtime::component::Resource::new_own(resource.rep())) } + + pub fn add_recipe_manager( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(RecipeManagerResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } } pub struct PluginHttpHooks { diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs index 67860d902..fcabeb638 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs @@ -348,6 +348,7 @@ pub fn serialize_java_packet( ClientboundPacket::CRecipeBookAdd(data) => { let p = pumpkin_protocol::java::client::play::CRecipeBookAdd { replace: data.replace.try_into().unwrap(), + dynamic_recipes: &[], }; let mut buf = Vec::new(); crate::net::java::JavaClient::write_packet_for_version(&p, version, &mut buf).unwrap(); diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs index 27a43c7b0..03b3ce17e 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs @@ -22,6 +22,7 @@ pub mod java_dialogs; pub mod logging; pub mod permission; pub mod player; +pub mod recipe; pub mod scheduler; pub mod scoreboard; pub mod server; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/recipe.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/recipe.rs new file mode 100644 index 000000000..ea087af4c --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/recipe.rs @@ -0,0 +1,143 @@ +use crate::plugin::loader::wasm::wasm_host::state::PluginHostState; +use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::recipe::{ + CookingRecipe as WitCookingRecipe, CookingType as WitCookingType, Host as RecipeHost, + HostRecipeManager, Ingredient as WitIngredient, RecipeManager as WitRecipeManager, + ShapedRecipe as WitShapedRecipe, ShapelessRecipe as WitShapelessRecipe, +}; +use pumpkin_data::recipes::RecipeCategoryTypes; +use pumpkin_protocol::codec::recipe::{ + DynamicRecipe, OwnedCookingRecipe, OwnedCookingRecipeType, OwnedCraftingRecipe, + OwnedRecipeIngredient, OwnedRecipeResult, +}; +use wasmtime::component::Resource; + +impl RecipeHost for PluginHostState {} + +impl HostRecipeManager for PluginHostState { + async fn register_shaped( + &mut self, + _res: Resource, + _id: String, + recipe: WitShapedRecipe, + ) -> wasmtime::Result<()> { + let result_stack = self.get_item_stack(&recipe.output)?; + let result_stack = result_stack.lock().await; + + let owned_recipe = OwnedCraftingRecipe::Shaped { + category: RecipeCategoryTypes::Misc, // TODO: Allow specifying category + group: recipe.group, + show_notification: true, + key: recipe + .key + .into_iter() + .map(|(k, ing)| (k.chars().next().unwrap_or(' '), to_owned_ingredient(ing))) + .collect(), + pattern: recipe.pattern, + result: OwnedRecipeResult { + item_id: result_stack.item.registry_key.to_string(), + count: result_stack.item_count, + }, + }; + + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + server + .recipe_manager + .add_recipe(DynamicRecipe::Crafting(owned_recipe)) + .await; + Ok(()) + } + + async fn register_shapeless( + &mut self, + _res: Resource, + _id: String, + recipe: WitShapelessRecipe, + ) -> wasmtime::Result<()> { + let result_stack = self.get_item_stack(&recipe.output)?; + let result_stack = result_stack.lock().await; + + let owned_recipe = OwnedCraftingRecipe::Shapeless { + category: RecipeCategoryTypes::Misc, + group: recipe.group, + ingredients: recipe + .ingredients + .into_iter() + .map(to_owned_ingredient) + .collect(), + result: OwnedRecipeResult { + item_id: result_stack.item.registry_key.to_string(), + count: result_stack.item_count, + }, + }; + + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + server + .recipe_manager + .add_recipe(DynamicRecipe::Crafting(owned_recipe)) + .await; + Ok(()) + } + + async fn register_cooking( + &mut self, + _res: Resource, + id: String, + station_type: WitCookingType, + recipe: WitCookingRecipe, + ) -> wasmtime::Result<()> { + let result_stack = self.get_item_stack(&recipe.output)?; + let result_stack = result_stack.lock().await; + + let owned_cooking = OwnedCookingRecipe { + recipe_id: id, + category: RecipeCategoryTypes::Misc, + group: recipe.group, + ingredient: to_owned_ingredient(recipe.ingredient), + cooking_time: recipe.cooking_time as i32, + experience: recipe.experience, + result: OwnedRecipeResult { + item_id: result_stack.item.registry_key.to_string(), + count: result_stack.item_count, + }, + }; + + let dynamic_recipe = match station_type { + WitCookingType::Smelting => { + DynamicRecipe::Cooking(OwnedCookingRecipeType::Smelting(owned_cooking)) + } + WitCookingType::Blasting => { + DynamicRecipe::Cooking(OwnedCookingRecipeType::Blasting(owned_cooking)) + } + WitCookingType::Smoking => { + DynamicRecipe::Cooking(OwnedCookingRecipeType::Smoking(owned_cooking)) + } + WitCookingType::Campfire => { + DynamicRecipe::Cooking(OwnedCookingRecipeType::CampfireCooking(owned_cooking)) + } + }; + + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + server.recipe_manager.add_recipe(dynamic_recipe).await; + Ok(()) + } + + async fn drop(&mut self, _rep: Resource) -> wasmtime::Result<()> { + Ok(()) + } +} + +fn to_owned_ingredient(ing: WitIngredient) -> OwnedRecipeIngredient { + match ing { + WitIngredient::Item(id) => OwnedRecipeIngredient::Simple(id), + WitIngredient::Tag(tag) => OwnedRecipeIngredient::Tagged(tag), + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index fc7cf1c68..76379b7bf 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -2,6 +2,7 @@ use pumpkin_util::text::TextComponent; use wasmtime::component::Resource; use crate::command::CommandSender; +use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::recipe::RecipeManager as WitRecipeManager; use pumpkin::plugin::server::CommandSender as WasmCommandSender; use super::player::text_component_from_resource; @@ -380,6 +381,17 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { )) } + async fn get_recipe_manager( + &mut self, + _rep: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + self.add_recipe_manager(server.recipe_manager.clone()) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 99b9ec70c..17f16bfa2 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -54,11 +54,14 @@ use tokio_util::task::TaskTracker; mod connection_cache; mod key_store; +pub mod recipe; pub mod scheduler; pub mod seasonal_events; pub mod tick_rate_manager; pub mod ticker; +pub use recipe::RecipeManager; + use crate::command::args::entities::{ EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition, }; @@ -99,6 +102,7 @@ pub struct Server { pub dimensions: Vec, /// Assigns unique IDs to containers. container_id: AtomicU32, + pub recipe_manager: Arc, /// Assigns unique IDs to maps. map_id: AtomicI32, /// Mojang's public keys, used for chat session signing @@ -223,6 +227,7 @@ impl Server { ))), permission_registry, container_id: 0.into(), + recipe_manager: Arc::new(recipe::RecipeManager::new()), map_id: level_info.load().map_id.into(), worlds: ArcSwap::from_pointee(vec![]), dimensions: vec![ diff --git a/pumpkin/src/server/recipe.rs b/pumpkin/src/server/recipe.rs new file mode 100644 index 000000000..17c6e87de --- /dev/null +++ b/pumpkin/src/server/recipe.rs @@ -0,0 +1,38 @@ +use pumpkin_inventory::crafting::recipe_provider::RecipeProvider; +use pumpkin_inventory::slot::BoxFuture; +pub use pumpkin_protocol::codec::recipe::DynamicRecipe; +use tokio::sync::RwLock; + +pub struct RecipeManager { + dynamic_recipes: RwLock>, +} + +impl Default for RecipeManager { + fn default() -> Self { + Self::new() + } +} + +impl RecipeManager { + #[must_use] + pub fn new() -> Self { + Self { + dynamic_recipes: RwLock::new(Vec::new()), + } + } + + pub async fn add_recipe(&self, recipe: DynamicRecipe) { + let mut recipes = self.dynamic_recipes.write().await; + recipes.push(recipe); + } + + pub async fn get_dynamic_recipes_internal(&self) -> Vec { + self.dynamic_recipes.read().await.clone() + } +} + +impl RecipeProvider for RecipeManager { + fn get_dynamic_recipes(&self) -> BoxFuture<'_, Vec> { + Box::pin(async move { self.dynamic_recipes.read().await.clone() }) + } +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 3ba45a57c..b32ba6727 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -63,6 +63,7 @@ use pumpkin_data::{ world::{RAW, WorldEvent}, }; use pumpkin_data::{BlockDirection, BlockState, translation}; +use pumpkin_inventory::crafting::recipe_provider::RecipeProvider; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed}; use pumpkin_protocol::bedrock::client::set_actor_data::{ @@ -2265,8 +2266,9 @@ impl World { java_client .send_packet_now(&CRecipeBookSettings::default_closed()) .await; + let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; java_client - .send_packet_now(&CRecipeBookAdd::new(true)) + .send_packet_now(&CRecipeBookAdd::new(true, &dynamic_recipes)) .await; }