diff --git a/steel-registry/build/build.rs b/steel-registry/build/build.rs index 2ecc6344cf50..fa2b9765c002 100644 --- a/steel-registry/build/build.rs +++ b/steel-registry/build/build.rs @@ -37,6 +37,7 @@ mod painting_variants; mod pig_sound_variants; mod pig_variants; mod poi_types; +mod recipe_property_sets; mod recipes; mod sound_events; mod sound_types; @@ -106,6 +107,7 @@ const MENU_TYPES: &str = "menu_types"; const TIMELINES: &str = "timelines"; const TIMELINE_TAGS: &str = "timeline_tags"; const ZOMBIE_NAUTILUS_VARIANTS: &str = "zombie_nautilus_variants"; +const RECIPE_PROPERTY_SETS: &str = "recipe_property_sets"; const RECIPES: &str = "recipes"; const VANILLA_ENTITIES: &str = "entities"; const ENTITY_DATA: &str = "entity_data"; @@ -176,6 +178,7 @@ pub fn main() { (timelines::build(), TIMELINES), (timeline_tags::build(), TIMELINE_TAGS), (zombie_nautilus_variants::build(), ZOMBIE_NAUTILUS_VARIANTS), + (recipe_property_sets::build(), RECIPE_PROPERTY_SETS), (recipes::build(), RECIPES), (entities::build(), VANILLA_ENTITIES), (entity_data::build(), ENTITY_DATA), diff --git a/steel-registry/build/recipe_property_sets.rs b/steel-registry/build/recipe_property_sets.rs new file mode 100644 index 000000000000..90342765fe51 --- /dev/null +++ b/steel-registry/build/recipe_property_sets.rs @@ -0,0 +1,52 @@ +use std::{collections::BTreeMap, fs}; + +use heck::ToSnakeCase; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +pub(crate) fn build() -> TokenStream { + println!("cargo:rerun-if-changed=build_assets/recipe_property_sets.json"); + + let recipe_property_sets_json = fs::read_to_string("build_assets/recipe_property_sets.json") + .expect("Failed to read recipe_property_sets.json"); + let recipe_property_sets_entries: BTreeMap> = + serde_json::from_str(&recipe_property_sets_json) + .expect("Failed to parse recipe_property_sets.json"); + + let mut statics = TokenStream::new(); + let mut registrations = TokenStream::new(); + + recipe_property_sets_entries + .iter() + .for_each(|(ident, list)| { + let key = Ident::new(&ident.to_snake_case().to_uppercase(), Span::call_site()); + let items = list.iter().map(|it| { + let ident = Ident::new( + it.strip_prefix("minecraft:").unwrap_or(it), + Span::call_site(), + ); + quote! { &ITEMS.#ident } + }); + + statics.extend(quote! { + pub static #key: RecipePropertySet = RecipePropertySet { + key: Identifier::vanilla_static(#ident), + items: OnceLock::new() + }; + }); + registrations + .extend(quote! { registry.register_with_items(&#key, vec![#(#items),*]); registry.register(&#key); }); + }); + + quote! { + use steel_utils::Identifier; + use crate::{items::ItemRef, recipe::{RecipePropertySet, RecipePropertySetRegistry}, vanilla_items::ITEMS}; + use std::sync::OnceLock; + + #statics + + pub fn register_recipe_property_sets(registry: &mut RecipePropertySetRegistry) { + #registrations + } + } +} diff --git a/steel-registry/build/recipes.rs b/steel-registry/build/recipes.rs index b8c85a48cab0..6624af87b5c0 100644 --- a/steel-registry/build/recipes.rs +++ b/steel-registry/build/recipes.rs @@ -32,6 +32,18 @@ struct RecipeJson { // Shapeless recipe fields #[serde(default)] ingredients: Option>, + + // Smelting recipe fields + #[serde(default)] + ingredient: Option, + + #[serde(default, rename = "cookingtime")] + cooking_time: Option, + #[serde(default)] + experience: Option, + #[serde(default)] + group: Option, + // Common fields #[serde(default)] result: Option, @@ -269,6 +281,238 @@ fn generate_ingredient_tokens(ingredient: &ParsedIngredient) -> TokenStream { } } +fn category_to_tokens(category: &str) -> TokenStream { + match category { + "building" => quote! { CraftingCategory::Building }, + "redstone" => quote! { CraftingCategory::Redstone }, + "equipment" => quote! { CraftingCategory::Equipment }, + "food" => quote! { CraftingCategory::Food }, + _ => quote! { CraftingCategory::Misc }, + } +} + +struct SmeltingRecipeData { + name: String, + ident: Ident, + category: TokenStream, + cooking_time: i32, + experience: f32, + group: Option, + ingredient: ParsedIngredient, + result: Ident, +} + +fn parse_smelting_like_recipe(recipe_name: &str, data: &RecipeJson) -> Option { + let category = data.category.as_ref()?; + + let category = category_to_tokens(category); + + let cooking_time = data.cooking_time?; + let experience = data.experience?; + let ingredient = data.ingredient.as_ref()?; + let result = data.result.as_ref()?; + let group = data.group.clone(); + + let result_item_id = result.id.strip_prefix("minecraft:").unwrap_or(&result.id); + let result_item_ident = Ident::new(result_item_id, Span::call_site()); + + let ingredient = parse_ingredient(ingredient); + + Some(SmeltingRecipeData { + name: recipe_name.to_string(), + ident: Ident::new(&recipe_name.to_snake_case(), Span::call_site()), + category, + cooking_time, + experience, + ingredient, + group, + result: result_item_ident, + }) +} + +struct StonecuttingRecipeData { + name: String, + ident: Ident, + ingredient: ParsedIngredient, + result: Ident, +} + +fn parse_stonecutting_recipe( + recipe_name: &str, + data: &RecipeJson, +) -> Option { + let ingredient = data.ingredient.as_ref()?; + let result = data.result.as_ref()?; + + let result_item_id = result.id.strip_prefix("minecraft:").unwrap_or(&result.id); + let result_item_ident = Ident::new(result_item_id, Span::call_site()); + + let ingredient = parse_ingredient(ingredient); + + Some(StonecuttingRecipeData { + name: recipe_name.to_string(), + ident: Ident::new(&recipe_name.to_snake_case(), Span::call_site()), + ingredient, + result: result_item_ident, + }) +} + +struct RecipeCodegen { + creator_fns: Vec, + fields: Vec, + field_inits: Vec, + registers: Vec, +} + +fn generate_codegen( + prefix: &str, + recipe_type: &str, + recipes: &[(Ident, TokenStream)], +) -> RecipeCodegen { + let type_ident = Ident::new(recipe_type, Span::call_site()); + + let mut cg = RecipeCodegen { + creator_fns: Vec::new(), + fields: Vec::new(), + field_inits: Vec::new(), + registers: Vec::new(), + }; + + for (ident, body) in recipes { + let fn_ident = Ident::new(&format!("create_{prefix}_{ident}"), Span::call_site()); + let register_method = Ident::new(&format!("register_{prefix}"), Span::call_site()); + let prefix_ident = Ident::new(prefix, Span::call_site()); + + cg.creator_fns.push(quote! { + #[inline(never)] + fn #fn_ident() -> #type_ident { #body } + }); + cg.fields.push(quote! { pub #ident: #type_ident, }); + cg.field_inits.push(quote! { #ident: #fn_ident(), }); + cg.registers.push(quote! { + registry.#register_method(&RECIPES.#prefix_ident.#ident); + }); + } + + cg +} + +fn generate_shaped_body(r: &ShapedRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let width = r.width; + let height = r.height; + let result_item_ident = &r.result_item_ident; + let result_count = r.result_count; + let show_notification = r.show_notification; + let symmetrical = r.symmetrical; + + let pattern_tokens: Vec = r + .pattern_data + .iter() + .map(generate_ingredient_tokens) + .collect(); + + quote! { + let pattern: &'static [Ingredient] = Box::leak( + vec![#(#pattern_tokens),*].into_boxed_slice() + ); + ShapedRecipe { + id: Identifier::vanilla_static(#name), + category: #category, + width: #width, + height: #height, + pattern, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: #result_count, + }, + show_notification: #show_notification, + symmetrical: #symmetrical, + } + } +} + +fn generate_shapeless_body(r: &ShapelessRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let result_item_ident = &r.result_item_ident; + let result_count = r.result_count; + + let ingredient_tokens: Vec = r + .ingredient_data + .iter() + .map(generate_ingredient_tokens) + .collect(); + + quote! { + + // Box::leak creates a &'static [Ingredient] from the Vec. + // This is intentional: vanilla recipes live forever. + let ingredients: &'static [Ingredient] = Box::leak( + vec![#(#ingredient_tokens),*].into_boxed_slice() + ); + ShapelessRecipe { + id: Identifier::vanilla_static(#name), + category: #category, + ingredients, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: #result_count, + }, + } + } +} + +fn generate_smelting_body(r: SmeltingRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let result_item_ident = &r.result; + + let ingredient_tokens: TokenStream = generate_ingredient_tokens(&r.ingredient); + + let cooking_time = r.cooking_time; + let experience = r.experience; + + let group = match r.group { + Some(v) => quote! { Some(#v) }, + None => quote! {None}, + }; + + quote! { + SmeltingRecipe { + ident: Identifier::vanilla_static(#name), + category: #category, + ingredient: #ingredient_tokens, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: 1, + }, + group: #group, + cooking_time: #cooking_time, + experience: #experience, + } + } +} + +fn generate_stonecutting_body(r: StonecuttingRecipeData) -> TokenStream { + let name = &r.name; + let result_item_ident = &r.result; + + let ingredient_tokens: TokenStream = generate_ingredient_tokens(&r.ingredient); + + quote! { + StonecuttingRecipe { + ident: Identifier::vanilla_static(#name), + ingredient: #ingredient_tokens, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: 1, + } + } + } +} + pub(crate) fn build() -> TokenStream { println!("cargo:rerun-if-changed=build_assets/builtin_datapacks/minecraft/recipe/"); @@ -276,19 +520,39 @@ pub(crate) fn build() -> TokenStream { let mut shaped_recipes: Vec = Vec::new(); let mut shapeless_recipes: Vec = Vec::new(); + let mut smelting_recipes: Vec = Vec::new(); + let mut campfire_recipes: Vec = Vec::new(); + let mut smoking_recipes: Vec = Vec::new(); + let mut blasting_recipes: Vec = Vec::new(); + let mut stonecutting_recipes: Vec = Vec::new(); // Read all recipe files + #[expect(clippy::too_many_arguments, reason = "its fine")] fn read_recipes( dir: &Path, shaped: &mut Vec, shapeless: &mut Vec, + smelting: &mut Vec, + campfire: &mut Vec, + smoking: &mut Vec, + blasting: &mut Vec, + stonecutting: &mut Vec, ) { for entry in fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { - read_recipes(&path, shaped, shapeless); + read_recipes( + &path, + shaped, + shapeless, + smelting, + campfire, + smoking, + blasting, + stonecutting, + ); } else if path.extension().and_then(|s| s.to_str()) == Some("json") { let recipe_name = path .file_stem() @@ -316,7 +580,32 @@ pub(crate) fn build() -> TokenStream { shapeless.push(r); } } - // Skip other recipe types for now (smelting, stonecutting, smithing, etc.) + "minecraft:smelting" => { + if let Some(r) = parse_smelting_like_recipe(recipe_name, &recipe) { + smelting.push(r); + } + } + "minecraft:campfire_cooking" => { + if let Some(r) = parse_smelting_like_recipe(recipe_name, &recipe) { + campfire.push(r); + } + } + "minecraft:smoking_cooking" => { + if let Some(r) = parse_smelting_like_recipe(recipe_name, &recipe) { + smoking.push(r); + } + } + "minecraft:blasting" => { + if let Some(r) = parse_smelting_like_recipe(recipe_name, &recipe) { + blasting.push(r); + } + } + "minecraft:stonecutting" => { + if let Some(r) = parse_stonecutting_recipe(recipe_name, &recipe) { + stonecutting.push(r); + } + } + "minecraft:smithing_trim" => {} _ => {} } } @@ -327,153 +616,126 @@ pub(crate) fn build() -> TokenStream { Path::new(recipe_dir), &mut shaped_recipes, &mut shapeless_recipes, + &mut smelting_recipes, + &mut campfire_recipes, + &mut smoking_recipes, + &mut blasting_recipes, + &mut stonecutting_recipes, ); - // Generate individual creator functions for each shaped recipe. - // Each function creates just one recipe in its own stack frame, - // preventing stack overflow from large struct literals. - // Uses Box::leak to create &'static [Ingredient] slices. - let shaped_creator_fns: Vec = shaped_recipes - .iter() - .map(|r| { - let fn_ident = Ident::new(&format!("create_shaped_{}", r.ident), Span::call_site()); - let name = &r.name; - let category = &r.category; - let width = r.width; - let height = r.height; - let result_item_ident = &r.result_item_ident; - let result_count = r.result_count; - let show_notification = r.show_notification; - let symmetrical = r.symmetrical; - - let pattern_tokens: Vec = r - .pattern_data - .iter() - .map(generate_ingredient_tokens) - .collect(); + let shaped = generate_codegen( + "shaped", + "ShapedRecipe", + &shaped_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_shaped_body(recipe))) + .collect::>(), + ); - quote! { - #[inline(never)] - fn #fn_ident() -> ShapedRecipe { - // Box::leak creates a &'static [Ingredient] from the Vec. - // This is intentional: vanilla recipes live forever. - let pattern: &'static [Ingredient] = Box::leak( - vec![#(#pattern_tokens),*].into_boxed_slice() - ); - ShapedRecipe { - id: Identifier::vanilla_static(#name), - category: #category, - width: #width, - height: #height, - pattern, - result: RecipeResult { - item: &ITEMS.#result_item_ident, - count: #result_count, - }, - show_notification: #show_notification, - symmetrical: #symmetrical, - } - } - } - }) - .collect(); + let shapeless = generate_codegen( + "shapeless", + "ShapelessRecipe", + &shapeless_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_shapeless_body(recipe))) + .collect::>(), + ); - // Generate individual creator functions for each shapeless recipe. - let shapeless_creator_fns: Vec = shapeless_recipes - .iter() - .map(|r| { - let fn_ident = Ident::new(&format!("create_shapeless_{}", r.ident), Span::call_site()); - let name = &r.name; - let category = &r.category; - let result_item_ident = &r.result_item_ident; - let result_count = r.result_count; - - let ingredient_tokens: Vec = r - .ingredient_data - .iter() - .map(generate_ingredient_tokens) - .collect(); + let smelting = generate_codegen( + "smelting", + "SmeltingRecipe", + &smelting_recipes + .into_iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - quote! { - #[inline(never)] - fn #fn_ident() -> ShapelessRecipe { - // Box::leak creates a &'static [Ingredient] from the Vec. - // This is intentional: vanilla recipes live forever. - let ingredients: &'static [Ingredient] = Box::leak( - vec![#(#ingredient_tokens),*].into_boxed_slice() - ); - ShapelessRecipe { - id: Identifier::vanilla_static(#name), - category: #category, - ingredients, - result: RecipeResult { - item: &ITEMS.#result_item_ident, - count: #result_count, - }, - } - } - } - }) - .collect(); + let campfire = generate_codegen( + "campfire", + "SmeltingRecipe", + &campfire_recipes + .into_iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - // Generate struct fields - let shaped_fields: Vec = shaped_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { pub #ident: ShapedRecipe, } - }) - .collect(); + let smoking = generate_codegen( + "smoking", + "SmeltingRecipe", + &smoking_recipes + .into_iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - let shapeless_fields: Vec = shapeless_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { pub #ident: ShapelessRecipe, } - }) - .collect(); + let blasting = generate_codegen( + "blasting", + "SmeltingRecipe", + &blasting_recipes + .into_iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - // Generate field initializers that call the creator functions - let shaped_field_inits: Vec = shaped_recipes - .iter() - .map(|r| { - let ident = &r.ident; - let fn_ident = Ident::new(&format!("create_shaped_{}", r.ident), Span::call_site()); - quote! { #ident: #fn_ident(), } - }) - .collect(); + let stonecutting = generate_codegen( + "stonecutting", + "StonecuttingRecipe", + &stonecutting_recipes + .into_iter() + .map(|recipe| (recipe.ident.clone(), generate_stonecutting_body(recipe))) + .collect::>(), + ); - let shapeless_field_inits: Vec = shapeless_recipes + let all_creator_fns: Vec<&TokenStream> = shaped + .creator_fns .iter() - .map(|r| { - let ident = &r.ident; - let fn_ident = Ident::new(&format!("create_shapeless_{}", r.ident), Span::call_site()); - quote! { #ident: #fn_ident(), } - }) + .chain(shapeless.creator_fns.iter()) + .chain(smelting.creator_fns.iter()) + .chain(campfire.creator_fns.iter()) + .chain(smoking.creator_fns.iter()) + .chain(blasting.creator_fns.iter()) + .chain(stonecutting.creator_fns.iter()) .collect(); - // Generate registration calls - let shaped_registers: Vec = shaped_recipes + let all_registers: Vec<&TokenStream> = shaped + .registers .iter() - .map(|r| { - let ident = &r.ident; - quote! { registry.register_shaped(&RECIPES.shaped.#ident); } - }) + .chain(shapeless.registers.iter()) + .chain(smelting.registers.iter()) + .chain(campfire.registers.iter()) + .chain(smoking.registers.iter()) + .chain(blasting.registers.iter()) + .chain(stonecutting.registers.iter()) .collect(); - let shapeless_registers: Vec = shapeless_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { registry.register_shapeless(&RECIPES.shapeless.#ident); } - }) - .collect(); + let shaped_fields = &shaped.fields; + let shaped_field_inits = &shaped.field_inits; + + let shapeless_fields = &shapeless.fields; + let shapeless_field_inits = &shapeless.field_inits; + + let smelting_fields = &smelting.fields; + let smelting_field_inits = &smelting.field_inits; + + let campfire_fields = &campfire.fields; + let campfire_field_inits = &campfire.field_inits; + + let smoking_fields = &smoking.fields; + let smoking_field_inits = &smoking.field_inits; + + let blasting_fields = &blasting.fields; + let blasting_field_inits = &blasting.field_inits; + + let stonecutting_fields = &stonecutting.fields; + let stonecutting_field_inits = &stonecutting.field_inits; quote! { + #![allow(clippy)] + #![allow(unknown_lints)] use crate::{ recipe::{ CraftingCategory, Ingredient, RecipeRegistry, RecipeResult, - ShapedRecipe, ShapelessRecipe, + ShapedRecipe, ShapelessRecipe, SmeltingRecipe, StonecuttingRecipe }, vanilla_items::ITEMS, }; @@ -487,33 +749,24 @@ pub(crate) fn build() -> TokenStream { /// `&'static` slices, providing zero-cost access after initialization. pub static RECIPES: LazyLock = LazyLock::new(Recipes::init); - pub struct ShapedRecipes { - #(#shaped_fields)* - } - - pub struct ShapelessRecipes { - #(#shapeless_fields)* - } + pub struct ShapedRecipes { #(#shaped_fields)* } + pub struct ShapelessRecipes { #(#shapeless_fields)* } + pub struct SmeltingRecipes { #(#smelting_fields)* } + pub struct CampfireRecipes { #(#campfire_fields)* } + pub struct SmokingRecipes { #(#smoking_fields)* } + pub struct BlastingRecipes { #(#blasting_fields)* } + pub struct StonecuttingRecipes { #(#stonecutting_fields)* } pub struct Recipes { pub shaped: ShapedRecipes, pub shapeless: ShapelessRecipes, + pub smelting: SmeltingRecipes, + pub campfire: CampfireRecipes, + pub smoking: SmokingRecipes, + pub blasting: BlastingRecipes, + pub stonecutting: StonecuttingRecipes, } - // Individual recipe creator functions. - // - // Each function is marked `#[inline(never)]` to ensure it gets its own - // stack frame. This prevents stack overflow that would occur if all - // recipes were initialized in a single large struct literal. - // - // Each function uses `Box::leak` to convert the ingredient Vec into - // a `&'static [Ingredient]`. This is intentional and correct: - // - Vanilla recipes live for the entire program lifetime - // - The leaked memory is a one-time cost at startup - // - Access to recipe data after init is zero-cost (just pointer + length) - #(#shaped_creator_fns)* - #(#shapeless_creator_fns)* - impl Recipes { fn init() -> Self { Self { @@ -523,16 +776,32 @@ pub(crate) fn build() -> TokenStream { shapeless: ShapelessRecipes { #(#shapeless_field_inits)* }, + smelting: SmeltingRecipes { + #(#smelting_field_inits)* + }, + campfire: CampfireRecipes { + #(#campfire_field_inits)* + }, + smoking: SmokingRecipes { + #(#smoking_field_inits)* + }, + blasting: BlastingRecipes { + #(#blasting_field_inits)* + }, + stonecutting: StonecuttingRecipes { + #(#stonecutting_field_inits)* + }, } } } + #(#all_creator_fns)* + /// Registers all vanilla recipes with the recipe registry. pub fn register_recipes(registry: &mut RecipeRegistry) { // Force initialization of RECIPES let _ = &*RECIPES; - #(#shaped_registers)* - #(#shapeless_registers)* + #(#all_registers)* } } } diff --git a/steel-registry/build_assets/recipe_property_sets.json b/steel-registry/build_assets/recipe_property_sets.json new file mode 100644 index 000000000000..641b2824f73b --- /dev/null +++ b/steel-registry/build_assets/recipe_property_sets.json @@ -0,0 +1,319 @@ +{ + "campfire_input": [ + "minecraft:beef", + "minecraft:kelp", + "minecraft:salmon", + "minecraft:chicken", + "minecraft:cod", + "minecraft:rabbit", + "minecraft:potato", + "minecraft:porkchop", + "minecraft:mutton" + ], + "blast_furnace_input": [ + "minecraft:iron_hoe", + "minecraft:golden_leggings", + "minecraft:golden_sword", + "minecraft:golden_spear", + "minecraft:golden_shovel", + "minecraft:deepslate_diamond_ore", + "minecraft:chainmail_chestplate", + "minecraft:copper_axe", + "minecraft:deepslate_coal_ore", + "minecraft:copper_sword", + "minecraft:golden_nautilus_armor", + "minecraft:copper_ore", + "minecraft:ancient_debris", + "minecraft:deepslate_lapis_ore", + "minecraft:iron_shovel", + "minecraft:raw_copper", + "minecraft:deepslate_iron_ore", + "minecraft:golden_chestplate", + "minecraft:golden_hoe", + "minecraft:copper_leggings", + "minecraft:iron_pickaxe", + "minecraft:golden_pickaxe", + "minecraft:lapis_ore", + "minecraft:chainmail_boots", + "minecraft:iron_axe", + "minecraft:copper_horse_armor", + "minecraft:raw_iron", + "minecraft:iron_spear", + "minecraft:redstone_ore", + "minecraft:copper_boots", + "minecraft:iron_chestplate", + "minecraft:nether_gold_ore", + "minecraft:iron_nautilus_armor", + "minecraft:copper_chestplate", + "minecraft:copper_nautilus_armor", + "minecraft:copper_hoe", + "minecraft:golden_helmet", + "minecraft:deepslate_redstone_ore", + "minecraft:iron_boots", + "minecraft:iron_sword", + "minecraft:deepslate_copper_ore", + "minecraft:raw_gold", + "minecraft:golden_boots", + "minecraft:copper_helmet", + "minecraft:coal_ore", + "minecraft:deepslate_emerald_ore", + "minecraft:golden_horse_armor", + "minecraft:iron_leggings", + "minecraft:nether_quartz_ore", + "minecraft:chainmail_leggings", + "minecraft:golden_axe", + "minecraft:emerald_ore", + "minecraft:copper_pickaxe", + "minecraft:copper_shovel", + "minecraft:copper_spear", + "minecraft:iron_horse_armor", + "minecraft:iron_ore", + "minecraft:chainmail_helmet", + "minecraft:diamond_ore", + "minecraft:deepslate_gold_ore", + "minecraft:iron_helmet", + "minecraft:gold_ore" + ], + "furnace_input": [ + "minecraft:deepslate_iron_ore", + "minecraft:birch_log", + "minecraft:cactus", + "minecraft:cobblestone", + "minecraft:basalt", + "minecraft:chainmail_helmet", + "minecraft:mangrove_wood", + "minecraft:chorus_fruit", + "minecraft:raw_iron", + "minecraft:golden_chestplate", + "minecraft:mutton", + "minecraft:deepslate_gold_ore", + "minecraft:stripped_acacia_wood", + "minecraft:copper_boots", + "minecraft:stripped_acacia_log", + "minecraft:flowering_azalea_leaves", + "minecraft:pale_oak_wood", + "minecraft:stripped_jungle_wood", + "minecraft:stripped_spruce_wood", + "minecraft:deepslate_coal_ore", + "minecraft:cherry_leaves", + "minecraft:polished_blackstone_bricks", + "minecraft:stripped_pale_oak_wood", + "minecraft:golden_horse_armor", + "minecraft:jungle_log", + "minecraft:copper_chestplate", + "minecraft:oak_leaves", + "minecraft:copper_hoe", + "minecraft:spruce_leaves", + "minecraft:copper_ore", + "minecraft:pink_terracotta", + "minecraft:stone_bricks", + "minecraft:iron_shovel", + "minecraft:stripped_birch_wood", + "minecraft:iron_spear", + "minecraft:acacia_wood", + "minecraft:pale_oak_leaves", + "minecraft:iron_helmet", + "minecraft:acacia_log", + "minecraft:oak_wood", + "minecraft:cod", + "minecraft:emerald_ore", + "minecraft:copper_spear", + "minecraft:golden_pickaxe", + "minecraft:stripped_spruce_log", + "minecraft:dark_oak_wood", + "minecraft:stripped_oak_wood", + "minecraft:kelp", + "minecraft:green_terracotta", + "minecraft:potato", + "minecraft:deepslate_lapis_ore", + "minecraft:light_gray_terracotta", + "minecraft:iron_pickaxe", + "minecraft:diamond_ore", + "minecraft:lapis_ore", + "minecraft:stripped_mangrove_log", + "minecraft:stripped_dark_oak_wood", + "minecraft:deepslate_bricks", + "minecraft:yellow_terracotta", + "minecraft:nether_bricks", + "minecraft:cherry_log", + "minecraft:nether_quartz_ore", + "minecraft:copper_leggings", + "minecraft:clay_ball", + "minecraft:orange_terracotta", + "minecraft:iron_sword", + "minecraft:beef", + "minecraft:deepslate_emerald_ore", + "minecraft:golden_spear", + "minecraft:dark_oak_log", + "minecraft:quartz_block", + "minecraft:stripped_oak_log", + "minecraft:copper_pickaxe", + "minecraft:dark_oak_leaves", + "minecraft:white_terracotta", + "minecraft:copper_helmet", + "minecraft:chainmail_boots", + "minecraft:raw_copper", + "minecraft:pale_oak_log", + "minecraft:spruce_wood", + "minecraft:brown_terracotta", + "minecraft:copper_shovel", + "minecraft:copper_sword", + "minecraft:cyan_terracotta", + "minecraft:netherrack", + "minecraft:iron_horse_armor", + "minecraft:sea_pickle", + "minecraft:golden_nautilus_armor", + "minecraft:mangrove_leaves", + "minecraft:sand", + "minecraft:porkchop", + "minecraft:coal_ore", + "minecraft:iron_axe", + "minecraft:iron_chestplate", + "minecraft:stone", + "minecraft:lime_terracotta", + "minecraft:birch_wood", + "minecraft:golden_boots", + "minecraft:rabbit", + "minecraft:deepslate_tiles", + "minecraft:stripped_pale_oak_log", + "minecraft:copper_horse_armor", + "minecraft:acacia_leaves", + "minecraft:wet_sponge", + "minecraft:stripped_jungle_log", + "minecraft:blue_terracotta", + "minecraft:deepslate_redstone_ore", + "minecraft:birch_leaves", + "minecraft:clay", + "minecraft:golden_axe", + "minecraft:red_sand", + "minecraft:purple_terracotta", + "minecraft:golden_helmet", + "minecraft:redstone_ore", + "minecraft:magenta_terracotta", + "minecraft:jungle_wood", + "minecraft:chainmail_leggings", + "minecraft:salmon", + "minecraft:stripped_cherry_wood", + "minecraft:nether_gold_ore", + "minecraft:spruce_log", + "minecraft:iron_boots", + "minecraft:black_terracotta", + "minecraft:deepslate_diamond_ore", + "minecraft:raw_gold", + "minecraft:iron_ore", + "minecraft:sandstone", + "minecraft:cherry_wood", + "minecraft:light_blue_terracotta", + "minecraft:copper_nautilus_armor", + "minecraft:mangrove_log", + "minecraft:stripped_mangrove_wood", + "minecraft:red_sandstone", + "minecraft:golden_shovel", + "minecraft:ancient_debris", + "minecraft:golden_sword", + "minecraft:red_terracotta", + "minecraft:iron_hoe", + "minecraft:gray_terracotta", + "minecraft:stripped_dark_oak_log", + "minecraft:resin_clump", + "minecraft:azalea_leaves", + "minecraft:stripped_cherry_log", + "minecraft:gold_ore", + "minecraft:oak_log", + "minecraft:iron_nautilus_armor", + "minecraft:golden_hoe", + "minecraft:cobbled_deepslate", + "minecraft:iron_leggings", + "minecraft:jungle_leaves", + "minecraft:deepslate_copper_ore", + "minecraft:stripped_birch_log", + "minecraft:golden_leggings", + "minecraft:copper_axe", + "minecraft:chainmail_chestplate", + "minecraft:chicken" + ], + "smoker_input": [ + "minecraft:beef", + "minecraft:kelp", + "minecraft:salmon", + "minecraft:chicken", + "minecraft:cod", + "minecraft:rabbit", + "minecraft:potato", + "minecraft:porkchop", + "minecraft:mutton" + ], + "smithing_addition": [ + "minecraft:copper_ingot", + "minecraft:netherite_ingot", + "minecraft:quartz", + "minecraft:emerald", + "minecraft:redstone", + "minecraft:lapis_lazuli", + "minecraft:resin_brick", + "minecraft:amethyst_shard", + "minecraft:iron_ingot", + "minecraft:diamond", + "minecraft:gold_ingot" + ], + "smithing_template": [ + "minecraft:eye_armor_trim_smithing_template", + "minecraft:sentry_armor_trim_smithing_template", + "minecraft:wild_armor_trim_smithing_template", + "minecraft:rib_armor_trim_smithing_template", + "minecraft:bolt_armor_trim_smithing_template", + "minecraft:wayfinder_armor_trim_smithing_template", + "minecraft:flow_armor_trim_smithing_template", + "minecraft:snout_armor_trim_smithing_template", + "minecraft:vex_armor_trim_smithing_template", + "minecraft:spire_armor_trim_smithing_template", + "minecraft:ward_armor_trim_smithing_template", + "minecraft:coast_armor_trim_smithing_template", + "minecraft:raiser_armor_trim_smithing_template", + "minecraft:netherite_upgrade_smithing_template", + "minecraft:tide_armor_trim_smithing_template", + "minecraft:host_armor_trim_smithing_template", + "minecraft:dune_armor_trim_smithing_template", + "minecraft:shaper_armor_trim_smithing_template", + "minecraft:silence_armor_trim_smithing_template" + ], + "smithing_base": [ + "minecraft:copper_boots", + "minecraft:golden_leggings", + "minecraft:copper_leggings", + "minecraft:copper_helmet", + "minecraft:diamond_pickaxe", + "minecraft:netherite_helmet", + "minecraft:diamond_chestplate", + "minecraft:leather_leggings", + "minecraft:diamond_nautilus_armor", + "minecraft:chainmail_helmet", + "minecraft:diamond_horse_armor", + "minecraft:diamond_spear", + "minecraft:iron_boots", + "minecraft:iron_helmet", + "minecraft:diamond_hoe", + "minecraft:diamond_shovel", + "minecraft:golden_boots", + "minecraft:golden_helmet", + "minecraft:chainmail_leggings", + "minecraft:diamond_boots", + "minecraft:netherite_boots", + "minecraft:iron_chestplate", + "minecraft:diamond_sword", + "minecraft:leather_boots", + "minecraft:netherite_leggings", + "minecraft:iron_leggings", + "minecraft:diamond_helmet", + "minecraft:golden_chestplate", + "minecraft:diamond_leggings", + "minecraft:copper_chestplate", + "minecraft:leather_chestplate", + "minecraft:leather_helmet", + "minecraft:chainmail_boots", + "minecraft:turtle_helmet", + "minecraft:netherite_chestplate", + "minecraft:chainmail_chestplate", + "minecraft:diamond_axe" + ] +} diff --git a/steel-registry/src/lib.rs b/steel-registry/src/lib.rs index 8b2bcd696fd2..aca6531f72cb 100644 --- a/steel-registry/src/lib.rs +++ b/steel-registry/src/lib.rs @@ -1,5 +1,6 @@ #![feature(const_trait_impl, const_cmp, derive_const)] +use crate::recipe::RecipePropertySetRegistry; use crate::world_clock::WorldClockRegistry; use crate::{ attribute::AttributeRegistry, @@ -261,6 +262,11 @@ pub mod vanilla_timeline_tags; #[path = "generated/vanilla_recipes.rs"] pub mod vanilla_recipes; +#[expect(warnings)] +#[rustfmt::skip] +#[path = "generated/vanilla_recipe_property_sets.rs"] +pub mod vanilla_recipe_property_sets; + #[expect(warnings)] #[rustfmt::skip] #[path = "generated/vanilla_entities.rs"] @@ -527,6 +533,7 @@ pub struct Registry { pub poi_types: PoiTypeRegistry, pub enchantments: EnchantmentRegistry, pub world_clocks: WorldClockRegistry, + pub recipe_property_sets: RecipePropertySetRegistry, pub configured_carvers: ConfiguredCarverRegistry, pub structures: StructureRegistry, } @@ -617,6 +624,10 @@ impl Registry { vanilla_configured_carvers::register_configured_carvers(&mut registry.configured_carvers); + vanilla_recipe_property_sets::register_recipe_property_sets( + &mut registry.recipe_property_sets, + ); + registry } @@ -662,6 +673,7 @@ impl Registry { self.poi_types.freeze(); self.enchantments.freeze(); self.world_clocks.freeze(); + self.recipe_property_sets.freeze(); self.configured_carvers.freeze(); self.structures.freeze(); } @@ -721,6 +733,7 @@ impl Registry { world_clocks: WorldClockRegistry::new(), poi_types: PoiTypeRegistry::new(), enchantments: EnchantmentRegistry::new(), + recipe_property_sets: RecipePropertySetRegistry::new(), configured_carvers: ConfiguredCarverRegistry::new(), structures: StructureRegistry::new(), } diff --git a/steel-registry/src/recipe/crafting.rs b/steel-registry/src/recipe/crafting.rs index 3adf32bad682..c8ecb3bcf687 100644 --- a/steel-registry/src/recipe/crafting.rs +++ b/steel-registry/src/recipe/crafting.rs @@ -2,9 +2,12 @@ use steel_utils::Identifier; -use crate::{item_stack::ItemStack, items::ItemRef}; - use super::ingredient::Ingredient; +use crate::{ + item_stack::ItemStack, + items::ItemRef, + recipe::{SmeltingRecipe, StonecuttingRecipe}, +}; /// Category for crafting recipes (used by recipe book). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -12,6 +15,7 @@ pub enum CraftingCategory { Building, Redstone, Equipment, + Food, Misc, } @@ -250,6 +254,8 @@ impl ShapelessRecipe { pub enum CraftingRecipe { Shaped(&'static ShapedRecipe), Shapeless(&'static ShapelessRecipe), + Smelting(&'static SmeltingRecipe), + Stonecutting(&'static StonecuttingRecipe), } impl CraftingRecipe { @@ -259,15 +265,19 @@ impl CraftingRecipe { match self { Self::Shaped(r) => &r.id, Self::Shapeless(r) => &r.id, + Self::Smelting(r) => &r.ident, + Self::Stonecutting(r) => &r.ident, } } /// Returns the recipe category. #[must_use] - pub fn category(&self) -> CraftingCategory { + pub fn category(&self) -> Option { match self { - Self::Shaped(r) => r.category, - Self::Shapeless(r) => r.category, + Self::Shaped(r) => Some(r.category), + Self::Shapeless(r) => Some(r.category), + Self::Smelting(r) => Some(r.category), + Self::Stonecutting(_r) => None, } } @@ -277,6 +287,8 @@ impl CraftingRecipe { match self { Self::Shaped(r) => &r.result, Self::Shapeless(r) => &r.result, + Self::Smelting(r) => &r.result, + Self::Stonecutting(r) => &r.result, } } @@ -287,6 +299,8 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.matches(input), Self::Shapeless(r) => r.matches(input), + Self::Smelting(r) => r.matches(input), + Self::Stonecutting(r) => r.matches(input), } } @@ -296,6 +310,8 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.assemble(), Self::Shapeless(r) => r.assemble(), + Self::Smelting(r) => r.assemble(), + Self::Stonecutting(r) => r.assemble(), } } @@ -305,6 +321,8 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.get_remaining_items(input), Self::Shapeless(r) => r.get_remaining_items(input), + Self::Smelting(_r) => vec![], + Self::Stonecutting(_r) => vec![], } } @@ -314,6 +332,8 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.fits_in_2x2(), Self::Shapeless(r) => r.fits_in_2x2(), + Self::Smelting(_r) => false, + Self::Stonecutting(_r) => false, } } } diff --git a/steel-registry/src/recipe/mod.rs b/steel-registry/src/recipe/mod.rs index b425a628e0a4..2838799eb987 100644 --- a/steel-registry/src/recipe/mod.rs +++ b/steel-registry/src/recipe/mod.rs @@ -5,11 +5,19 @@ mod crafting; mod ingredient; +pub mod recipe_property_sets; mod registry; +mod smelting; +mod stonecutting; pub use crafting::{ CraftingCategory, CraftingInput, CraftingRecipe, PositionedCraftingInput, RecipeResult, ShapedRecipe, ShapelessRecipe, }; pub use ingredient::Ingredient; +pub use recipe_property_sets::{ + RecipePropertySet, RecipePropertySetRef, RecipePropertySetRegistry, +}; pub use registry::RecipeRegistry; +pub use smelting::SmeltingRecipe; +pub use stonecutting::StonecuttingRecipe; diff --git a/steel-registry/src/recipe/recipe_property_sets.rs b/steel-registry/src/recipe/recipe_property_sets.rs new file mode 100644 index 000000000000..d9ff1cde5bda --- /dev/null +++ b/steel-registry/src/recipe/recipe_property_sets.rs @@ -0,0 +1,106 @@ +use std::sync::OnceLock; + +use rustc_hash::FxHashMap; +use steel_utils::Identifier; + +use crate::items::ItemRef; + +pub struct RecipePropertySet { + pub key: Identifier, + pub items: OnceLock>, +} + +pub type RecipePropertySetRef = &'static RecipePropertySet; + +pub struct RecipePropertySetRegistry { + pending_sets: FxHashMap>, + + recipe_property_set_by_id: Vec, + recipe_property_set_by_key: FxHashMap, + allows_registering: bool, +} + +impl RecipePropertySetRegistry { + pub fn new() -> Self { + Self { + pending_sets: FxHashMap::default(), + recipe_property_set_by_id: Vec::new(), + recipe_property_set_by_key: FxHashMap::default(), + allows_registering: true, + } + } + + pub fn register_with_items(&mut self, set: &'static RecipePropertySet, items: Vec) { + self.pending_sets.insert(set.key.clone(), items); + } + + pub fn add_items(&mut self, key: &Identifier, items: impl IntoIterator) { + if !self.allows_registering { + panic!( + "Registering new items to RecipePropertySets isn't allowed after `freeze()` has been called on the registry." + ) + } + + if let Some(entry) = self.pending_sets.get_mut(key) { + entry.extend(items); + } else { + self.pending_sets + .entry(key.to_owned()) + .or_default() + .extend(items); + } + } +} + +impl crate::RegistryExt for RecipePropertySetRegistry { + type Entry = RecipePropertySet; + + fn freeze(&mut self) { + if !self.allows_registering { + return; + } + for (key, value) in self.pending_sets.drain() { + if let Some(set) = self + .recipe_property_set_by_key + .get(&key) + .and_then(|&id| self.recipe_property_set_by_id.get(id)) + { + set.items.set(value).unwrap_or_else(|_| { + log::error!("Failed to put items into `RecipePropertySet` - `{:?}` because the registry isn't frozen, but the `OnceLock` somehow already was initialized.", key) + }); + } + } + self.allows_registering = false; + } + + fn by_id(&self, id: usize) -> Option<&'static RecipePropertySet> { + self.recipe_property_set_by_id.get(id).copied() + } + + fn by_key(&self, key: &Identifier) -> Option<&'static RecipePropertySet> { + self.recipe_property_set_by_key + .get(key) + .and_then(|&id| self.recipe_property_set_by_id.get(id).copied()) + } + + fn id_from_key(&self, key: &Identifier) -> Option { + self.recipe_property_set_by_key.get(key).copied() + } + + fn len(&self) -> usize { + self.recipe_property_set_by_id.len() + } + fn is_empty(&self) -> bool { + self.recipe_property_set_by_id.is_empty() + } +} + +crate::impl_standard_methods!( + RecipePropertySetRegistry, + RecipePropertySetRef, + recipe_property_set_by_id, + recipe_property_set_by_key, + allows_registering +); + +crate::impl_registry_entry!(RecipePropertySet, recipe_property_sets); diff --git a/steel-registry/src/recipe/registry.rs b/steel-registry/src/recipe/registry.rs index 838acddef3f0..cda55677cfbb 100644 --- a/steel-registry/src/recipe/registry.rs +++ b/steel-registry/src/recipe/registry.rs @@ -3,6 +3,8 @@ use rustc_hash::FxHashMap; use steel_utils::Identifier; +use crate::recipe::{SmeltingRecipe, StonecuttingRecipe}; + use super::crafting::{CraftingInput, CraftingRecipe, ShapedRecipe, ShapelessRecipe}; /// Registry for all recipes. @@ -15,6 +17,16 @@ pub struct RecipeRegistry { shaped_recipes: Vec<&'static ShapedRecipe>, /// All shapeless crafting recipes (for type-specific iteration). shapeless_recipes: Vec<&'static ShapelessRecipe>, + /// All smelting recipes (for type-specific iteration). + smelting_recipes: Vec<&'static SmeltingRecipe>, + /// All campfire recipes (for type-specific iteration). + campfire_recipes: Vec<&'static SmeltingRecipe>, + /// All smoking recipes (for type-specific iteration). + smoking_recipes: Vec<&'static SmeltingRecipe>, + /// All blasting recipes (for type-specific iteration). + blasting_recipes: Vec<&'static SmeltingRecipe>, + /// All stonecutting recipes (for type-specific iteration). + stonecutting_recipes: Vec<&'static StonecuttingRecipe>, /// Whether registration is still allowed. allows_registering: bool, } @@ -35,6 +47,11 @@ impl RecipeRegistry { shaped_recipes: Vec::new(), shapeless_recipes: Vec::new(), allows_registering: true, + smelting_recipes: Vec::new(), + campfire_recipes: Vec::new(), + smoking_recipes: Vec::new(), + blasting_recipes: Vec::new(), + stonecutting_recipes: Vec::new(), } } @@ -64,6 +81,66 @@ impl RecipeRegistry { self.shapeless_recipes.push(recipe); } + pub fn register_smelting(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.smelting_recipes.push(recipe); + } + + pub fn register_campfire(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.campfire_recipes.push(recipe); + } + + pub fn register_smoking(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.smoking_recipes.push(recipe); + } + + pub fn register_blasting(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.blasting_recipes.push(recipe); + } + + pub fn register_stonecutting(&mut self, recipe: &'static StonecuttingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Stonecutting(recipe)))); + self.stonecutting_recipes.push(recipe); + } + /// Finds a matching crafting recipe for the given positioned input. /// Returns the first matching recipe, or None if no recipe matches. #[must_use] diff --git a/steel-registry/src/recipe/smelting.rs b/steel-registry/src/recipe/smelting.rs new file mode 100644 index 000000000000..9dbb49ca26b4 --- /dev/null +++ b/steel-registry/src/recipe/smelting.rs @@ -0,0 +1,27 @@ +use steel_utils::Identifier; + +use crate::{ + item_stack::ItemStack, + recipe::{CraftingCategory, CraftingInput, Ingredient, RecipeResult}, +}; + +#[derive(Debug, Clone)] +pub struct SmeltingRecipe { + pub ident: Identifier, + pub category: CraftingCategory, + pub result: RecipeResult, + pub ingredient: Ingredient, + pub group: Option<&'static str>, + pub cooking_time: i32, + pub experience: f32, +} + +impl SmeltingRecipe { + pub fn matches(&self, input: &CraftingInput) -> bool { + self.ingredient.test(input.get(0, 0)) + } + + pub fn assemble(&self) -> ItemStack { + self.result.to_item_stack() + } +} diff --git a/steel-registry/src/recipe/stonecutting.rs b/steel-registry/src/recipe/stonecutting.rs new file mode 100644 index 000000000000..d3efea66063a --- /dev/null +++ b/steel-registry/src/recipe/stonecutting.rs @@ -0,0 +1,23 @@ +use steel_utils::Identifier; + +use crate::{ + item_stack::ItemStack, + recipe::{CraftingInput, Ingredient, RecipeResult}, +}; + +#[derive(Debug, Clone)] +pub struct StonecuttingRecipe { + pub ident: Identifier, + pub result: RecipeResult, + pub ingredient: Ingredient, +} + +impl StonecuttingRecipe { + pub fn matches(&self, input: &CraftingInput) -> bool { + self.ingredient.test(input.get(0, 0)) + } + + pub fn assemble(&self) -> ItemStack { + self.result.to_item_stack() + } +}