feat(plugin-api): allow to register custom recipies
This commit is contained in:
parent
cfeb5d80fc
commit
e2ff17d6f0
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<dyn RecipeInputInventory>,
|
||||
/// 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<Mutex<ItemStack>>,
|
||||
/// Cached reference to the last matched recipe for quick re-matching.
|
||||
recipe_cache: AtomicCell<Option<&'static CraftingRecipeTypes>>,
|
||||
/// Provider for dynamic recipes.
|
||||
pub recipe_provider: Option<Arc<dyn RecipeProvider>>,
|
||||
}
|
||||
|
||||
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<RecipeResult> {
|
||||
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::<usize>()
|
||||
{
|
||||
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<dyn RecipeInputInventory>) -> Self {
|
||||
pub fn new(
|
||||
inventory: Arc<dyn RecipeInputInventory>,
|
||||
provider: Option<Arc<dyn RecipeProvider>>,
|
||||
) -> 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<RecipeResult> {
|
||||
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<dyn Inventory> {
|
||||
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<Mutex<ItemStack>>> {
|
||||
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<I: RecipeInputInventory>:
|
||||
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<dyn RecipeInputInventory>,
|
||||
provider: Option<Arc<dyn RecipeProvider>>,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + 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<dyn RecipeInputInventory>,
|
||||
}
|
||||
|
||||
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<PlayerInventory>) -> Self {
|
||||
pub async fn new(
|
||||
sync_id: u8,
|
||||
player_inventory: &Arc<PlayerInventory>,
|
||||
provider: Option<Arc<dyn RecipeProvider>>,
|
||||
) -> Self {
|
||||
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
||||
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<dyn Inventory> = 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()
|
||||
})
|
||||
}
|
||||
|
||||
@ -7,4 +7,5 @@
|
||||
|
||||
pub mod crafting_inventory;
|
||||
pub mod crafting_screen_handler;
|
||||
pub mod recipe_provider;
|
||||
pub mod recipes;
|
||||
|
||||
26
pumpkin-inventory/src/crafting/recipe_provider.rs
Normal file
26
pumpkin-inventory/src/crafting/recipe_provider.rs
Normal file
@ -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<DynamicRecipe>>;
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,6 +81,7 @@ impl PlayerScreenHandler {
|
||||
player_inventory: &Arc<PlayerInventory>,
|
||||
window_type: Option<WindowType>,
|
||||
sync_id: u8,
|
||||
provider: Option<Arc<dyn crate::crafting::recipe_provider::RecipeProvider>>,
|
||||
) -> Self {
|
||||
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
||||
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)
|
||||
|
||||
67
pumpkin-plugin-wit/v0.1/recipe.wit
Normal file
67
pumpkin-plugin-wit/v0.1/recipe.wit
Normal file
@ -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<string>,
|
||||
/// Mapping of characters in the pattern to ingredients.
|
||||
key: list<tuple<string, ingredient>>,
|
||||
/// The resulting item stack.
|
||||
output: item-stack,
|
||||
/// The recipe group (optional).
|
||||
group: option<string>,
|
||||
}
|
||||
|
||||
/// Represents a shapeless crafting recipe.
|
||||
record shapeless-recipe {
|
||||
/// The list of ingredients required.
|
||||
ingredients: list<ingredient>,
|
||||
/// The resulting item stack.
|
||||
output: item-stack,
|
||||
/// The recipe group (optional).
|
||||
group: option<string>,
|
||||
}
|
||||
|
||||
/// 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<string>,
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
81
pumpkin-protocol/src/codec/recipe.rs
Normal file
81
pumpkin-protocol/src/codec/recipe.rs
Normal file
@ -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<String>),
|
||||
}
|
||||
|
||||
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<String>,
|
||||
show_notification: bool,
|
||||
key: Vec<(char, OwnedRecipeIngredient)>,
|
||||
pattern: Vec<String>,
|
||||
result: OwnedRecipeResult,
|
||||
},
|
||||
Shapeless {
|
||||
category: RecipeCategoryTypes,
|
||||
group: Option<String>,
|
||||
ingredients: Vec<OwnedRecipeIngredient>,
|
||||
result: OwnedRecipeResult,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OwnedCookingRecipe {
|
||||
pub recipe_id: String,
|
||||
pub category: RecipeCategoryTypes,
|
||||
pub group: Option<String>,
|
||||
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),
|
||||
}
|
||||
@ -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<i32>) -> 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<i32> {
|
||||
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<Cow<'_, str>, 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<Cow<'a, str>, i32>,
|
||||
next_group_id: &mut i32,
|
||||
group: Option<Cow<'a, str>>,
|
||||
) -> Option<i32> {
|
||||
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<i32> = 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<i32>,
|
||||
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<i32>,
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -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<crate::server::RecipeManager>);
|
||||
|
||||
impl ScreenHandlerFactory for CraftingTableScreenFactory {
|
||||
fn create_screen_handler<'a>(
|
||||
@ -37,7 +40,9 @@ impl ScreenHandlerFactory for CraftingTableScreenFactory {
|
||||
_player: &'a dyn InventoryPlayer,
|
||||
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
||||
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)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -926,80 +926,19 @@ impl JavaClient {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn handle_place_recipe(&self, player: &Arc<Player>, packet: SPlaceRecipe) {
|
||||
use pumpkin_data::recipes::{
|
||||
CookingRecipeType, CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING,
|
||||
RecipeIngredientTypes,
|
||||
pub async fn handle_place_recipe(
|
||||
&self,
|
||||
server: &Arc<Server>,
|
||||
player: &Arc<Player>,
|
||||
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<ItemStack> = 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<Option<GenericIngredient<'_>>> = 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<Option<&RecipeIngredientTypes>> = 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<GenericIngredient<'_>> =
|
||||
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<Player>, swing_arm: SSwingArm) {
|
||||
player.update_last_action_time();
|
||||
let Ok(hand) = Hand::try_from(swing_arm.hand.0) else {
|
||||
|
||||
81
pumpkin/src/net/java/recipe_helper.rs
Normal file
81
pumpkin/src/net/java/recipe_helper.rs
Normal file
@ -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<ItemStack> = 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
|
||||
}
|
||||
@ -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<CommandSender>;
|
||||
pub type ConsumedArgsResource = WasmResource<OwnedConsumedArgs>;
|
||||
pub type CommandNodeResource = WasmResource<NonLeafNodeBuilder>;
|
||||
pub type ItemStackResource = WasmResource<Arc<Mutex<pumpkin_data::item_stack::ItemStack>>>;
|
||||
pub type RecipeManagerResource = WasmResource<Arc<RecipeManager>>;
|
||||
|
||||
pub type OwnedConsumedArgs = HashMap<String, OwnedArg>;
|
||||
|
||||
@ -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<T>(
|
||||
&mut self,
|
||||
provider: Arc<RecipeManager>,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.push(RecipeManagerResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginHttpHooks {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
143
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/recipe.rs
Normal file
143
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/recipe.rs
Normal file
@ -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<WitRecipeManager>,
|
||||
_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<WitRecipeManager>,
|
||||
_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<WitRecipeManager>,
|
||||
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<WitRecipeManager>) -> wasmtime::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn to_owned_ingredient(ing: WitIngredient) -> OwnedRecipeIngredient {
|
||||
match ing {
|
||||
WitIngredient::Item(id) => OwnedRecipeIngredient::Simple(id),
|
||||
WitIngredient::Tag(tag) => OwnedRecipeIngredient::Tagged(tag),
|
||||
}
|
||||
}
|
||||
@ -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<Server>,
|
||||
) -> wasmtime::Result<Resource<WitRecipeManager>> {
|
||||
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<Server>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<ServerResource>(Resource::new_own(rep.rep()))
|
||||
|
||||
@ -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<Dimension>,
|
||||
/// Assigns unique IDs to containers.
|
||||
container_id: AtomicU32,
|
||||
pub recipe_manager: Arc<recipe::RecipeManager>,
|
||||
/// 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![
|
||||
|
||||
38
pumpkin/src/server/recipe.rs
Normal file
38
pumpkin/src/server/recipe.rs
Normal file
@ -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<Vec<DynamicRecipe>>,
|
||||
}
|
||||
|
||||
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<DynamicRecipe> {
|
||||
self.dynamic_recipes.read().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl RecipeProvider for RecipeManager {
|
||||
fn get_dynamic_recipes(&self) -> BoxFuture<'_, Vec<DynamicRecipe>> {
|
||||
Box::pin(async move { self.dynamic_recipes.read().await.clone() })
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user