//! Crafting screen handler implementation. //! //! This module provides screen handlers for crafting mechanics: //! - [`CraftingScreenHandler`] - Trait for crafting screen handlers //! - [`CraftingTableScreenHandler`] - The 3x3 crafting table UI //! - [`ResultSlot`] - The special result slot that shows crafted items //! //! # Recipe Matching //! //! Crafting recipes are matched against the items in the crafting grid. //! The system supports: //! - Shaped recipes (specific patterns) //! - Shapeless recipes (any arrangement) //! - Transmute recipes (upgrading items) //! - Special recipes (like decorated pots) use std::any::Any; 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; use crate::screen_handler::{ InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour, ScreenHandlerFuture, ScreenHandlerListener, }; use crate::slot::{BoxFuture, NormalSlot, Slot}; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING}; use pumpkin_data::screen::WindowType; use pumpkin_data::statistic::StatisticCategory; 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. 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. pub result: Arc>, /// 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. 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; } for j in 0..width / 2 { if row.chars().nth(j) != row.chars().nth(width - j - 1) { return false; } } } true } /// Checks if a crafting recipe matches the current inventory state. #[expect(clippy::too_many_lines)] async fn recipe_matches( recipe: GenericRecipe<'_>, input_height: usize, input_width: usize, top_x: usize, top_y: usize, count: usize, inventory: &dyn RecipeInputInventory, ) -> Option { match recipe { GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingShaped { key, pattern, 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.is_empty() { matched = false; break 'outer; } continue; } let ingredient = key .iter() .find_map(|(k, v)| (*k == current_key).then_some(v)) .expect("Crafting recipe used invalid key"); if !ingredient.match_item(slot.item) { matched = false; break 'outer; } } } 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() + (x_offset + input_width - 1 - x), ) .await; if current_key == ' ' { if !slot.is_empty() { matched = false; break 'outer; } continue; } let ingredient = key .iter() .find_map(|(k, v)| (*k == current_key).then_some(v)) .expect("Crafting recipe used invalid key"); if !ingredient.match_item(slot.item) { matched = false; break 'outer; } } } } matched.then_some(RecipeResult { item_id: result.id.to_string(), count: result.count, }) } 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; 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.id.to_string(), count: result.count, }) } 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; if slot.is_empty() { continue 'item_stack; } if !material.match_item(slot.item) && !input.match_item(slot.item) { return None; } } Some(RecipeResult { item_id: result.id.to_string(), count: result.count, }) } 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; if slot.is_empty() || !slot .item .has_tag(&tag::Item::MINECRAFT_DECORATED_POT_INGREDIENTS) { return None; } } Some(RecipeResult { item_id: "minecraft:decorated_pot".to_string(), count: 1, }) } 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.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"); 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; 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 { pub fn new( inventory: Arc, provider: Option>, ) -> Self { Self { inventory, id: AtomicU8::new(0), result: Arc::new(Mutex::new(ItemStack::EMPTY.clone())), recipe_provider: provider, } } async fn match_recipe(&self) -> Option { let mut count: usize = 0; let inventory_width = self.inventory.get_width(); let mut top_x = 9; let mut top_y = 9; let mut bottom_x = 0; let mut bottom_y = 0; 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; if !slot.is_empty() { top_x = top_x.min(x); top_y = top_y.min(y); bottom_x = bottom_x.max(x); bottom_y = bottom_y.max(y); count += 1; } } if count == 0 { return None; } let input_width = bottom_x + 1 - top_x; let input_height = bottom_y + 1 - top_y; for recipe in RECIPES_CRAFTING { if let Some(result) = recipe_matches( GenericRecipe::Vanilla(recipe), input_height, input_width, top_x, top_y, count, &*self.inventory, ) .await { 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 } async fn refill_output(&self) -> ItemStack { 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 } } impl Slot for ResultSlot { fn get_inventory(&self) -> Arc { self.inventory.clone() } fn get_index(&self) -> usize { 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 { self.refill_output().await; }) } fn on_take_item<'a>( &'a self, player: &'a dyn InventoryPlayer, stack: &'a ItemStack, ) -> BoxFuture<'a, ()> { Box::pin(async move { player .increment_stat( StatisticCategory::Crafted, stack.item.id as i32, stack.item_count as i32, ) .await; for i in 0..self.inventory.size() { self.inventory.remove_stack_specific(i, 1).await; } self.mark_dirty().await; }) } fn can_insert(&self, _stack: &ItemStack) -> BoxFuture<'_, bool> { Box::pin(async move { false }) } fn get_stack(&self) -> BoxFuture<'_, ItemStack> { Box::pin(async move { self.result.lock().await.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; for i in 0..self.inventory.size() { let slot = self.inventory.get_stack(i).await; if !slot.is_empty() { count = count.min(slot.item_count); } } count }) } fn take_stack(&self, _amount: u8) -> BoxFuture<'_, ItemStack> { Box::pin(async move { if self.has_stack().await { self.result.lock().await.clone() } else { ItemStack::EMPTY.clone() } }) } } impl ScreenHandlerListener for ResultSlot { fn on_slot_update<'a>( &'a self, screen_handler: &'a ScreenHandlerBehaviour, slot: u8, _stack: ItemStack, ) -> Pin + Send + 'a>> { Box::pin(async move { if (0..=(self.inventory.get_width() * self.inventory.get_height())) .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 .update_slot(screen_handler, 0, &result, next_revision) .await; } } }) } } pub trait CraftingScreenHandler: RecipeFinderScreenHandler + ScreenHandler { 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(), 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 { let input_slot = NormalSlot::new(crafing_inventory.clone(), j + i * width); self.add_slot(Arc::new(input_slot)); } } self.add_listener(result_slot).await; }) } } pub struct CraftingTableScreenHandler { behaviour: ScreenHandlerBehaviour, crafting_inventory: Arc, } impl CraftingTableScreenHandler { 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, provider) .await; let player_inventory: Arc = player_inventory.clone(); crafting_table_handler.add_player_slots(&player_inventory); crafting_table_handler } } impl RecipeFinderScreenHandler for CraftingTableScreenHandler {} 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; self.drop_inventory(player, self.crafting_inventory.clone()) .await; }) } fn quick_move<'a>( &'a mut self, player: &'a dyn InventoryPlayer, slot_index: i32, ) -> ItemStackFuture<'a> { Box::pin(async move { let slot = self.get_behaviour().slots[slot_index as usize].clone(); if slot.has_stack().await { let mut slot_stack = slot.get_stack().await; let stack_prev = slot_stack.clone(); if slot_index == 0 { if !self.insert_item(&mut slot_stack, 10, 46, true).await { return ItemStack::EMPTY.clone(); } } else if (1..=9).contains(&slot_index) { if !self.insert_item(&mut slot_stack, 10, 46, false).await { return ItemStack::EMPTY.clone(); } } else if (10..46).contains(&slot_index) { if !self.insert_item(&mut slot_stack, 1, 10, false).await { if slot_index < 37 { if !self.insert_item(&mut slot_stack, 37, 46, false).await { return ItemStack::EMPTY.clone(); } } else if !self.insert_item(&mut slot_stack, 10, 37, 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); 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 { return ItemStack::EMPTY.clone(); } let mut taken_stack = stack_prev.clone(); taken_stack.set_count(stack_prev.item_count - stack.item_count); slot.on_take_item(player, &taken_stack).await; if slot_index == 0 { slot.on_quick_move_crafted(stack.clone(), stack_prev.clone()) .await; if !stack.is_empty() { player.drop_item(stack, false).await; } } return stack_prev; } ItemStack::EMPTY.clone() }) } } impl CraftingScreenHandler for CraftingTableScreenHandler {}