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! {
|
quote! {
|
||||||
use crate::tag::Taggable;
|
use crate::tag::Taggable;
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CraftingRecipeTypes {
|
pub enum CraftingRecipeTypes {
|
||||||
CraftingShaped {
|
CraftingShaped {
|
||||||
category: RecipeCategoryTypes,
|
category: RecipeCategoryTypes,
|
||||||
@ -566,7 +567,7 @@ pub fn build() -> TokenStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct CookingRecipe {
|
pub struct CookingRecipe {
|
||||||
/// Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")
|
/// Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")
|
||||||
pub recipe_id: &'static str,
|
pub recipe_id: &'static str,
|
||||||
@ -578,14 +579,14 @@ pub fn build() -> TokenStream {
|
|||||||
pub result: RecipeResultStruct,
|
pub result: RecipeResultStruct,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CookingRecipeType {
|
pub enum CookingRecipeType {
|
||||||
Blasting(CookingRecipe),
|
Blasting(CookingRecipe),
|
||||||
Smelting(CookingRecipe),
|
Smelting(CookingRecipe),
|
||||||
Smoking(CookingRecipe),
|
Smoking(CookingRecipe),
|
||||||
CampfireCooking(CookingRecipe),
|
CampfireCooking(CookingRecipe),
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CookingRecipeKind {
|
pub enum CookingRecipeKind {
|
||||||
Blasting,
|
Blasting,
|
||||||
Smelting,
|
Smelting,
|
||||||
@ -626,20 +627,20 @@ pub fn build() -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct StonecutterRecipe {
|
pub struct StonecutterRecipe {
|
||||||
pub group: Option<&'static str>,
|
pub group: Option<&'static str>,
|
||||||
pub ingredient: RecipeIngredientTypes,
|
pub ingredient: RecipeIngredientTypes,
|
||||||
pub result: RecipeResultStruct,
|
pub result: RecipeResultStruct,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct RecipeResultStruct {
|
pub struct RecipeResultStruct {
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub count: u8,
|
pub count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum RecipeIngredientTypes {
|
pub enum RecipeIngredientTypes {
|
||||||
Simple(&'static str),
|
Simple(&'static str),
|
||||||
Tagged(&'static str),
|
Tagged(&'static str),
|
||||||
@ -664,7 +665,7 @@ pub fn build() -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum RecipeCategoryTypes {
|
pub enum RecipeCategoryTypes {
|
||||||
Equipment,
|
Equipment,
|
||||||
Building,
|
Building,
|
||||||
|
|||||||
@ -84,6 +84,23 @@ fn parse_packet_file(path: &Path, interface: &mut Interface, variant: &mut Varia
|
|||||||
|
|
||||||
if let Fields::Named(fields) = s.fields {
|
if let Fields::Named(fields) = s.fields {
|
||||||
for field in fields.named {
|
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_name = field.ident.as_ref().unwrap().to_string().to_kebab_case();
|
||||||
let field_type = map_type(&field.ty);
|
let field_type = map_type(&field.ty);
|
||||||
fields_list.push(Field::new(field_name, field_type));
|
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 wit_field = field_name.to_snake_case();
|
||||||
let (type_ident, is_ref, is_slice) = get_type_info(&field.ty);
|
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" {
|
if field_name == "type" {
|
||||||
field_name = "r#type".to_string();
|
field_name = "r#type".to_string();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
/* This file is generated. Do not edit manually. */
|
/* This file is generated. Do not edit manually. */
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
use crate::tag::Taggable;
|
use crate::tag::Taggable;
|
||||||
#[derive(Clone, Debug)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CraftingRecipeTypes {
|
pub enum CraftingRecipeTypes {
|
||||||
CraftingShaped {
|
CraftingShaped {
|
||||||
category: RecipeCategoryTypes,
|
category: RecipeCategoryTypes,
|
||||||
@ -30,7 +31,7 @@ pub enum CraftingRecipeTypes {
|
|||||||
CraftingSpecial,
|
CraftingSpecial,
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct CookingRecipe {
|
pub struct CookingRecipe {
|
||||||
#[doc = r#" Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")"#]
|
#[doc = r#" Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")"#]
|
||||||
pub recipe_id: &'static str,
|
pub recipe_id: &'static str,
|
||||||
@ -41,14 +42,14 @@ pub struct CookingRecipe {
|
|||||||
pub experience: f32,
|
pub experience: f32,
|
||||||
pub result: RecipeResultStruct,
|
pub result: RecipeResultStruct,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CookingRecipeType {
|
pub enum CookingRecipeType {
|
||||||
Blasting(CookingRecipe),
|
Blasting(CookingRecipe),
|
||||||
Smelting(CookingRecipe),
|
Smelting(CookingRecipe),
|
||||||
Smoking(CookingRecipe),
|
Smoking(CookingRecipe),
|
||||||
CampfireCooking(CookingRecipe),
|
CampfireCooking(CookingRecipe),
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum CookingRecipeKind {
|
pub enum CookingRecipeKind {
|
||||||
Blasting,
|
Blasting,
|
||||||
Smelting,
|
Smelting,
|
||||||
@ -85,18 +86,18 @@ impl CookingRecipeKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct StonecutterRecipe {
|
pub struct StonecutterRecipe {
|
||||||
pub group: Option<&'static str>,
|
pub group: Option<&'static str>,
|
||||||
pub ingredient: RecipeIngredientTypes,
|
pub ingredient: RecipeIngredientTypes,
|
||||||
pub result: RecipeResultStruct,
|
pub result: RecipeResultStruct,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct RecipeResultStruct {
|
pub struct RecipeResultStruct {
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub count: u8,
|
pub count: u8,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum RecipeIngredientTypes {
|
pub enum RecipeIngredientTypes {
|
||||||
Simple(&'static str),
|
Simple(&'static str),
|
||||||
Tagged(&'static str),
|
Tagged(&'static str),
|
||||||
@ -119,7 +120,7 @@ impl RecipeIngredientTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum RecipeCategoryTypes {
|
pub enum RecipeCategoryTypes {
|
||||||
Equipment,
|
Equipment,
|
||||||
Building,
|
Building,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ use std::pin::Pin;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
|
||||||
|
use super::recipe_provider::{GenericRecipe, RecipeProvider};
|
||||||
use super::recipes::{RecipeFinderScreenHandler, RecipeInputInventory};
|
use super::recipes::{RecipeFinderScreenHandler, RecipeInputInventory};
|
||||||
use crate::crafting::crafting_inventory::CraftingInventory;
|
use crate::crafting::crafting_inventory::CraftingInventory;
|
||||||
use crate::player::player_inventory::PlayerInventory;
|
use crate::player::player_inventory::PlayerInventory;
|
||||||
@ -28,52 +29,42 @@ use crate::screen_handler::{
|
|||||||
};
|
};
|
||||||
use crate::slot::{BoxFuture, NormalSlot, Slot};
|
use crate::slot::{BoxFuture, NormalSlot, Slot};
|
||||||
|
|
||||||
use crossbeam_utils::atomic::AtomicCell;
|
|
||||||
use pumpkin_data::item_stack::ItemStack;
|
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::screen::WindowType;
|
||||||
use pumpkin_data::tag;
|
use pumpkin_data::tag;
|
||||||
use pumpkin_data::tag::Taggable;
|
use pumpkin_data::tag::Taggable;
|
||||||
|
use pumpkin_protocol::codec::recipe::{DynamicRecipe, OwnedCraftingRecipe};
|
||||||
use pumpkin_world::inventory::Inventory;
|
use pumpkin_world::inventory::Inventory;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
/// The result slot in a crafting screen.
|
/// 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 {
|
pub struct ResultSlot {
|
||||||
/// The crafting inventory (grid) that provides recipe input.
|
/// The crafting inventory (grid) that provides recipe input.
|
||||||
pub inventory: Arc<dyn RecipeInputInventory>,
|
pub inventory: Arc<dyn RecipeInputInventory>,
|
||||||
/// Protocol ID for this slot (assigned by screen handler).
|
/// Protocol ID for this slot (assigned by screen handler).
|
||||||
pub id: AtomicU8,
|
pub id: AtomicU8,
|
||||||
/// The cached result item stack.
|
/// The cached result item stack.
|
||||||
///
|
|
||||||
/// Updated when the crafting grid changes and a recipe matches.
|
|
||||||
pub result: Arc<Mutex<ItemStack>>,
|
pub result: Arc<Mutex<ItemStack>>,
|
||||||
/// Cached reference to the last matched recipe for quick re-matching.
|
/// Provider for dynamic recipes.
|
||||||
recipe_cache: AtomicCell<Option<&'static CraftingRecipeTypes>>,
|
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.
|
/// Checks if a recipe pattern is symmetrical horizontally.
|
||||||
///
|
fn is_symmetrical_horizontally(pattern: &[&str]) -> bool {
|
||||||
/// Used to try both orientations when matching shaped recipes.
|
|
||||||
fn is_symmetrical_horizontally(pattern: &'static [&'static str]) -> bool {
|
|
||||||
let width = pattern.first().map_or(0, |s| s.len());
|
let width = pattern.first().map_or(0, |s| s.len());
|
||||||
for row in pattern {
|
for row in pattern {
|
||||||
if row.len() != width {
|
if row.len() != width {
|
||||||
return false; // All rows must have the same length
|
return false;
|
||||||
}
|
}
|
||||||
for j in 0..width / 2 {
|
for j in 0..width / 2 {
|
||||||
if row.chars().nth(j) != row.chars().nth(width - j - 1) {
|
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.
|
/// 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)]
|
#[expect(clippy::too_many_lines)]
|
||||||
async fn recipe_matches<'a>(
|
async fn recipe_matches(
|
||||||
recipe: &'static CraftingRecipeTypes,
|
recipe: GenericRecipe<'_>,
|
||||||
input_height: usize,
|
input_height: usize,
|
||||||
input_width: usize,
|
input_width: usize,
|
||||||
top_x: usize,
|
top_x: usize,
|
||||||
top_y: usize,
|
top_y: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
inventory: &'a dyn RecipeInputInventory,
|
inventory: &dyn RecipeInputInventory,
|
||||||
) -> Option<&'a RecipeResultStruct> {
|
) -> Option<RecipeResult> {
|
||||||
match recipe {
|
match recipe {
|
||||||
CraftingRecipeTypes::CraftingShaped {
|
GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingShaped {
|
||||||
key,
|
key,
|
||||||
pattern,
|
pattern,
|
||||||
result,
|
result,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if pattern.len() != input_height || pattern.first().unwrap().len() != input_width {
|
if pattern.len() != input_height || pattern.first().unwrap().len() != input_width {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -118,11 +106,10 @@ async fn recipe_matches<'a>(
|
|||||||
let y_offset = top_y;
|
let y_offset = top_y;
|
||||||
|
|
||||||
let mut matched = true;
|
let mut matched = true;
|
||||||
'outer: for y in 0..pattern.len() {
|
'outer: for (y, row_str) in pattern.iter().enumerate() {
|
||||||
for x in 0..pattern[y].len() {
|
for (x, current_key) in row_str.chars().enumerate() {
|
||||||
let current_key = pattern[y].chars().nth(x).unwrap();
|
|
||||||
let slot = inventory
|
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;
|
.await;
|
||||||
if current_key == ' ' {
|
if current_key == ' ' {
|
||||||
if !slot.lock().await.is_empty() {
|
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) {
|
if !matched && !is_symmetrical_horizontally(pattern) {
|
||||||
matched = true;
|
matched = true;
|
||||||
'outer: for y in 0..pattern.len() {
|
'outer: for y in 0..pattern.len() {
|
||||||
for x in 0..pattern[y].len() {
|
for x in 0..pattern[y].len() {
|
||||||
let current_key = pattern[y].chars().nth(x).unwrap();
|
let current_key = pattern[y].chars().nth(x).unwrap();
|
||||||
|
|
||||||
let slot = inventory
|
let slot = inventory
|
||||||
.get_stack(
|
.get_stack(
|
||||||
(y + y_offset) * inventory.get_height()
|
(y + y_offset) * inventory.get_height()
|
||||||
@ -166,14 +151,11 @@ async fn recipe_matches<'a>(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ingredient = key
|
let ingredient = key
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(k, v)| (*k == current_key).then_some(v))
|
.find_map(|(k, v)| (*k == current_key).then_some(v))
|
||||||
.expect("Crafting recipe used invalid key");
|
.expect("Crafting recipe used invalid key");
|
||||||
|
|
||||||
let slot = slot.lock().await;
|
let slot = slot.lock().await;
|
||||||
|
|
||||||
if !ingredient.match_item(slot.item) {
|
if !ingredient.match_item(slot.item) {
|
||||||
matched = false;
|
matched = false;
|
||||||
break 'outer;
|
break 'outer;
|
||||||
@ -182,75 +164,70 @@ async fn recipe_matches<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Apply components
|
matched.then_some(RecipeResult {
|
||||||
matched.then_some(result)
|
item_id: result.id.to_string(),
|
||||||
|
count: result.count,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
CraftingRecipeTypes::CraftingShapeless {
|
GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingShapeless {
|
||||||
ingredients,
|
ingredients,
|
||||||
result,
|
result,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if count != ingredients.len() {
|
if count != ingredients.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ingredient_used = vec![false; ingredients.len()];
|
let mut ingredient_used = vec![false; ingredients.len()];
|
||||||
'next_slot: for i in 0..inventory.size() {
|
'next_slot: for i in 0..inventory.size() {
|
||||||
let slot = inventory.get_stack(i).await;
|
let slot = inventory.get_stack(i).await;
|
||||||
let slot = slot.lock().await;
|
let slot = slot.lock().await;
|
||||||
|
|
||||||
if slot.is_empty() {
|
if slot.is_empty() {
|
||||||
continue 'next_slot;
|
continue 'next_slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..ingredients.len() {
|
for i in 0..ingredients.len() {
|
||||||
if !ingredient_used[i] && ingredients[i].match_item(slot.item) {
|
if !ingredient_used[i] && ingredients[i].match_item(slot.item) {
|
||||||
ingredient_used[i] = true;
|
ingredient_used[i] = true;
|
||||||
continue 'next_slot;
|
continue 'next_slot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
Some(RecipeResult {
|
||||||
// TODO: Apply components
|
item_id: result.id.to_string(),
|
||||||
Some(result)
|
count: result.count,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
CraftingRecipeTypes::CraftingTransmute {
|
GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingTransmute {
|
||||||
input,
|
input,
|
||||||
material,
|
material,
|
||||||
result,
|
result,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if count != 2 {
|
if count != 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
'item_stack: for i in 0..inventory.size() {
|
'item_stack: for i in 0..inventory.size() {
|
||||||
let slot = inventory.get_stack(i).await;
|
let slot = inventory.get_stack(i).await;
|
||||||
let slot = slot.lock().await;
|
let slot = slot.lock().await;
|
||||||
|
|
||||||
if slot.is_empty() {
|
if slot.is_empty() {
|
||||||
continue 'item_stack;
|
continue 'item_stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !material.match_item(slot.item) && !input.match_item(slot.item) {
|
if !material.match_item(slot.item) && !input.match_item(slot.item) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(RecipeResult {
|
||||||
// TODO: Copy components
|
item_id: result.id.to_string(),
|
||||||
Some(result)
|
count: result.count,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
CraftingRecipeTypes::CraftingDecoratedPot { .. } => {
|
GenericRecipe::Vanilla(CraftingRecipeTypes::CraftingDecoratedPot { .. }) => {
|
||||||
if count != 4 || inventory.get_width() != 3 || inventory.get_height() != 3 {
|
if count != 4 || inventory.get_width() != 3 || inventory.get_height() != 3 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for position in (1..=7).step_by(2) {
|
for position in (1..=7).step_by(2) {
|
||||||
let slot = inventory.get_stack(position).await;
|
let slot = inventory.get_stack(position).await;
|
||||||
let slot = slot.lock().await;
|
let slot = slot.lock().await;
|
||||||
|
|
||||||
if slot.is_empty()
|
if slot.is_empty()
|
||||||
|| !slot
|
|| !slot
|
||||||
.item
|
.item
|
||||||
@ -259,34 +236,106 @@ async fn recipe_matches<'a>(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(RecipeResult {
|
||||||
// TODO: Handle side textures
|
item_id: "minecraft:decorated_pot".to_string(),
|
||||||
Some(&RecipeResultStruct {
|
|
||||||
id: "minecraft:decorated_pot",
|
|
||||||
count: 1,
|
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 {
|
impl ResultSlot {
|
||||||
//fn stat_crafted(&self, _crafted_amount: u8, _player: &dyn InventoryPlayer) {}
|
pub fn new(
|
||||||
|
inventory: Arc<dyn RecipeInputInventory>,
|
||||||
/// Creates a new result slot for the given crafting inventory.
|
provider: Option<Arc<dyn RecipeProvider>>,
|
||||||
pub fn new(inventory: Arc<dyn RecipeInputInventory>) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inventory,
|
inventory,
|
||||||
id: AtomicU8::new(0),
|
id: AtomicU8::new(0),
|
||||||
result: Arc::new(Mutex::new(ItemStack::EMPTY.clone())),
|
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.
|
async fn match_recipe(&self) -> Option<RecipeResult> {
|
||||||
///
|
|
||||||
/// If no recipe matches, returns `None`.
|
|
||||||
async fn match_recipe(&self) -> Option<(&RecipeResultStruct, &'static CraftingRecipeTypes)> {
|
|
||||||
let mut count: usize = 0;
|
let mut count: usize = 0;
|
||||||
let inventory_width = self.inventory.get_width();
|
let inventory_width = self.inventory.get_width();
|
||||||
let mut top_x = 9;
|
let mut top_x = 9;
|
||||||
@ -296,7 +345,6 @@ impl ResultSlot {
|
|||||||
for i in 0..self.inventory.size() {
|
for i in 0..self.inventory.size() {
|
||||||
let x = i % inventory_width;
|
let x = i % inventory_width;
|
||||||
let y = i / inventory_width;
|
let y = i / inventory_width;
|
||||||
|
|
||||||
let slot = self.inventory.get_stack(i).await;
|
let slot = self.inventory.get_stack(i).await;
|
||||||
let slot = slot.lock().await;
|
let slot = slot.lock().await;
|
||||||
if !slot.is_empty() {
|
if !slot.is_empty() {
|
||||||
@ -307,31 +355,15 @@ impl ResultSlot {
|
|||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let input_width = bottom_x + 1 - top_x;
|
let input_width = bottom_x + 1 - top_x;
|
||||||
let input_height = bottom_y + 1 - top_y;
|
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 {
|
for recipe in RECIPES_CRAFTING {
|
||||||
if let Some(result) = recipe_matches(
|
if let Some(result) = recipe_matches(
|
||||||
recipe,
|
GenericRecipe::Vanilla(recipe),
|
||||||
input_height,
|
input_height,
|
||||||
input_width,
|
input_width,
|
||||||
top_x,
|
top_x,
|
||||||
@ -341,20 +373,44 @@ impl ResultSlot {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
self.recipe_cache.store(Some(recipe));
|
return Some(result);
|
||||||
return Some((result, recipe));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refills the output slot with the current recipe result.
|
|
||||||
async fn refill_output(&self) -> ItemStack {
|
async fn refill_output(&self) -> ItemStack {
|
||||||
let result = self
|
let result = if let Some(matched) = self.match_recipe().await {
|
||||||
.match_recipe()
|
let key = matched
|
||||||
.await
|
.item_id
|
||||||
.map_or(ItemStack::EMPTY.clone(), |x| ItemStack::from(x.0));
|
.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();
|
*self.result.lock().await = result.clone();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@ -364,26 +420,21 @@ impl Slot for ResultSlot {
|
|||||||
fn get_inventory(&self) -> Arc<dyn Inventory> {
|
fn get_inventory(&self) -> Arc<dyn Inventory> {
|
||||||
self.inventory.clone()
|
self.inventory.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_index(&self) -> usize {
|
fn get_index(&self) -> usize {
|
||||||
999 // this slot does not belong to any inventory
|
999
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&self, id: usize) {
|
fn set_id(&self, id: usize) {
|
||||||
self.id.store(id as u8, Ordering::Relaxed);
|
self.id.store(id as u8, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_quick_move_crafted(
|
fn on_quick_move_crafted(
|
||||||
&self,
|
&self,
|
||||||
_stack: ItemStack,
|
_stack: ItemStack,
|
||||||
_stack_prev: ItemStack,
|
_stack_prev: ItemStack,
|
||||||
) -> BoxFuture<'_, ()> {
|
) -> BoxFuture<'_, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// refill the result slot with the recipe result
|
|
||||||
self.refill_output().await;
|
self.refill_output().await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_take_item<'a>(
|
fn on_take_item<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_player: &'a dyn InventoryPlayer,
|
_player: &'a dyn InventoryPlayer,
|
||||||
@ -394,50 +445,39 @@ impl Slot for ResultSlot {
|
|||||||
let slot = self.inventory.get_stack(i).await;
|
let slot = self.inventory.get_stack(i).await;
|
||||||
let mut stack = slot.lock().await;
|
let mut stack = slot.lock().await;
|
||||||
if !stack.is_empty() {
|
if !stack.is_empty() {
|
||||||
//TODO: Handle remaining items.
|
|
||||||
stack.item_count -= 1;
|
stack.item_count -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
//self.stat_crafted(stack.item_count, player);
|
|
||||||
self.mark_dirty().await;
|
self.mark_dirty().await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_insert(&self, _stack: &ItemStack) -> BoxFuture<'_, bool> {
|
fn can_insert(&self, _stack: &ItemStack) -> BoxFuture<'_, bool> {
|
||||||
Box::pin(async move { false })
|
Box::pin(async move { false })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_stack(&self) -> BoxFuture<'_, Arc<Mutex<ItemStack>>> {
|
fn get_stack(&self) -> BoxFuture<'_, Arc<Mutex<ItemStack>>> {
|
||||||
Box::pin(async move { self.result.clone() })
|
Box::pin(async move { self.result.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cloned_stack(&self) -> BoxFuture<'_, ItemStack> {
|
fn get_cloned_stack(&self) -> BoxFuture<'_, ItemStack> {
|
||||||
Box::pin(async move { self.result.lock().await.clone() })
|
Box::pin(async move { self.result.lock().await.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_stack(&self) -> BoxFuture<'_, bool> {
|
fn has_stack(&self) -> BoxFuture<'_, bool> {
|
||||||
Box::pin(async move { !self.result.lock().await.is_empty() })
|
Box::pin(async move { !self.result.lock().await.is_empty() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_stack(&self, _stack: ItemStack) -> BoxFuture<'_, ()> {
|
fn set_stack(&self, _stack: ItemStack) -> BoxFuture<'_, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.refill_output().await;
|
self.refill_output().await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_stack_prev(&self, _stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> {
|
fn set_stack_prev(&self, _stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.refill_output().await;
|
self.refill_output().await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
|
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.inventory.mark_dirty();
|
self.inventory.mark_dirty();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_max_item_count(&self) -> BoxFuture<'_, u8> {
|
fn get_max_item_count(&self) -> BoxFuture<'_, u8> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut count = u8::MAX;
|
let mut count = u8::MAX;
|
||||||
@ -451,14 +491,10 @@ impl Slot for ResultSlot {
|
|||||||
count
|
count
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_stack(&self, _amount: u8) -> BoxFuture<'_, ItemStack> {
|
fn take_stack(&self, _amount: u8) -> BoxFuture<'_, ItemStack> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if self.has_stack().await {
|
if self.has_stack().await {
|
||||||
let stack = self.result.lock().await;
|
self.result.lock().await.clone()
|
||||||
// Vanilla: net.minecraft.world.inventory.ResultContainer#removeItem
|
|
||||||
// Regardless of the amount, we always return the full stack
|
|
||||||
stack.clone()
|
|
||||||
} else {
|
} else {
|
||||||
ItemStack::EMPTY.clone()
|
ItemStack::EMPTY.clone()
|
||||||
}
|
}
|
||||||
@ -478,7 +514,6 @@ impl ScreenHandlerListener for ResultSlot {
|
|||||||
.contains(&(slot as usize))
|
.contains(&(slot as usize))
|
||||||
{
|
{
|
||||||
let result = self.refill_output().await;
|
let result = self.refill_output().await;
|
||||||
|
|
||||||
let next_revision = screen_handler.next_revision();
|
let next_revision = screen_handler.next_revision();
|
||||||
if let Some(sync_handler) = screen_handler.sync_handler.as_ref() {
|
if let Some(sync_handler) = screen_handler.sync_handler.as_ref() {
|
||||||
sync_handler
|
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>:
|
pub trait CraftingScreenHandler<I: RecipeInputInventory>:
|
||||||
RecipeFinderScreenHandler + ScreenHandler
|
RecipeFinderScreenHandler + ScreenHandler
|
||||||
{
|
{
|
||||||
/// Adds the result slot and crafting grid slots to the screen handler.
|
|
||||||
fn add_recipe_slots<'a>(
|
fn add_recipe_slots<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
crafing_inventory: Arc<dyn RecipeInputInventory>,
|
crafing_inventory: Arc<dyn RecipeInputInventory>,
|
||||||
|
provider: Option<Arc<dyn RecipeProvider>>,
|
||||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
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());
|
self.add_slot(result_slot.clone());
|
||||||
|
|
||||||
let width = crafing_inventory.get_width();
|
let width = crafing_inventory.get_width();
|
||||||
let height = crafing_inventory.get_height();
|
let height = crafing_inventory.get_height();
|
||||||
for i in 0..width {
|
for i in 0..width {
|
||||||
for j in 0..height {
|
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);
|
let input_slot = NormalSlot::new(crafing_inventory.clone(), j + i * width);
|
||||||
self.add_slot(Arc::new(input_slot));
|
self.add_slot(Arc::new(input_slot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.add_listener(result_slot).await;
|
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 {
|
pub struct CraftingTableScreenHandler {
|
||||||
/// Core screen handler behavior (slots, sync ID, listeners).
|
|
||||||
behaviour: ScreenHandlerBehaviour,
|
behaviour: ScreenHandlerBehaviour,
|
||||||
/// The 3x3 crafting grid inventory.
|
|
||||||
crafting_inventory: Arc<dyn RecipeInputInventory>,
|
crafting_inventory: Arc<dyn RecipeInputInventory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CraftingTableScreenHandler {
|
impl CraftingTableScreenHandler {
|
||||||
/// Creates a new crafting table screen handler.
|
pub async fn new(
|
||||||
///
|
sync_id: u8,
|
||||||
/// # Arguments
|
player_inventory: &Arc<PlayerInventory>,
|
||||||
/// - `sync_id` - The sync ID for client-server matching
|
provider: Option<Arc<dyn RecipeProvider>>,
|
||||||
/// - `player_inventory` - The player's inventory
|
) -> Self {
|
||||||
pub async fn new(sync_id: u8, player_inventory: &Arc<PlayerInventory>) -> Self {
|
|
||||||
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
||||||
Arc::new(CraftingInventory::new(3, 3));
|
Arc::new(CraftingInventory::new(3, 3));
|
||||||
|
|
||||||
let mut crafting_table_handler = Self {
|
let mut crafting_table_handler = Self {
|
||||||
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Crafting)),
|
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Crafting)),
|
||||||
crafting_inventory: crafting_inventory.clone(),
|
crafting_inventory: crafting_inventory.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
crafting_table_handler
|
crafting_table_handler
|
||||||
.add_recipe_slots(crafting_inventory)
|
.add_recipe_slots(crafting_inventory, provider)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Add player inventory slots
|
|
||||||
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
|
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
|
||||||
crafting_table_handler.add_player_slots(&player_inventory);
|
crafting_table_handler.add_player_slots(&player_inventory);
|
||||||
|
|
||||||
crafting_table_handler
|
crafting_table_handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -567,33 +581,22 @@ impl ScreenHandler for CraftingTableScreenHandler {
|
|||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
|
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
|
||||||
&mut self.behaviour
|
&mut self.behaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.default_on_closed(player).await;
|
self.default_on_closed(player).await;
|
||||||
//TODO: this.craftingResultInventory.clear();
|
|
||||||
self.drop_inventory(player, self.crafting_inventory.clone())
|
self.drop_inventory(player, self.crafting_inventory.clone())
|
||||||
.await;
|
.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>(
|
fn quick_move<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
player: &'a dyn InventoryPlayer,
|
player: &'a dyn InventoryPlayer,
|
||||||
@ -601,74 +604,52 @@ impl ScreenHandler for CraftingTableScreenHandler {
|
|||||||
) -> ItemStackFuture<'a> {
|
) -> ItemStackFuture<'a> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
||||||
|
|
||||||
if slot.has_stack().await {
|
if slot.has_stack().await {
|
||||||
let slot_stack = slot.get_stack().await;
|
let slot_stack = slot.get_stack().await;
|
||||||
let mut slot_stack = slot_stack.lock().await;
|
let mut slot_stack = slot_stack.lock().await;
|
||||||
let stack_prev = slot_stack.clone();
|
let stack_prev = slot_stack.clone();
|
||||||
|
|
||||||
if slot_index == 0 {
|
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 {
|
if !self.insert_item(&mut slot_stack, 10, 46, true).await {
|
||||||
return ItemStack::EMPTY.clone();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
} else if (1..=9).contains(&slot_index) {
|
} 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 {
|
if !self.insert_item(&mut slot_stack, 10, 46, false).await {
|
||||||
return ItemStack::EMPTY.clone();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
} else if (10..46).contains(&slot_index) {
|
} 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 !self.insert_item(&mut slot_stack, 1, 10, false).await {
|
||||||
// If that fails, try moving within player inventory
|
|
||||||
if slot_index < 37 {
|
if slot_index < 37 {
|
||||||
// From main inventory to hotbar
|
|
||||||
if !self.insert_item(&mut slot_stack, 37, 46, false).await {
|
if !self.insert_item(&mut slot_stack, 37, 46, false).await {
|
||||||
return ItemStack::EMPTY.clone();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
} else {
|
} else if !self.insert_item(&mut slot_stack, 10, 37, false).await {
|
||||||
// From hotbar to main inventory
|
|
||||||
if !self.insert_item(&mut slot_stack, 10, 37, false).await {
|
|
||||||
return ItemStack::EMPTY.clone();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if !self.insert_item(&mut slot_stack, 10, 46, false).await {
|
||||||
} 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();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let stack = slot_stack.clone();
|
let stack = slot_stack.clone();
|
||||||
drop(slot_stack); // release the lock before calling other methods
|
drop(slot_stack);
|
||||||
|
|
||||||
if stack.is_empty() {
|
if stack.is_empty() {
|
||||||
slot.set_stack_prev(ItemStack::EMPTY.clone(), stack_prev.clone())
|
slot.set_stack_prev(ItemStack::EMPTY.clone(), stack_prev.clone())
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
slot.mark_dirty().await;
|
slot.mark_dirty().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.item_count == stack_prev.item_count {
|
if stack.item_count == stack_prev.item_count {
|
||||||
// Nothing changed
|
|
||||||
return ItemStack::EMPTY.clone();
|
return ItemStack::EMPTY.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
slot.on_take_item(player, &stack).await;
|
slot.on_take_item(player, &stack).await;
|
||||||
|
|
||||||
if slot_index == 0 {
|
if slot_index == 0 {
|
||||||
slot.on_quick_move_crafted(stack.clone(), stack_prev.clone())
|
slot.on_quick_move_crafted(stack.clone(), stack_prev.clone())
|
||||||
.await;
|
.await;
|
||||||
// For crafting result slot, drop any remaining items
|
|
||||||
if !stack.is_empty() {
|
if !stack.is_empty() {
|
||||||
player.drop_item(stack, false).await;
|
player.drop_item(stack, false).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stack_prev;
|
return stack_prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack::EMPTY.clone()
|
ItemStack::EMPTY.clone()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,5 @@
|
|||||||
|
|
||||||
pub mod crafting_inventory;
|
pub mod crafting_inventory;
|
||||||
pub mod crafting_screen_handler;
|
pub mod crafting_screen_handler;
|
||||||
|
pub mod recipe_provider;
|
||||||
pub mod recipes;
|
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>,
|
player_inventory: &Arc<PlayerInventory>,
|
||||||
window_type: Option<WindowType>,
|
window_type: Option<WindowType>,
|
||||||
sync_id: u8,
|
sync_id: u8,
|
||||||
|
provider: Option<Arc<dyn crate::crafting::recipe_provider::RecipeProvider>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
let crafting_inventory: Arc<dyn RecipeInputInventory> =
|
||||||
Arc::new(CraftingInventory::new(2, 2));
|
Arc::new(CraftingInventory::new(2, 2));
|
||||||
@ -91,7 +92,7 @@ impl PlayerScreenHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
player_screen_handler
|
player_screen_handler
|
||||||
.add_recipe_slots(crafting_inventory)
|
.add_recipe_slots(crafting_inventory, provider)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Add armor slots (head, chest, legs, feet)
|
// 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 %world.{%world};
|
||||||
use text.{text-component};
|
use text.{text-component};
|
||||||
use uuid.{uuid};
|
use uuid.{uuid};
|
||||||
|
use recipe.{recipe-manager};
|
||||||
|
|
||||||
/// Represents the difficulty setting of a Minecraft server.
|
/// Represents the difficulty setting of a Minecraft server.
|
||||||
enum difficulty {
|
enum difficulty {
|
||||||
@ -114,5 +115,8 @@ interface server {
|
|||||||
|
|
||||||
/// Returns the server's default gamemode.
|
/// Returns the server's default gamemode.
|
||||||
get-default-gamemode: func() -> game-mode;
|
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 little_endian;
|
||||||
pub mod lp_vector_3d;
|
pub mod lp_vector_3d;
|
||||||
pub mod optional_int;
|
pub mod optional_int;
|
||||||
|
pub mod recipe;
|
||||||
mod u24_type;
|
mod u24_type;
|
||||||
pub mod uuid;
|
pub mod uuid;
|
||||||
pub mod var_int;
|
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_SMOKER_FOOD: i32 = 9;
|
||||||
const CATEGORY_CAMPFIRE: i32 = 12;
|
const CATEGORY_CAMPFIRE: i32 = 12;
|
||||||
|
|
||||||
|
use crate::codec::recipe::DynamicRecipe;
|
||||||
|
|
||||||
/// Clientbound packet that adds recipes to the client's recipe book.
|
/// Clientbound packet that adds recipes to the client's recipe book.
|
||||||
/// `replace = true` means the client replaces its current recipe list.
|
/// `replace = true` means the client replaces its current recipe list.
|
||||||
#[java_packet(PLAY_RECIPE_BOOK_ADD)]
|
#[java_packet(PLAY_RECIPE_BOOK_ADD)]
|
||||||
pub struct CRecipeBookAdd {
|
pub struct CRecipeBookAdd<'a> {
|
||||||
pub replace: bool,
|
pub replace: bool,
|
||||||
|
pub dynamic_recipes: &'a [DynamicRecipe],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CRecipeBookAdd {
|
impl<'a> CRecipeBookAdd<'a> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(replace: bool) -> Self {
|
pub const fn new(replace: bool, dynamic_recipes: &'a [DynamicRecipe]) -> Self {
|
||||||
Self { replace }
|
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))?;
|
write.write_var_int(&VarInt(encoded))?;
|
||||||
Ok(())
|
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 {
|
const fn entry_flags(replace: bool, notification: bool, highlight: bool) -> u8 {
|
||||||
if replace {
|
if replace {
|
||||||
return 0;
|
return 0;
|
||||||
@ -463,7 +455,10 @@ fn write_entry(
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientPacket for CRecipeBookAdd {
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
impl ClientPacket for CRecipeBookAdd<'_> {
|
||||||
fn write_packet_data(
|
fn write_packet_data(
|
||||||
&self,
|
&self,
|
||||||
write: impl Write,
|
write: impl Write,
|
||||||
@ -494,13 +489,14 @@ impl ClientPacket for CRecipeBookAdd {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.count();
|
.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)
|
// Entry count (VarInt)
|
||||||
write.write_var_int(&VarInt(total as i32))?;
|
write.write_var_int(&VarInt(total as i32))?;
|
||||||
|
|
||||||
let mut display_id: i32 = 0;
|
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 mut next_group_id: i32 = 0;
|
||||||
let highlight = !self.replace;
|
let highlight = !self.replace;
|
||||||
|
|
||||||
@ -511,13 +507,15 @@ impl ClientPacket for CRecipeBookAdd {
|
|||||||
group,
|
group,
|
||||||
show_notification,
|
show_notification,
|
||||||
..
|
..
|
||||||
} => (*group, *show_notification),
|
} => (group.map(Cow::Borrowed), *show_notification),
|
||||||
CraftingRecipeTypes::CraftingShapeless { group, .. }
|
CraftingRecipeTypes::CraftingShapeless { group, .. }
|
||||||
| CraftingRecipeTypes::CraftingTransmute { group, .. } => (*group, true),
|
| CraftingRecipeTypes::CraftingTransmute { group, .. } => {
|
||||||
|
(group.map(Cow::Borrowed), true)
|
||||||
|
}
|
||||||
CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
||||||
| CraftingRecipeTypes::CraftingSpecial => (None, true),
|
| 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 flags = entry_flags(self.replace, notification, highlight);
|
||||||
let written = write_entry(
|
let written = write_entry(
|
||||||
&mut write,
|
&mut write,
|
||||||
@ -559,7 +557,11 @@ impl ClientPacket for CRecipeBookAdd {
|
|||||||
CookingRecipeType::Smoking(r) => (CATEGORY_SMOKER_FOOD, r.group),
|
CookingRecipeType::Smoking(r) => (CATEGORY_SMOKER_FOOD, r.group),
|
||||||
CookingRecipeType::CampfireCooking(r) => (CATEGORY_CAMPFIRE, 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);
|
let flags = entry_flags(self.replace, true, highlight);
|
||||||
write_entry(
|
write_entry(
|
||||||
&mut write,
|
&mut write,
|
||||||
@ -578,8 +580,318 @@ impl ClientPacket for CRecipeBookAdd {
|
|||||||
display_id += 1;
|
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
|
// replace flag
|
||||||
write.write_bool(self.replace)?;
|
write.write_bool(self.replace)?;
|
||||||
Ok(())
|
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> {
|
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
args.player
|
args.player
|
||||||
.open_handled_screen(&CraftingTableScreenFactory, Some(*args.position))
|
.open_handled_screen(
|
||||||
|
&CraftingTableScreenFactory(args.server.recipe_manager.clone()),
|
||||||
|
Some(*args.position),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
BlockActionResult::Success
|
BlockActionResult::Success
|
||||||
@ -27,7 +30,7 @@ impl BlockBehaviour for CraftingTableBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CraftingTableScreenFactory;
|
struct CraftingTableScreenFactory(Arc<crate::server::RecipeManager>);
|
||||||
|
|
||||||
impl ScreenHandlerFactory for CraftingTableScreenFactory {
|
impl ScreenHandlerFactory for CraftingTableScreenFactory {
|
||||||
fn create_screen_handler<'a>(
|
fn create_screen_handler<'a>(
|
||||||
@ -37,7 +40,9 @@ impl ScreenHandlerFactory for CraftingTableScreenFactory {
|
|||||||
_player: &'a dyn InventoryPlayer,
|
_player: &'a dyn InventoryPlayer,
|
||||||
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
||||||
Box::pin(async move {
|
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));
|
let concrete_arc = Arc::new(Mutex::new(handler));
|
||||||
|
|
||||||
Some(concrete_arc as SharedScreenHandler)
|
Some(concrete_arc as SharedScreenHandler)
|
||||||
|
|||||||
@ -536,7 +536,13 @@ impl Player {
|
|||||||
let ender_chest_inventory = Arc::new(EnderChestInventory::new());
|
let ender_chest_inventory = Arc::new(EnderChestInventory::new());
|
||||||
|
|
||||||
let player_screen_handler = Arc::new(Mutex::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())
|
// Initialize abilities based on gamemode (like vanilla's GameMode.setAbilities())
|
||||||
|
|||||||
@ -60,6 +60,7 @@ pub mod config;
|
|||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod play;
|
pub mod play;
|
||||||
|
pub mod recipe_helper;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
use crate::entity::player::Player;
|
use crate::entity::player::Player;
|
||||||
@ -945,8 +946,8 @@ impl JavaClient {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
id if id == SPlaceRecipe::to_id(version) => {
|
id if id == SPlaceRecipe::to_id(version) => {
|
||||||
self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?)
|
let packet = SPlaceRecipe::read(payload, &version)?;
|
||||||
.await;
|
self.handle_place_recipe(server, player, packet).await;
|
||||||
}
|
}
|
||||||
id if id
|
id if id
|
||||||
== pumpkin_protocol::java::server::play::SCustomClickAction::to_id(version) =>
|
== pumpkin_protocol::java::server::play::SCustomClickAction::to_id(version) =>
|
||||||
|
|||||||
@ -926,80 +926,19 @@ impl JavaClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn handle_place_recipe(&self, player: &Arc<Player>, packet: SPlaceRecipe) {
|
pub async fn handle_place_recipe(
|
||||||
use pumpkin_data::recipes::{
|
&self,
|
||||||
CookingRecipeType, CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING,
|
server: &Arc<Server>,
|
||||||
RecipeIngredientTypes,
|
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;
|
use pumpkin_data::screen::WindowType;
|
||||||
|
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_id = packet.recipe_display_id.0 as usize;
|
let target_id = packet.recipe_display_id.0 as usize;
|
||||||
let use_max = packet.use_max_items;
|
let use_max = packet.use_max_items;
|
||||||
@ -1015,6 +954,8 @@ impl JavaClient {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
|
let cooking_display_count = RECIPES_COOKING.len();
|
||||||
|
let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await;
|
||||||
|
|
||||||
let (grid_width, crafting_inv) = {
|
let (grid_width, crafting_inv) = {
|
||||||
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
|
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())
|
(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 {
|
if target_id < crafting_display_count {
|
||||||
// Crafting recipe
|
// Crafting recipe
|
||||||
let mut counter = 0usize;
|
let mut counter = 0usize;
|
||||||
@ -1044,10 +988,6 @@ impl JavaClient {
|
|||||||
});
|
});
|
||||||
let Some(recipe) = recipe else { return };
|
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 {
|
match recipe {
|
||||||
CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => {
|
CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => {
|
||||||
for (row, row_str) in pattern.iter().enumerate() {
|
for (row, row_str) in pattern.iter().enumerate() {
|
||||||
@ -1055,25 +995,67 @@ impl JavaClient {
|
|||||||
if ch != ' '
|
if ch != ' '
|
||||||
&& let Some(ing) =
|
&& let Some(ing) =
|
||||||
key.iter().find_map(|(k, v)| (*k == ch).then_some(v))
|
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, .. } => {
|
CraftingRecipeTypes::CraftingShapeless { ingredients, .. } => {
|
||||||
for (i, ing) in ingredients.iter().enumerate().take(grid_size) {
|
for (i, ing) in ingredients.iter().enumerate().take(grid_size) {
|
||||||
ingredient_slots[i] = Some(ing);
|
ingredient_slots[i] = Some(GenericIngredient::Vanilla(ing));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CraftingRecipeTypes::CraftingTransmute {
|
CraftingRecipeTypes::CraftingTransmute {
|
||||||
input, material, ..
|
input, material, ..
|
||||||
} => {
|
} => {
|
||||||
ingredient_slots[0] = Some(input);
|
if grid_size >= 2 {
|
||||||
ingredient_slots[1] = Some(material);
|
ingredient_slots[0] = Some(GenericIngredient::Vanilla(input));
|
||||||
|
ingredient_slots[1] = Some(GenericIngredient::Vanilla(material));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => return,
|
_ => 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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).
|
// Check if this exact recipe is already placed (determines stacking vs fresh fill).
|
||||||
let recipe_matches = {
|
let recipe_matches = {
|
||||||
@ -1125,7 +1107,7 @@ impl JavaClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine how many of each ingredient to place per slot.
|
// Determine how many of each ingredient to place per slot.
|
||||||
let active_ingredients: Vec<&RecipeIngredientTypes> =
|
let active_ingredients: Vec<GenericIngredient<'_>> =
|
||||||
ingredient_slots.iter().flatten().copied().collect();
|
ingredient_slots.iter().flatten().copied().collect();
|
||||||
let amount_to_craft = if use_max {
|
let amount_to_craft = if use_max {
|
||||||
compute_biggest_craftable(&active_ingredients, &player.inventory).await
|
compute_biggest_craftable(&active_ingredients, &player.inventory).await
|
||||||
@ -1146,68 +1128,14 @@ impl JavaClient {
|
|||||||
let Some(ingredient) = ing else { continue };
|
let Some(ingredient) = ing else { continue };
|
||||||
let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await;
|
let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await;
|
||||||
if !taken.is_empty() {
|
if !taken.is_empty() {
|
||||||
crafting_inv.set_stack(idx, taken).await;
|
*crafting_inv.get_stack(idx).await.lock().await = taken;
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Cooking recipe
|
|
||||||
let cooking_index = target_id - crafting_display_count;
|
|
||||||
let Some(recipe) = RECIPES_COOKING.get(cooking_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
|
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
|
||||||
screen_handler_arc.lock().await.send_content_updates().await;
|
screen_handler_arc.lock().await.send_content_updates().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_swing_arm(&self, player: &Arc<Player>, swing_arm: SSwingArm) {
|
pub async fn handle_swing_arm(&self, player: &Arc<Player>, swing_arm: SSwingArm) {
|
||||||
player.update_last_action_time();
|
player.update_last_action_time();
|
||||||
let Ok(hand) = Hand::try_from(swing_arm.hand.0) else {
|
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,
|
api::gui::PluginGui,
|
||||||
loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg},
|
loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg},
|
||||||
},
|
},
|
||||||
server::Server,
|
server::{RecipeManager, Server},
|
||||||
world::World,
|
world::World,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ pub type CommandSenderResource = WasmResource<CommandSender>;
|
|||||||
pub type ConsumedArgsResource = WasmResource<OwnedConsumedArgs>;
|
pub type ConsumedArgsResource = WasmResource<OwnedConsumedArgs>;
|
||||||
pub type CommandNodeResource = WasmResource<NonLeafNodeBuilder>;
|
pub type CommandNodeResource = WasmResource<NonLeafNodeBuilder>;
|
||||||
pub type ItemStackResource = WasmResource<Arc<Mutex<pumpkin_data::item_stack::ItemStack>>>;
|
pub type ItemStackResource = WasmResource<Arc<Mutex<pumpkin_data::item_stack::ItemStack>>>;
|
||||||
|
pub type RecipeManagerResource = WasmResource<Arc<RecipeManager>>;
|
||||||
|
|
||||||
pub type OwnedConsumedArgs = HashMap<String, OwnedArg>;
|
pub type OwnedConsumedArgs = HashMap<String, OwnedArg>;
|
||||||
|
|
||||||
@ -229,6 +230,16 @@ impl PluginHostState {
|
|||||||
let resource = self.resource_table.push(ItemStackResource { provider })?;
|
let resource = self.resource_table.push(ItemStackResource { provider })?;
|
||||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
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 {
|
pub struct PluginHttpHooks {
|
||||||
|
|||||||
@ -348,6 +348,7 @@ pub fn serialize_java_packet(
|
|||||||
ClientboundPacket::CRecipeBookAdd(data) => {
|
ClientboundPacket::CRecipeBookAdd(data) => {
|
||||||
let p = pumpkin_protocol::java::client::play::CRecipeBookAdd {
|
let p = pumpkin_protocol::java::client::play::CRecipeBookAdd {
|
||||||
replace: data.replace.try_into().unwrap(),
|
replace: data.replace.try_into().unwrap(),
|
||||||
|
dynamic_recipes: &[],
|
||||||
};
|
};
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
crate::net::java::JavaClient::write_packet_for_version(&p, version, &mut buf).unwrap();
|
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 logging;
|
||||||
pub mod permission;
|
pub mod permission;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
pub mod recipe;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod scoreboard;
|
pub mod scoreboard;
|
||||||
pub mod server;
|
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 wasmtime::component::Resource;
|
||||||
|
|
||||||
use crate::command::CommandSender;
|
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 pumpkin::plugin::server::CommandSender as WasmCommandSender;
|
||||||
|
|
||||||
use super::player::text_component_from_resource;
|
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<()> {
|
async fn drop(&mut self, rep: Resource<Server>) -> wasmtime::Result<()> {
|
||||||
self.resource_table
|
self.resource_table
|
||||||
.delete::<ServerResource>(Resource::new_own(rep.rep()))
|
.delete::<ServerResource>(Resource::new_own(rep.rep()))
|
||||||
|
|||||||
@ -54,11 +54,14 @@ use tokio_util::task::TaskTracker;
|
|||||||
|
|
||||||
mod connection_cache;
|
mod connection_cache;
|
||||||
mod key_store;
|
mod key_store;
|
||||||
|
pub mod recipe;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod seasonal_events;
|
pub mod seasonal_events;
|
||||||
pub mod tick_rate_manager;
|
pub mod tick_rate_manager;
|
||||||
pub mod ticker;
|
pub mod ticker;
|
||||||
|
|
||||||
|
pub use recipe::RecipeManager;
|
||||||
|
|
||||||
use crate::command::args::entities::{
|
use crate::command::args::entities::{
|
||||||
EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition,
|
EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition,
|
||||||
};
|
};
|
||||||
@ -99,6 +102,7 @@ pub struct Server {
|
|||||||
pub dimensions: Vec<Dimension>,
|
pub dimensions: Vec<Dimension>,
|
||||||
/// Assigns unique IDs to containers.
|
/// Assigns unique IDs to containers.
|
||||||
container_id: AtomicU32,
|
container_id: AtomicU32,
|
||||||
|
pub recipe_manager: Arc<recipe::RecipeManager>,
|
||||||
/// Assigns unique IDs to maps.
|
/// Assigns unique IDs to maps.
|
||||||
map_id: AtomicI32,
|
map_id: AtomicI32,
|
||||||
/// Mojang's public keys, used for chat session signing
|
/// Mojang's public keys, used for chat session signing
|
||||||
@ -223,6 +227,7 @@ impl Server {
|
|||||||
))),
|
))),
|
||||||
permission_registry,
|
permission_registry,
|
||||||
container_id: 0.into(),
|
container_id: 0.into(),
|
||||||
|
recipe_manager: Arc::new(recipe::RecipeManager::new()),
|
||||||
map_id: level_info.load().map_id.into(),
|
map_id: level_info.load().map_id.into(),
|
||||||
worlds: ArcSwap::from_pointee(vec![]),
|
worlds: ArcSwap::from_pointee(vec![]),
|
||||||
dimensions: 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},
|
world::{RAW, WorldEvent},
|
||||||
};
|
};
|
||||||
use pumpkin_data::{BlockDirection, BlockState, translation};
|
use pumpkin_data::{BlockDirection, BlockState, translation};
|
||||||
|
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
|
||||||
use pumpkin_inventory::screen_handler::InventoryPlayer;
|
use pumpkin_inventory::screen_handler::InventoryPlayer;
|
||||||
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
|
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
|
||||||
use pumpkin_protocol::bedrock::client::set_actor_data::{
|
use pumpkin_protocol::bedrock::client::set_actor_data::{
|
||||||
@ -2265,8 +2266,9 @@ impl World {
|
|||||||
java_client
|
java_client
|
||||||
.send_packet_now(&CRecipeBookSettings::default_closed())
|
.send_packet_now(&CRecipeBookSettings::default_closed())
|
||||||
.await;
|
.await;
|
||||||
|
let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await;
|
||||||
java_client
|
java_client
|
||||||
.send_packet_now(&CRecipeBookAdd::new(true))
|
.send_packet_now(&CRecipeBookAdd::new(true, &dynamic_recipes))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user