Pumpkin/pumpkin-data/build/recipes.rs
Alexander Medvedev b0ab5006ca chore: remove unused deps
also apply some clippy lints in some places
2026-02-09 00:07:39 +01:00

626 lines
21 KiB
Rust

use std::{collections::BTreeMap, fs};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum RecipeTypes {
#[serde(rename = "minecraft:blasting")]
Blasting(CookingRecipeStruct),
#[serde(rename = "minecraft:campfire_cooking")]
CampfireCooking(CookingRecipeStruct),
#[serde(rename = "minecraft:crafting_shaped")]
CraftingShaped(CraftingShapedRecipeStruct),
#[serde(rename = "minecraft:crafting_shapeless")]
CraftingShapeless(CraftingShapelessRecipeStruct),
#[serde(rename = "minecraft:crafting_transmute")]
CraftingTransmute(CraftingTransmuteRecipeStruct),
#[serde(rename = "minecraft:crafting_decorated_pot")]
CraftingDecoratedPot(CraftingDecoratedPotStruct),
#[serde(rename = "minecraft:smelting")]
Smelting(CookingRecipeStruct),
#[serde(rename = "minecraft:smithing_transform")]
SmithingTransform,
#[serde(rename = "minecraft:smithing_trim")]
SmithingTrim,
#[serde(rename = "minecraft:smoking")]
Smoking(CookingRecipeStruct),
#[serde(rename = "minecraft:stonecutting")]
Stonecutting,
#[serde(other)]
#[serde(rename = "minecraft:crafting_special_*")]
CraftingSpecial,
}
#[derive(Deserialize)]
pub struct CookingRecipeStruct {
category: Option<RecipeCategoryTypes>,
group: Option<String>,
ingredient: RecipeIngredientTypes,
cookingtime: i32,
experience: f32,
result: RecipeResultStruct,
}
impl CookingRecipeStruct {
/// Generate a recipe ID based on result, ingredient, and cooking type
/// Format: minecraft:{result}_from_{`cooking_type`}_{ingredient}
fn generate_recipe_id(&self, cooking_type: &str) -> String {
let result_name = self
.result
.id
.strip_prefix("minecraft:")
.unwrap_or(&self.result.id);
let ingredient_name = match &self.ingredient {
RecipeIngredientTypes::Simple(s) => {
if s.starts_with('#') {
// Tagged ingredient - strip # and replace : with _
s.strip_prefix('#').unwrap_or(s).replace(':', "_")
} else {
s.strip_prefix("minecraft:").unwrap_or(s).to_string()
}
}
RecipeIngredientTypes::OneOf(items) => {
// Use first item for ID generation
items
.first()
.map_or("unknown", |s| s.strip_prefix("minecraft:").unwrap_or(s))
.to_string()
}
};
format!("minecraft:{result_name}_from_{cooking_type}_{ingredient_name}")
}
fn to_tokens_with_id(&self, tokens: &mut TokenStream, recipe_id: &str) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
let group = if let Some(group) = &self.group {
quote! { Some(#group) }
} else {
quote! { None }
};
let ingredient = self.ingredient.to_token_stream();
let cookingtime = self.cookingtime.to_token_stream();
let experience = self.experience.to_token_stream();
let result = self.result.to_token_stream();
tokens.extend(quote! {
recipe_id: #recipe_id,
category: #category,
group: #group,
ingredient: #ingredient,
cookingtime: #cookingtime,
experience: #experience,
result: #result,
});
}
}
impl ToTokens for CookingRecipeStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
let group = if let Some(group) = &self.group {
quote! { Some(#group) }
} else {
quote! { None }
};
let ingredient = self.ingredient.to_token_stream();
let cookingtime = self.cookingtime.to_token_stream();
let experience = self.experience.to_token_stream();
let result = self.result.to_token_stream();
tokens.extend(quote! {
//CookingRecipeType::Blasting,CampfireCooking,Smelting,Smoking{
category: #category,
group: #group,
ingredient: #ingredient,
cookingtime: #cookingtime,
experience: #experience,
result: #result,
//}
});
}
}
#[derive(Deserialize)]
pub struct CraftingShapedRecipeStruct {
category: Option<RecipeCategoryTypes>,
group: Option<String>,
show_notification: Option<bool>,
key: BTreeMap<String, RecipeIngredientTypes>,
pattern: Vec<String>,
result: RecipeResultStruct,
}
impl ToTokens for CraftingShapedRecipeStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
let group = if let Some(group) = &self.group {
quote! { Some(#group) }
} else {
quote! { None }
};
let show_notification = self.show_notification.unwrap_or(true);
let key = self
.key
.iter()
.map(|(key, ingredient)| {
let key = key.chars().next().unwrap();
quote! { (#key, #ingredient) }
})
.collect::<Vec<_>>();
let pattern = self
.pattern
.iter()
.map(quote::ToTokens::to_token_stream)
.collect::<Vec<_>>();
let result = self.result.to_token_stream();
tokens.extend(quote! {
CraftingRecipeTypes::CraftingShaped {
category: #category,
group: #group,
show_notification: #show_notification,
key: &[#(#key),*],
pattern: &[#(#pattern),*],
result: #result,
}
});
}
}
#[derive(Deserialize)]
pub struct CraftingShapelessRecipeStruct {
category: Option<RecipeCategoryTypes>,
group: Option<String>,
ingredients: Vec<RecipeIngredientTypes>,
result: RecipeResultStruct,
}
impl ToTokens for CraftingShapelessRecipeStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
let group = if let Some(group) = &self.group {
quote! { Some(#group) }
} else {
quote! { None }
};
let ingredients = self
.ingredients
.iter()
.map(quote::ToTokens::to_token_stream)
.collect::<Vec<_>>();
let result = self.result.to_token_stream();
tokens.extend(quote! {
CraftingRecipeTypes::CraftingShapeless {
category: #category,
group: #group,
ingredients: &[#(#ingredients),*],
result: #result,
}
});
}
}
#[derive(Deserialize)]
pub struct CraftingTransmuteRecipeStruct {
category: Option<RecipeCategoryTypes>,
group: Option<String>,
input: RecipeIngredientTypes,
material: RecipeIngredientTypes,
result: RecipeResultStruct,
}
impl ToTokens for CraftingTransmuteRecipeStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
let group = if let Some(group) = &self.group {
quote! { Some(#group) }
} else {
quote! { None }
};
let input = self.input.to_token_stream();
let material = self.material.to_token_stream();
let result = self.result.to_token_stream();
tokens.extend(quote! {
CraftingRecipeTypes::CraftingTransmute {
category: #category,
group: #group,
input: #input,
material: #material,
result: #result,
}
});
}
}
#[derive(Deserialize)]
pub struct CraftingDecoratedPotStruct {
category: Option<RecipeCategoryTypes>,
}
impl ToTokens for CraftingDecoratedPotStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let category = match &self.category {
Some(category) => category.to_token_stream(),
None => RecipeCategoryTypes::Misc.to_token_stream(),
};
tokens.extend(quote! {
CraftingRecipeTypes::CraftingDecoratedPot {
category: #category,
}
});
}
}
#[derive(Deserialize)]
pub struct RecipeResultStruct {
id: String,
count: Option<u8>,
// TODO: components: Option<RecipeResultComponentsStruct>,
}
impl ToTokens for RecipeResultStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
let id = self.id.to_token_stream();
let count = self.count.unwrap_or(1).to_token_stream();
tokens.extend(quote! {
RecipeResultStruct {
id: #id,
count: #count,
}
});
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum RecipeIngredientTypes {
Simple(String),
OneOf(Vec<String>),
}
impl ToTokens for RecipeIngredientTypes {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = match self {
Self::Simple(ingredient) => {
if ingredient.starts_with('#') {
quote! { RecipeIngredientTypes::Tagged(#ingredient) }
} else {
quote! { RecipeIngredientTypes::Simple(#ingredient) }
}
}
Self::OneOf(ingredients) => {
let ingredients = ingredients
.iter()
.map(quote::ToTokens::to_token_stream)
.collect::<Vec<_>>();
quote! { RecipeIngredientTypes::OneOf(&[#(#ingredients),*]) }
}
};
tokens.extend(name);
}
}
#[derive(Deserialize)]
pub enum RecipeCategoryTypes {
#[serde(rename = "equipment")]
Equipment,
#[serde(rename = "building")]
Building,
#[serde(rename = "redstone")]
Restone,
#[serde(rename = "misc")]
Misc,
#[serde(rename = "food")]
Food,
#[serde(rename = "blocks")]
Blocks,
}
impl ToTokens for RecipeCategoryTypes {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = match self {
Self::Equipment => {
quote! { RecipeCategoryTypes::Equipment }
}
Self::Building => {
quote! { RecipeCategoryTypes::Building }
}
Self::Restone => {
quote! { RecipeCategoryTypes::Restone }
}
Self::Misc => {
quote! { RecipeCategoryTypes::Misc }
}
Self::Food => {
quote! { RecipeCategoryTypes::Food }
}
Self::Blocks => {
quote! { RecipeCategoryTypes::Blocks }
}
};
tokens.extend(name);
}
}
pub fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/recipes.json");
let recipes_assets: Vec<RecipeTypes> =
serde_json::from_str(&fs::read_to_string("../assets/recipes.json").unwrap())
.expect("Failed to parse recipes.json");
let mut crafting_recipes = Vec::new();
let mut cooking_recipes = Vec::new();
for recipe in recipes_assets {
match recipe {
RecipeTypes::Blasting(recipe) => {
let recipe_id = recipe.generate_recipe_id("blasting");
let mut common_cooking_token = TokenStream::new();
recipe.to_tokens_with_id(&mut common_cooking_token, &recipe_id);
let blasting_token = quote! {
CookingRecipeType::Blasting (CookingRecipe {
#common_cooking_token
})
};
cooking_recipes.push(blasting_token);
}
RecipeTypes::CampfireCooking(recipe) => {
let recipe_id = recipe.generate_recipe_id("campfire_cooking");
let mut common_cooking_token = TokenStream::new();
recipe.to_tokens_with_id(&mut common_cooking_token, &recipe_id);
let campfire_token = quote! {
CookingRecipeType::CampfireCooking (CookingRecipe {
#common_cooking_token
})
};
cooking_recipes.push(campfire_token);
}
RecipeTypes::CraftingShaped(recipe) => {
crafting_recipes.push(recipe.to_token_stream());
}
RecipeTypes::CraftingShapeless(recipe) => {
crafting_recipes.push(recipe.to_token_stream());
}
RecipeTypes::CraftingTransmute(recipe) => {
crafting_recipes.push(recipe.to_token_stream());
}
RecipeTypes::CraftingDecoratedPot(recipe) => {
crafting_recipes.push(recipe.to_token_stream());
}
RecipeTypes::Smelting(recipe) => {
let recipe_id = recipe.generate_recipe_id("smelting");
let mut common_cooking_token = TokenStream::new();
recipe.to_tokens_with_id(&mut common_cooking_token, &recipe_id);
let smelting_token = quote! {
CookingRecipeType::Smelting(CookingRecipe {
#common_cooking_token
})
};
cooking_recipes.push(smelting_token);
}
RecipeTypes::SmithingTransform => {}
RecipeTypes::SmithingTrim => {}
RecipeTypes::Smoking(recipe) => {
let recipe_id = recipe.generate_recipe_id("smoking");
let mut common_cooking_token = TokenStream::new();
recipe.to_tokens_with_id(&mut common_cooking_token, &recipe_id);
let smoking_token = quote! {
CookingRecipeType::Smoking(CookingRecipe{
#common_cooking_token
})
};
cooking_recipes.push(smoking_token);
}
RecipeTypes::Stonecutting => {}
RecipeTypes::CraftingSpecial => {}
}
}
quote! {
use crate::tag::Taggable;
use crate::item::Item;
#[derive(Clone, Debug)]
pub enum CraftingRecipeTypes {
CraftingShaped {
category: RecipeCategoryTypes,
group: Option<&'static str>,
show_notification: bool,
key: &'static [(char, RecipeIngredientTypes)],
pattern: &'static [&'static str],
result: RecipeResultStruct,
},
CraftingShapeless {
category: RecipeCategoryTypes,
group: Option<&'static str>,
ingredients: &'static [RecipeIngredientTypes],
result: RecipeResultStruct,
},
CraftingTransmute {
category: RecipeCategoryTypes,
group: Option<&'static str>,
input: RecipeIngredientTypes,
material: RecipeIngredientTypes,
result: RecipeResultStruct,
},
CraftingDecoratedPot {
category: RecipeCategoryTypes,
},
CraftingSpecial,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct CookingRecipe {
/// Vanilla-compatible recipe ID (e.g., "minecraft:iron_ingot_from_smelting_iron_ore")
pub recipe_id: &'static str,
pub category: RecipeCategoryTypes,
pub group: Option<&'static str>,
pub ingredient: RecipeIngredientTypes,
pub cookingtime: i32,
pub experience: f32,
pub result: RecipeResultStruct,
}
#[derive(Clone, Debug)]
pub enum CookingRecipeType {
Blasting(CookingRecipe),
Smelting(CookingRecipe),
Smoking(CookingRecipe),
CampfireCooking(CookingRecipe),
}
#[derive(Clone, Debug)]
pub enum CookingRecipeKind {
Blasting,
Smelting,
Smoking,
CampfireCooking,
}
impl From<&CookingRecipeType> for CookingRecipeKind {
fn from(recipe_type: &CookingRecipeType) -> Self {
match recipe_type {
CookingRecipeType::Blasting(_) => CookingRecipeKind::Blasting,
CookingRecipeType::Smelting(_) => CookingRecipeKind::Smelting,
CookingRecipeType::Smoking(_) => CookingRecipeKind::Smoking,
CookingRecipeType::CampfireCooking(_) => CookingRecipeKind::CampfireCooking,
}
}
}
impl From<CookingRecipeType> for CookingRecipeKind {
fn from(recipe_type: CookingRecipeType) -> Self {
match recipe_type {
CookingRecipeType::Blasting(_) => CookingRecipeKind::Blasting,
CookingRecipeType::Smelting(_) => CookingRecipeKind::Smelting,
CookingRecipeType::Smoking(_) => CookingRecipeKind::Smoking,
CookingRecipeType::CampfireCooking(_) => CookingRecipeKind::CampfireCooking,
}
}
}
impl CookingRecipeKind {
pub fn to_type(self, recipe: CookingRecipe) -> CookingRecipeType {
match self {
CookingRecipeKind::Blasting => CookingRecipeType::Blasting(recipe),
CookingRecipeKind::Smelting => CookingRecipeType::Smelting(recipe),
CookingRecipeKind::Smoking => CookingRecipeType::Smoking(recipe),
CookingRecipeKind::CampfireCooking => CookingRecipeType::CampfireCooking(recipe),
}
}
}
#[derive(Clone, Debug)]
pub struct RecipeResultStruct {
pub id: &'static str,
pub count: u8,
}
#[derive(Clone, Debug)]
pub enum RecipeIngredientTypes {
Simple(&'static str),
Tagged(&'static str),
OneOf(&'static [&'static str]),
}
impl RecipeIngredientTypes {
pub fn match_item(&self, item: &Item) -> bool {
match self {
RecipeIngredientTypes::Simple(ingredient) => {
let name = format!("minecraft:{}", item.registry_key);
name == *ingredient
}
RecipeIngredientTypes::Tagged(tag) => item
.is_tagged_with(tag)
.expect("Crafting recipe used invalid tag"),
RecipeIngredientTypes::OneOf(ingredients) => {
let name = format!("minecraft:{}", item.registry_key);
ingredients.contains(&name.as_str())
}
}
}
}
#[derive(Clone, Debug)]
pub enum RecipeCategoryTypes {
Equipment,
Building,
Restone,
Misc,
Food,
Blocks,
}
pub static RECIPES_CRAFTING: &[CraftingRecipeTypes] = &[
#(#crafting_recipes),*
];
pub static RECIPES_COOKING: &[CookingRecipeType] = &[
#(#cooking_recipes ),*
];
pub fn get_cooking_recipe_with_ingredient(ingredient: &Item, recipe_type: CookingRecipeKind) -> Option<&'static CookingRecipe> {
RECIPES_COOKING.iter().find_map(|recipe| {
match (recipe, &recipe_type) {
(CookingRecipeType::Blasting(cooking_recipe), CookingRecipeKind::Blasting) |
(CookingRecipeType::Smelting(cooking_recipe), CookingRecipeKind::Smelting) |
(CookingRecipeType::Smoking(cooking_recipe), CookingRecipeKind::Smoking) |
(CookingRecipeType::CampfireCooking(cooking_recipe), CookingRecipeKind::CampfireCooking) => {
if cooking_recipe.ingredient.match_item(ingredient) {
Some(cooking_recipe)
} else {
None
}
}
_ => None
}
})
}
/// Get the experience value for a recipe by its recipe ID.
/// Used for calculating XP when extracting from furnace.
/// Recipe IDs are in vanilla format like "minecraft:iron_ingot_from_smelting_iron_ore"
pub fn get_recipe_experience(recipe_id: &str) -> Option<f32> {
RECIPES_COOKING.iter().find_map(|recipe| {
let cooking_recipe = match recipe {
CookingRecipeType::Blasting(r)
| CookingRecipeType::Smelting(r)
| CookingRecipeType::Smoking(r)
| CookingRecipeType::CampfireCooking(r) => r,
};
if cooking_recipe.recipe_id == recipe_id {
Some(cooking_recipe.experience)
} else {
None
}
})
}
}
}