From dda28d5ecbf59844fa8b891b01ec6ae976baf0b5 Mon Sep 17 00:00:00 2001 From: ChampionAsh5357 Date: Thu, 24 Oct 2024 13:15:57 -0400 Subject: [PATCH] feat(docs): Update recipes section --- docs/resources/server/recipes/builtin.md | 178 ++++--- docs/resources/server/recipes/index.md | 524 +++++++++++++++---- docs/resources/server/recipes/ingredients.md | 66 +-- 3 files changed, 557 insertions(+), 211 deletions(-) diff --git a/docs/resources/server/recipes/builtin.md b/docs/resources/server/recipes/builtin.md index a3d9d91e..b67d15ac 100644 --- a/docs/resources/server/recipes/builtin.md +++ b/docs/resources/server/recipes/builtin.md @@ -14,19 +14,15 @@ Some of the most important recipes - such as the crafting table, sticks, or most { "type": "minecraft:crafting_shaped", "category": "equipment", + "key": { + "#": "minecraft:stick", + "X": "minecraft:iron_ingot" + }, "pattern": [ "XXX", " # ", " # " ], - "key": { - "#": { - "item": "minecraft:stick" - }, - "X": { - "item": "minecraft:iron_ingot" - } - }, "result": { "count": 1, "id": "minecraft:iron_pickaxe" @@ -37,20 +33,21 @@ Some of the most important recipes - such as the crafting table, sticks, or most Let's digest this line for line: - `type`: This is the id of the shaped recipe serializer, `minecraft:crafting_shaped`. -- `category`: This optional field defines the category in the crafting book. +- `category`: This optional field defines the `CraftingBookCategory` in the crafting book. - `key` and `pattern`: Together, these define how the items must be put into the crafting grid. - The pattern defines up to three lines of up to three-wide strings that define the shape. All lines must be the same length, i.e. the pattern must form a rectangular shape. Spaces can be used to denote slots that should stay empty. - The key associates the characters used in the pattern with [ingredients][ingredient]. In the above example, all `X`s in the pattern must be iron ingots, and all `#`s must be sticks. - `result`: The result of the recipe. This is [an item stack's JSON representation][itemjson]. - Not shown in the example is the `group` key. This optional string property creates a group in the recipe book. Recipes in the same group will be displayed as one in the recipe book. +- Not shown in the example is `show_notification`. This optional boolean, when false, disables the toast shown on the top right hand corner on first use or unlock. -And then, let's have a look at how you'd generate this recipe: +And then, let's have a look at how you'd generate this recipe within `RecipeProvider#buildRecipes`: ```java // We use a builder pattern, therefore no variable is created. Create a new builder by calling // ShapedRecipeBuilder#shaped with the recipe category (found in the RecipeCategory enum) // and a result item, a result item and count, or a result item stack. -ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, Items.IRON_PICKAXE) +ShapedRecipeBuilder.shaped(this.registries.lookupOrThrow(Registries.ITEM), RecipeCategory.TOOLS, Items.IRON_PICKAXE) // Create the lines of your pattern. Each call to #pattern adds a new line. // Patterns will be validated, i.e. their shape will be checked. .pattern("XXX") @@ -64,13 +61,13 @@ ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, Items.IRON_PICKAXE) // the recipe builder will crash if you omit this. The first parameter is the advancement name, // and the second one is the condition. Normally, you want to use the has() shortcut for the condition. // Multiple advancement requirements can be added by calling #unlockedBy multiple times. - .unlockedBy("has_iron_ingot", has(Items.IRON_INGOT)) + .unlockedBy("has_iron_ingot", this.has(Items.IRON_INGOT)) // Stores the recipe in the passed RecipeOutput, to be written to disk. // If you want to add conditions to the recipe, those can be set on the output. - .save(output); + .save(this.output); ``` -Additionally, you can call `#group` to set the recipe book group. +Additionally, you can call `#group` and `#showNotification` to set the recipe book group and toggle the toast pop-up, respectively. ### Shapeless Crafting @@ -81,15 +78,9 @@ Unlike shaped crafting recipes, shapeless crafting recipes do not care about the "type": "minecraft:crafting_shapeless", "category": "misc", "ingredients": [ - { - "item": "minecraft:brown_mushroom" - }, - { - "item": "minecraft:red_mushroom" - }, - { - "item": "minecraft:bowl" - } + "minecraft:brown_mushroom", + "minecraft:red_mushroom", + "minecraft:bowl" ], "result": { "count": 1, @@ -106,13 +97,13 @@ Like before, let's digest this line for line: - `result`: The result of the recipe. This is [an item stack's JSON representation][itemjson]. - Not shown in the example is the `group` key. This optional string property creates a group in the recipe book. Recipes in the same group will be displayed as one in the recipe book. -And then, let's have a look at how you'd generate this recipe: +And then, let's have a look at how you'd generate this recipe in `RecipeProvider#buildRecipes`: ```java // We use a builder pattern, therefore no variable is created. Create a new builder by calling // ShapelessRecipeBuilder#shapeless with the recipe category (found in the RecipeCategory enum) // and a result item, a result item and count, or a result item stack. -ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.MUSHROOM_STEW) +ShapelessRecipeBuilder.shapeless(this.registries.lookupOrThrow(Registries.ITEM), RecipeCategory.MISC, Items.MUSHROOM_STEW) // Add the recipe ingredients. This can either accept Ingredients, TagKeys or ItemLikes. // Overloads also exist that additionally accept a count, adding the same ingredient multiple times. .requires(Blocks.BROWN_MUSHROOM) @@ -122,13 +113,13 @@ ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.MUSHROOM_STEW) // the recipe builder will crash if you omit this. The first parameter is the advancement name, // and the second one is the condition. Normally, you want to use the has() shortcut for the condition. // Multiple advancement requirements can be added by calling #unlockedBy multiple times. - .unlockedBy("has_mushroom_stew", has(Items.MUSHROOM_STEW)) - .unlockedBy("has_bowl", has(Items.BOWL)) - .unlockedBy("has_brown_mushroom", has(Blocks.BROWN_MUSHROOM)) - .unlockedBy("has_red_mushroom", has(Blocks.RED_MUSHROOM)) + .unlockedBy("has_mushroom_stew", this.has(Items.MUSHROOM_STEW)) + .unlockedBy("has_bowl", this.has(Items.BOWL)) + .unlockedBy("has_brown_mushroom", this.has(Blocks.BROWN_MUSHROOM)) + .unlockedBy("has_red_mushroom", this.has(Blocks.RED_MUSHROOM)) // Stores the recipe in the passed RecipeOutput, to be written to disk. // If you want to add conditions to the recipe, those can be set on the output. - .save(output); + .save(this.output); ``` Additionally, you can call `#group` to set the recipe book group. @@ -137,26 +128,70 @@ Additionally, you can call `#group` to set the recipe book group. One-item recipes (e.g. storage blocks unpacking) should be shapeless recipes to follow vanilla standards. ::: -### Special Crafting +### Transmute Crafting + +Transmute recipes are a special type of single item crafting recipes where the input stack's data components are completely copied to the resulting stack. Transmutations usually occur between two different items where one is the dyed version of another. For example: + +```json5 +{ + "type": "minecraft:crafting_transmute", + "category": "misc", + "group": "shulker_box_dye", + "input": "#minecraft:shulker_boxes", + "material": "minecraft:blue_dye", + "result": "minecraft:blue_shulker_box" +} +``` -In some cases, outputs must be created dynamically from inputs. Most of the time, this is to set data components on the output by copying or calculating their values from the input stacks. These recipes usually only specify the type and hardcode everything else. For example: +Like before, let's digest this line for line: + +- `type`: This is the id of the shapeless recipe serializer, `minecraft:crafting_transmute`. +- `category`: This optional field defines the category in the crafting book. +- `group`: This optional string property creates a group in the recipe book. Recipes in the same group will be displayed as one in the recipe book, which typically makes sense for transmuted recipes. +- `input`: The [ingredient] to transmute. +- `material`: The [ingredient] that transforms the stack into its result. +- `result`: The result of the recipe. This is an item, as the stack would be constructed given the components of the input. + +And then, let's have a look at how you'd generate this recipe in `RecipeProvider#buildRecipes`: ```java +// We use a builder pattern, therefore no variable is created. Create a new builder by calling +// TransmuteRecipeBuilder#transmute with the recipe category (found in the RecipeCategory enum), +// the ingredient input, the ingredient material, and the resulting item. +TransmuteRecipeBuilder.transmute(RecipeCategory.MISC, this.tag(ItemTags.SHULKER_BOXES), + Ingredient.of(DyeItem.byColor(DyeColor.BLUE)), ShulkerBoxBlock.getBlockByColor(DyeColor.BLUE).asItem()) + // Sets the group of the recipe to display in the recipe book. + .group("shulker_box_dye") + // Creates the recipe advancement. While not mandated by the consuming background systems, + // the recipe builder will crash if you omit this. The first parameter is the advancement name, + // and the second one is the condition. Normally, you want to use the has() shortcut for the condition. + // Multiple advancement requirements can be added by calling #unlockedBy multiple times. + .unlockedBy("has_shulker_box", this.has(ItemTags.SHULKER_BOXES)) + // Stores the recipe in the passed RecipeOutput, to be written to disk. + // If you want to add conditions to the recipe, those can be set on the output. + .save(this.output); +``` + +### Special Crafting + +In some cases, outputs must be created dynamically from inputs. Most of the time, this is to set data components on the output by calculating their values from the input stacks. These recipes usually only specify the type and hardcode everything else. For example: + +```json5 { "type": "minecraft:crafting_special_armordye" } ``` -This recipe, which is for leather armor dyeing, just specifies the type and hardcodes everything else - most notably the color calculation, which would be hard to express in JSON. Minecraft prefixes all special crafting recipes with `crafting_special_`, however this practice is not necessary to follow. +This recipe, which is for leather armor dyeing, just specifies the type and hardcodes everything else - most notably the color calculation, which would be hard to express in JSON. Minecraft prefixes most special crafting recipes with `crafting_special_`, however this practice is not necessary to follow. -Generating this recipe looks as follows: +Generating this recipe looks as follows in `RecipeProvider#buildRecipes`: ```java // The parameter of #special is a Function>. // All vanilla special recipes use a constructor with one CraftingBookCategory parameter for this. SpecialRecipeBuilder.special(ArmorDyeRecipe::new) // This overload of #save allows us to specify a name. It can also be used on shaped or shapeless builders. - .save(output, "armor_dye"); + .save(this.output, "armor_dye"); ``` Vanilla provides the following special crafting serializers (mods may add more): @@ -172,7 +207,6 @@ Vanilla provides the following special crafting serializers (mods may add more): - `minecraft:crafting_special_repairitem`: For repairing two broken items into one. - `minecraft:crafting_special_shielddecoration`: For applying a banner to a shield. - `minecraft:crafting_special_shulkerboxcoloring`: For coloring a shulker box while preserving its contents. -- `minecraft:crafting_special_suspiciousstew`: For crafting suspicious stews depending on the input flower. - `minecraft:crafting_special_tippedarrow`: For crafting tipped arrows depending on the input potion. - `minecraft:crafting_decorated_pot`: For crafting decorated pots from sherds. @@ -204,7 +238,7 @@ Let's digest this line by line: - `ingredient`: The input [ingredient] of the recipe. - `result`: The result of the recipe. This is [an item stack's JSON representation][itemjson]. -Datagen for these recipes looks like this: +Datagen for these recipes looks like this in `RecipeProvider#buildRecipes`: ```java // Use #smoking for smoking recipes, #blasting for blasting recipes, and #campfireCooking for campfire recipes. @@ -222,9 +256,9 @@ SimpleCookingRecipeBuilder.smelting( 200 ) // The recipe advancement, like with the crafting recipes above. - .unlockedBy("has_kelp", has(Blocks.KELP)) + .unlockedBy("has_kelp", this.has(Blocks.KELP)) // This overload of #save allows us to specify a name. - .save(p_301191_, "dried_kelp_smelting"); + .save(this.output, "dried_kelp_smelting"); ``` :::info @@ -238,9 +272,7 @@ Stonecutter recipes use the `minecraft:stonecutting` recipe type. They are about ```json5 { "type": "minecraft:stonecutting", - "ingredient": { - "item": "minecraft:andesite" - }, + "ingredient": "minecraft:andesite", "result": { "count": 2, "id": "minecraft:andesite_slab" @@ -250,12 +282,12 @@ Stonecutter recipes use the `minecraft:stonecutting` recipe type. They are about The `type` defines the recipe serializer (`minecraft:stonecutting`). The ingredient is an [ingredient], and the result is a basic [item stack JSON][itemjson]. Like crafting recipes, they can also optionally specify a `group` for grouping in the recipe book. -Datagen is also simple: +Datagen is also simple in `RecipeProvider#buildRecipes`: ```java SingleItemRecipeBuilder.stonecutting(Ingredient.of(Items.ANDESITE), RecipeCategory.BUILDING_BLOCKS, Items.ANDESITE_SLAB, 2) - .unlockedBy("has_andesite", has(Items.ANDESITE)) - .save(output, "andesite_slab_from_andesite_stonecutting"); + .unlockedBy("has_andesite", this.has(Items.ANDESITE)) + .save(this.output, "andesite_slab_from_andesite_stonecutting"); ``` Note that the single item recipe builder does not support actual ItemStack results, and as such, no results with data components. The recipe codec, however, does support them, so a custom builder would need to be implemented if this functionality was desired. @@ -270,20 +302,14 @@ This recipe serializer is for transforming two input items into one, preserving ```json5 { - "type": "minecraft:smithing_transform", - "base": { - "item": "minecraft:diamond_axe" - }, - "template": { - "item": "minecraft:netherite_upgrade_smithing_template" - }, - "addition": { - "item": "minecraft:netherite_ingot" - }, - "result": { - "count": 1, - "id": "minecraft:netherite_axe" - } + "type": "minecraft:smithing_transform", + "addition": "#minecraft:netherite_tool_materials", + "base": "minecraft:diamond_axe", + "result": { + "count": 1, + "id": "minecraft:netherite_axe" + }, + "template": "minecraft:netherite_upgrade_smithing_template" } ``` @@ -295,7 +321,7 @@ Let's break this down line by line: - `addition`: The addition [ingredient] of the recipe. Usually, this is some sort of material, for example a netherite ingot. - `result`: The result of the recipe. This is [an item stack's JSON representation][itemjson]. -During datagen, call on `SmithingTransformRecipeBuilder#smithing` to add your recipe: +During datagen, call on `SmithingTransformRecipeBuilder#smithing` to add your recipe in `RecipeProvider#buildRecipes`: ```java SmithingTransformRecipeBuilder.smithing( @@ -304,7 +330,7 @@ SmithingTransformRecipeBuilder.smithing( // The base ingredient. Ingredient.of(Items.DIAMOND_AXE), // The addition ingredient. - Ingredient.of(Items.NETHERITE_INGOT), + this.tag(ItemTags.NETHERITE_TOOL_MATERIALS), // The recipe book category. RecipeCategory.TOOLS, // The result item. Note that while the recipe codec accepts an item stack here, the builder does not. @@ -312,9 +338,9 @@ SmithingTransformRecipeBuilder.smithing( Items.NETHERITE_AXE ) // The recipe advancement, like with the other recipes above. - .unlocks("has_netherite_ingot", has(Items.NETHERITE_INGOT)) + .unlocks("has_netherite_ingot", this.has(ItemTags.NETHERITE_TOOL_MATERIALS)) // This overload of #save allows us to specify a name. - .save(output, "netherite_axe_smithing"); + .save(this.output, "netherite_axe_smithing"); ``` ### Trim Smithing @@ -323,16 +349,10 @@ Trim smithing is the process of applying armor trims to armor: ```json5 { - "type": "minecraft:smithing_trim", - "addition": { - "tag": "minecraft:trim_materials" - }, - "base": { - "tag": "minecraft:trimmable_armor" - }, - "template": { - "item": "minecraft:bolt_armor_trim_smithing_template" - } + "type": "minecraft:smithing_trim", + "addition": "#minecraft:trim_materials", + "base": "#minecraft:trimmable_armor", + "template": "minecraft:bolt_armor_trim_smithing_template" } ``` @@ -345,23 +365,23 @@ Again, let's break this down into its bits: This recipe serializer is notably missing a result field. This is because it uses the base input and "applies" the template and addition items on it, i.e., it sets the base's components based on the other inputs and uses the result of that operation as the recipe's result. -During datagen, call on `SmithingTrimRecipeBuilder#smithingTrim` to add your recipe: +During datagen, call on `SmithingTrimRecipeBuilder#smithingTrim` to add your recipe in `RecipeProvider#buildRecipes`: ```java SmithingTrimRecipeBuilder.smithingTrim( - // The base ingredient. - Ingredient.of(ItemTags.TRIMMABLE_ARMOR), // The template ingredient. Ingredient.of(Items.BOLT_ARMOR_TRIM_SMITHING_TEMPLATE), + // The base ingredient. + this.tag(ItemTags.TRIMMABLE_ARMOR), // The addition ingredient. - Ingredient.of(ItemTags.TRIM_MATERIALS), + this.tag(ItemTags.TRIM_MATERIALS), // The recipe book category. RecipeCategory.MISC ) // The recipe advancement, like with the other recipes above. - .unlocks("has_smithing_trim_template", has(Items.BOLT_ARMOR_TRIM_SMITHING_TEMPLATE)) + .unlocks("has_smithing_trim_template", this.has(Items.BOLT_ARMOR_TRIM_SMITHING_TEMPLATE)) // This overload of #save allows us to specify a name. Yes, this name is copied from vanilla. - .save(output, "bolt_armor_trim_smithing_template_smithing_trim"); + .save(this.output, "bolt_armor_trim_smithing_template_smithing_trim"); ``` [ingredient]: ingredients.md diff --git a/docs/resources/server/recipes/index.md b/docs/resources/server/recipes/index.md index 90b5cd84..a269e5a2 100644 --- a/docs/resources/server/recipes/index.md +++ b/docs/resources/server/recipes/index.md @@ -1,6 +1,6 @@ # Recipes -Recipes are a way to transform a set of objects into other objects within a Minecraft world. Although Minecraft uses this system purely for item transformations, the system is built in a way that allows any kind of objects - blocks, entities, etc. - to be transformed. Almost all recipes use recipe data files; a "recipe" is assumed to be a data-driven recipe in this article unless explicitly stated otherwise. +Recipes are a datapack registry used as a way to transform a set of objects into other objects within a Minecraft world. Although Minecraft uses this system purely for item transformations, the system is built in a way that allows any kind of objects - blocks, entities, etc. - to be transformed. Recipe data files are located at `data//recipe/.json`. For example, the recipe `minecraft:diamond_block` is located at `data/minecraft/recipe/diamond_block.json`. @@ -10,10 +10,15 @@ Recipe data files are located at `data//recipe/.json`. For exam - A **`Recipe`** holds in-code representations of all JSON fields, alongside the matching logic ("Does this input match the recipe?") and some other properties. - A **`RecipeInput`** is a type that provides inputs to a recipe. Comes in several subclasses, e.g. `CraftingInput` or `SingleRecipeInput` (for furnaces and similar). - A **recipe ingredient**, or just **ingredient**, is a single input for a recipe (whereas the `RecipeInput` generally represents a collection of inputs to check against a recipe's ingredients). Ingredients are a very powerful system and as such outlined [in their own article][ingredients]. +- A **`PlacementInfo`** is a definition of items the recipe contains and what indexes they should populate. If the recipe cannot be captured to some degree based on the items provided (e.g., only changing the data components), then `PlacementInfo#NOT_PLACEABLE` is used. +- A **`SlotDisplay`** defines how a single slot should display within a recipe viewer, like the recipe book. +- A **`RecipeDisplay`** defines the `SlotDisplay`s of a recipe to be consumed by a recipe viewer, like the recipe book. While the interface only contains methods for the result of a recipe and the workstation the recipe was conducted within, a subtype can capture information like ingredients or grid size. - The **`RecipeManager`** is a singleton field on the server that holds all loaded recipes. - A **`RecipeSerializer`** is basically a wrapper around a [`MapCodec`][codec] and a [`StreamCodec`][streamcodec], both used for serialization. - A **`RecipeType`** is the registered type equivalent of a `Recipe`. It is mainly used when looking up recipes by type. As a rule of thumb, different crafting containers should use different `RecipeType`s. For example, the `minecraft:crafting` recipe type covers the `minecraft:crafting_shaped` and `minecraft:crafting_shapeless` recipe serializers, as well as the special crafting serializers. +- A **`RecipeBookCategory`** is a group representing some recipes when viewed through a recipe book. - A **recipe [advancement]** is an advancement responsible for unlocking a recipe in the recipe book. They are not required, and generally neglected by players in favor of recipe viewer mods, however the [recipe data provider][datagen] generates them for you, so it's recommended to just roll with it. +- A **`RecipePropertySet`** defines the available list of ingredients that can be accepted by the defined input slot in a menu. - A **`RecipeBuilder`** is used during datagen to create JSON recipes. - A **recipe factory** is a method reference used to create a `Recipe` from a `RecipeBuilder`. It can either be a reference to a constructor, or a static builder method, or a functional interface (often named `Factory`) created specifically for this purpose. @@ -34,31 +39,33 @@ A full list of types provided by Minecraft can be found in the [Built-In Recipe ## Using Recipes -Recipes are loaded, stored and obtained via the `RecipeManager` class, which is in turn obtained via `ServerLevel#getRecipeManager` or - if you don't have a `ServerLevel` available - `ServerLifecycleHooks.getCurrentServer()#getRecipeManager`. Be aware that while the client has a full copy of the `RecipeManager` for display purposes, recipe logic should always run on the server to avoid sync issues. +Recipes are registered, stored and obtained via the `RecipeManager` class, which is in turn obtained via `ServerLevel#recipeAccess` or - if you don't have a `ServerLevel` available - `ServerLifecycleHooks.getCurrentServer()#getRecipeManager`. The client does not sync the recipes themselves, only the `RecipePropertySet` for restricting inputs on menu slots and a `RecipeDisplayEntry` for the recipe book. All recipe logic should always run on the server. -The easiest way to get a recipe is by ID: +The easiest way to get a recipe is by its resource key: ```java -RecipeManager recipes = serverLevel.getRecipeManager(); -// RecipeHolder is a record of the recipe id and the recipe itself. -Optional> optional = recipes.byId(ResourceLocation.withDefaultNamespace("diamond_block")); +RecipeManager recipes = serverLevel.recipeAccess(); +// RecipeHolder is a record of the resource key and the recipe itself. +Optional> optional = recipes.byKey( + ResourceKey.create(Registries.RECIPE, ResourceLocation.withDefaultNamespace("diamond_block")) +); optional.map(RecipeHolder::value).ifPresent(recipe -> { - // Do whatever you want to do with the recipe here. Be aware that the recipe may be of any type. + // Do whatever you want to do with the recipe here. Be aware that the recipe may be of any type. }); ``` A more practically applicable method is constructing a `RecipeInput` and trying to get a matching recipe. In this example, we will be creating a `CraftingInput` containing one diamond block using `CraftingInput#of`. This will create a shapeless input, a shaped input would instead use `CraftingInput#ofPositioned`, and other inputs would use other `RecipeInput`s (for example, furnace recipes will generally use `new SingleRecipeInput`). ```java -RecipeManager recipes = serverLevel.getRecipeManager(); +RecipeManager recipes = serverLevel.recipeAccess(); // Construct a RecipeInput, as required by the recipe. For example, construct a CraftingInput for a crafting recipe. // The parameters are width, height and items, respectively. -CraftingInput input = CraftingInput.of(1, 1, List.of(Items.DIAMOND_BLOCK)); -// The generic wildcard on the recipe holder should then extend Recipe. +CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK))); +// The generic wildcard on the recipe holder should then extend CraftingRecipe. // This allows for more type safety later on. -Optional>> optional = recipes.getRecipeFor( +Optional> optional = recipes.getRecipeFor( // The recipe type to get the recipe for. In our case, we use the crafting type. - RecipeTypes.CRAFTING, + RecipeType.CRAFTING, // Our recipe input. input, // Our level context. @@ -66,41 +73,41 @@ Optional>> optional = recipes.getRe ); // This returns the diamond block -> 9 diamonds recipe (unless a datapack changes that recipe). optional.map(RecipeHolder::value).ifPresent(recipe -> { - // Do whatever you want here. Note that the recipe is now a Recipe instead of a Recipe. + // Do whatever you want here. Note that the recipe is now a CraftingRecipe instead of a Recipe. }); ``` Alternatively, you can also get yourself a potentially empty list of recipes that match your input, this is especially useful for cases where it can be reasonably assumed that multiple recipes match: ```java -RecipeManager recipes = serverLevel.getRecipeManager(); -CraftingInput input = CraftingInput.of(1, 1, List.of(Items.DIAMOND_BLOCK)); +RecipeManager recipes = serverLevel.recipeAccess(); +CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK))); // These are not Optionals, and can be used directly. However, the list may be empty, indicating no matching recipes. -List>> list = recipes.getRecipesFor( - // Same parameters as above. - RecipeTypes.CRAFTING, input, serverLevel +Stream>> list = recipes.recipeMap().getRecipesFor( + // Same parameters as above. + RecipeType.CRAFTING, input, serverLevel ); ``` Once we have our correct recipe inputs, we also want to get the recipe outputs. This is done by calling `Recipe#assemble`: ```java -RecipeManager recipes = serverLevel.getRecipeManager(); +RecipeManager recipes = serverLevel.recipeAccess(); CraftingInput input = CraftingInput.of(...); -Optional>> optional = recipes.getRecipeFor(...); +Optional> optional = recipes.getRecipeFor(...); // Use ItemStack.EMPTY as a fallback. ItemStack result = optional .map(RecipeHolder::value) - .map(e -> e.assemble(input, serverLevel.registryAccess())) + .map(recipe -> recipe.assemble(input, serverLevel.registryAccess())) .orElse(ItemStack.EMPTY); ``` If necessary, it is also possible to iterate over all recipes of a type. This is done like so: ```java -RecipeManager recipes = serverLevel.getRecipeManager(); +RecipeManager recipes = serverLevel.recipeAccess(); // Like before, pass the desired recipe type. -List> list = recipes.getAllRecipesFor(RecipeTypes.CRAFTING); +Collection> list = recipes.recipeMap().byType(RecipeType.CRAFTING); ``` ## Other Recipe Mechanisms @@ -135,7 +142,7 @@ See [the Brewing chapter in the Mob Effects & Potions article][brewing]. ## Custom Recipes -To add custom recipes, we need at least three things: a `Recipe`, a `RecipeType`, and a `RecipeSerializer`. Depending on what you are implementing, you may also need a custom `RecipeInput` if reusing an existing subclass is not feasible. +To add custom recipes, we need at least three things: a `Recipe`, a `RecipeType`, and a `RecipeSerializer`. Depending on what you are implementing, you may also need a custom `RecipeInput`, `RecipeDisplay`, `SlotDisplay`, `RecipeBookCategory`, and `RecipePropertySet` if reusing an existing subclass is not feasible. For the sake of example, and to highlight many different features, we are going to implement a recipe-driven mechanic that requires you to right-click a `BlockState` in-world with a certain item, breaking the `BlockState` and dropping the result item. @@ -188,36 +195,14 @@ public class RightClickBlockRecipe implements Recipe { this.result = result; } - // A list of our ingredients. Does not need to be overridden if you have no ingredients - // (the default implementation returns an empty list here). It makes sense to cache larger lists in a field. - @Override - public NonNullList getIngredients() { - NonNullList list = NonNullList.create(); - list.add(this.inputItem); - return list; - } - - // Grid-based recipes should return whether their recipe can fit in the given dimensions. - // We don't have a grid, so we just return if any item can be placed in there. - @Override - public boolean canCraftInDimensions(int width, int height) { - return width * height >= 1; - } - // Check whether the given input matches this recipe. The first parameter matches the generic. // We check our blockstate and our item stack, and only return true if both match. + // If we needed to check the dimensions of our input, we would also do so here. @Override public boolean matches(RightClickBlockInput input, Level level) { return this.inputState == input.state() && this.inputItem.test(input.stack()); } - // Return an UNMODIFIABLE version of your result here. The result of this method is mainly intended - // for the recipe book, and commonly used by JEI and other recipe viewers as well. - @Override - public ItemStack getResultItem(HolderLookup.Provider registries) { - return this.result; - } - // Return the result of the recipe here, based on the given input. The first parameter matches the generic. // IMPORTANT: Always call .copy() if you use an existing result! If you don't, things can and will break, // as the result exists once per recipe, but the assembled stack is created each time the recipe is crafted. @@ -226,11 +211,296 @@ public class RightClickBlockRecipe implements Recipe { return this.result.copy(); } + // When false, will prevent the recipe from being synced within the recipe book or awarded on use/unlock. + // This should only be false if the recipe shouldn't appear in a recipe book, such as extended a map. + // Although this recipe takes in an input state, it could still be used in a custom recipe book using + // the methods below. + @Override + public boolean isSpecial() { + return true; + } + // This example outlines the most important methods. There is a number of other methods to override. + // Some methods will be explained in the below sections as they cannot be easily compressed and understood here. // Check the class definition of Recipe to view them all. } ``` +### Recipe Book Categories + +A `RecipeBookCategory` simply defines a group to display this recipe within in a recipe book. For example, an iron pickaxe crafting recipe would show up in the `RecipeBookCategories#CARFTING_EQUIPMENT` while a cooked cod recipe would show up in `#FURNANCE_FOOD` or `#SMOKER_FOOD`. Each recipe has one associated `RecipeBookCategory`. The vanilla categories can be found in `RecipeBookCategories`. + +:::note +There are two cooked cod recipes, one for the furnance and one for the smoker. The furnace and smoker recipes have different book categories. +::: + +If your recipe does not fit into one of the existing categories, typically because the recipe does not use one of the existing workbench-types (e.g., crafting table, furnace), then a new `RecipeBookCategory` can be created. Each `RecipeBookCategory` must be [registered][registry] to `BuiltInRegistries#RECIPE_BOOK_CATEGORY`: + +```java +/// For some DeferredRegister RECIPE_BOOK_CATEGORIES +public static final Supplier RIGHT_CLICK_BLOCK_CATEGORY = RECIPE_BOOK_CATEGORIES.register( + "right_click_block", RecipeBookCategory::new +); +``` + +Then, to set the category, we must override `#recipeBookCategory` like so: + +```java +public class RightClickBlockRecipe implements Recipe { + // other stuff here + + @Override + public RecipeBookCategory recipeBookCategory() { + return RIGHT_CLICK_BLOCK_CATEGORY.get(); + } +} +``` + +#### Search Categories + +All `RecipeBookCategory`s are technically `ExtendedRecipeBookCategory`s. There is another type of `ExtendedRecipeBookCategory` called `SearchRecipeBookCategory`, which is used to aggregate `RecipeBookCategory`s when viewing all recipes in a recipe book. + +NeoForge allows users to specify their own `ExtendedRecipeBookCategory` as a search category via `RegisterRecipeBookSearchCategoriesEvent#register` on the mod event bus. `register` takes in the `ExtendedRecipeBookCategory` representing the search category and the `RecipeBookCategory`s that make up that search category. The `ExtendedRecipeBookCategory` search category does not need to be registered to some static vanilla registry. + +```java +// In some location +public static final ExtendedRecipeBookCategory RIGHT_CLICK_BLOCK_SEARCH_CATEGORY = new ExtendedRecipeBookCategory() {}; + +// On the mod event bus +@SubscribeEvent +public static void registerSearchCategories(RegisterRecipeBookSearchCategoriesEvent event) { + event.register( + // The search category + RIGHT_CLICK_BLOCK_SEARCH_CATEGORY, + // All recipe categories within the search category as varargs + RIGHT_CLICK_BLOCK_CATEGORY.get() + ) +} +``` + +### Placement Info + +A `PlacementInfo` is meant to define the crafting requirements used by the recipe consumer to determine what recipes can be placed in workbench-type. `PlacementInfo` are only meant for item ingredients, so if other types of ingredients are desired (e.g., fluid, block), the surrounding logic will need to be implemented from scratch. In these cases, the recipe can be labelled as not placeable, and say as such via `PlacementInfo#NOT_PLACEABLE`. However, if there is at least one item-like object in your recipe, you should create a `PlacementInfo`. + +A `PlacementInfo` can be created via `create`, which takes in one or a list of ingredient, or `createFromOptionals`, which takes in a list of optional ingredients. If your recipe contains some representation of empty slots, then `createFromOptionals` should be used, providing an empty optional for an empty slot: + +```java +public class RightClickBlockRecipe implements Recipe { + // other stuff here + private PlacementInfo info; + + @Override + public PlacementInfo placementInfo() { + // This delegate is in case the ingredient is not fully populated at this point in time + // Tags and recipes are loaded at the same time, which is why this might be the case. + if (this.info == null) { + // Use optional ingredient as the block state may have an item representation + List> ingredients = new ArrayList<>(); + Item stateItem = this.inputState.getBlock().asItem(); + ingredients.add(stateItem != Items.AIR ? Optional.of(Ingredient.of(stateItem)): Optional.empty()); + ingredients.add(Optional.of(this.inputItem)); + + // Create placement info + this.info = PlacementInfo.createFromOptionals(ingredients); + } + + return this.info; + } +} +``` + +### Slot Displays + +`SlotDisplay`s represent the information on what should render in what slot when viewed by a recipe consumer, like a recipe book. A `SlotDisplay` has two methods. First there's `resolve`, which takes in the `ContextMap` containing the available registries and fuel values (as shown in `SlotDisplayContext`); and the current `DisplayContentsFactory`, which accepts the contents to display for this slot; and returns the transformed list of contents into the output to be accepted. Then there's `type`, which holds the [`MapCodec`][codec] and [`StreamCodec`][streamcodec] used to encode/decode the display. + +`SlotDisplay`s are typically implemented on the [`Ingredient` via `#display`, or `ICustomIngredient#display` for modded ingredients][ingredients]; however, in some cases, the input may not be an ingredient, meaning a `SlotDisplay` will need to use one available, or have a new one created. + +These are the available slot displays provided by Vanilla and NeoForge: + +- `SlotDisplay.Empty`: A slot that represents nothing. +- `SlotDisplay.ItemSlotDisplay`: A slot that respresents an item. +- `SlotDisplay.ItemStackSlotDisplay`: A slot that represents an item stack. +- `SlotDisplay.TagSlotDisplay`: A slot that represents an item tag. +- `SlotDisplay.WithRemainder`: A slot that represents some input that has some crafting remainder. +- `SlotDisplay.AnyFuel`: A slot that represents all fuel items. +- `SlotDisplay.Composite`: A slot that represents a combination of other slot displays. +- `SlotDisplay.SmithingTrimDemoSlotDisplay`: A slot that represents a random smithing drim being applied to some base with the given material. + +- `FluidSlotDisplay`: A slot that represents a fluid. +- `FluidStackSlotDisplay`: A slot that represents a fluid stack. +- `FluidTagSlotDisplay`: A slot that represents a fluid tag. + +We have three 'slots' in our recipe: the `BlockState` input, the `Ingredient` input, and the `ItemStack` result. The `Ingredient` input will already have an associated `SlotDisplay` and the `ItemStack` can be represented by `SlotDisplay.ItemStackSlotDisplay`. The `BlockState`, on the other hand, will need its own custom `SlotDisplay` and `DisplayContentsFactory`, as existing ones only take in item stacks, and for this example, block states are handled in a different fashion. + +Starting with the `DisplayContentsFactory`, it is meant to be a transformer for some type to desired content display type. The available factories are: + +- `DisplayContentsFactory.ForStacks`: A transformer that takes in `ItemStack`s. +- `DisplayContentsFactory.ForRemainders`: A transformer that takes in the input object and a list of remainder objects. + +- `DisplayContentsFactory.ForFluidStacks`: A transformer that takes in a `FluidStack`. + +With this, the `DisplayContentsFactory` can be implemented to transform the provided objects into the desired output. For example, `SlotDisplay.ItemStackContentsFactory`, takes the `ForStacks` transformer and has the stacks transformed into `ItemStack`s. + +For our `BlockState`, we'll create a factory that takes in the state, along with a basic implementation that outputs the state itself. + +```java +// A basic transformer for block states +public interface ForBlockStates extends DisplayContentsFactory { + + // Delegate methods + default forState(Holder block) { + return this.forState(block.value()); + } + + default forState(Block block) { + return this.forState(block.defaultBlockState()); + } + + // The block state to take in and transform to the desired output + T forState(BlockState state); +} + +// An implementation for a block state output +public class BlockStateContentsFactory implements ForBlockStates { + // Singleton instance + public static final BlockStateContentsFactory INSTANCE = new BlockStateContentsFactory(); + + private BlockStateContentsFactory() {} + + @Override + public BlockState forState(BlockState state) { + return state; + } +} + +// An implementation for an item stack output +public class BlockStateStackContentsFactory implements ForBlockStates { + // Singleton instance + public static final BlockStateStackContentsFactory INSTANCE = new BlockStateStackContentsFactory(); + + private BlockStateStackContentsFactory() {} + + @Override + public ItemStack forState(BlockState state) { + return new ItemStack(state.getBlock()); + } +} +``` + +Then, with that, we can create a new `SlotDisplay`. The `SlotDisplay.Type` must be [registered][registry]: + +```java +// A simple slot display +public record BlockStateSlotDisplay(BlockState state) implements SlotDisplay { + public static final MapCodec CODEC = BlockState.CODEC.fieldOf("state") + .xmap(BlockStateSlotDisplay::new, BlockStateSlotDisplay::state); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), BlockStateSlotDisplay::state, + BlockStateSlotDisplay::new + ); + + @Override + public Stream resolve(ContextMap context, DisplayContentsFactory factory) { + return switch (factory) { + // Check for our contents factory and transform if necessary + case ForBlockStates states -> Stream.of(states.forState(this.state)); + // If you want the contents to be handled differently depending on contents display + // then you can case on other displays like so + case ForStacks stacks -> Stream.of(stacks.forStack(state.getBlock().asItem())); + // If no factories match, then do not return anything in the transformed stream + default -> Stream.empty(); + } + } + + @Override + public SlotDisplay.Type type() { + // Return the registered type from below + return BLOCK_STATE_SLOT_DISPLAY.get(); + } +} + +// In some registrar class +/// For some DeferredRegister> SLOT_DISPLAY_TYPES +public static final Supplier> BLOCK_STATE_SLOT_DISPLAY = SLOT_DISPLAY_TYPES.register( + "block_state", + () -> new SlotDisplay.Type<>(BlockStateSlotDisplay.CODEC, BlockStateSlotDisplay.STREAM_CODEC) +); +``` + +### Recipe Display + +A `RecipeDisplay` is the same as a `SlotDisplay`, except that it represents an entire recipe. The default interface only keeps track of the `result` of recipe and the `craftingStation` which represents the workbench where the recipe is applied. The `RecipeDisplay` also has a `type` that holds the [`MapCodec`][codec] and [`StreamCodec`][streamcodec] used to encode/decode the display.However, subtypes of `RecipeDisplay` contains all the information required to properly render the slot on the client. As such, we will need to create our own `RecipeDisplay`. + +All slots and ingredients should be represented as `SlotDisplay`s. Any restrictions, such as grid size, can be provided in any manner the user decides. + +```java +// A simple recipe display +public record RightClickBlockRecipeDisplay( + SlotDisplay inputState, + SlotDisplay inputItem, + SlotDisplay result, // Implements RecipeDisplay#result + SlotDisplay craftingStation // Implements RecipeDisplay#craftingStation +) implements RecipeDisplay { + public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group( + SlotDisplay.CODEC.fieldOf("inputState").forGetter(RightClickBlockRecipeDisplay::inputState), + SlotDisplay.CODEC.fieldOf("inputState").forGetter(RightClickBlockRecipeDisplay::inputItem), + SlotDisplay.CODEC.fieldOf("result").forGetter(RightClickBlockRecipeDisplay::result), + SlotDisplay.CODEC.fieldOf("crafting_station").forGetter(RightClickBlockRecipeDisplay::craftingStation) + ) + .apply(instance, RightClickBlockRecipeDisplay::new) + ); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + SlotDisplay.STREAM_CODEC, + RightClickBlockRecipeDisplay::inputState, + SlotDisplay.STREAM_CODEC, + RightClickBlockRecipeDisplay::inputItem, + SlotDisplay.STREAM_CODEC, + RightClickBlockRecipeDisplay::result, + SlotDisplay.STREAM_CODEC, + RightClickBlockRecipeDisplay::craftingStation, + RightClickBlockRecipeDisplay::new + ); + + @Override + public RecipeDisplay.Type type() { + // Return the registered type from below + return RIGHT_CLICK_BLOCK_RECIPE_DISPLAY.get(); + } +} + +// In some registrar class +/// For some DeferredRegister> RECIPE_DISPLAY_TYPES +public static final Supplier> RIGHT_CLICK_BLOCK_RECIPE_DISPLAY = RECIPE_DISPLAY_TYPES.register( + "right_click_block", + () -> new RecipeDisplay.Type<>(RightClickBlockRecipeDisplay.CODEC, RightClickBlockRecipeDisplay.STREAM_CODEC) +); +``` + +Then we can create the recipe display for the recipe by overriding `#display` like so: + +```java +public class RightClickBlockRecipe implements Recipe { + // other stuff here + + @Override + public List display() { + // You can have many different displays for the same recipe + // But this example will only use one like the other recipes. + return List.of( + // Add our recipe display with the specified slots + new RightClickBlockRecipeDisplay( + new BlockStateSlotDisplay(this.inputState), + this.inputItem.display(), + new SlotDisplay.ItemStackSlotDisplay(this.result), + new SlotDisplay.ItemSlotDisplay(Items.GRASS_BLOCK) + ) + ) + } +} +``` + ### The Recipe Type Next up, our recipe type. This is fairly straightforward because there's no data other than a name associated with a recipe type. They are one of two [registered][registry] parts of the recipe system, so like with all other registries, we create a `DeferredRegister` and register to it: @@ -239,11 +509,17 @@ Next up, our recipe type. This is fairly straightforward because there's no data public static final DeferredRegister> RECIPE_TYPES = DeferredRegister.create(Registries.RECIPE_TYPE, ExampleMod.MOD_ID); -public static final Supplier> RIGHT_CLICK_BLOCK = +public static final Supplier> RIGHT_CLICK_BLOCK_TYPE = RECIPE_TYPES.register( "right_click_block", // We need the qualifying generic here due to generics being generics. - () -> RecipeType.simple(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "right_click_block")) + registryName -> new RecipeType { + + @Override + public String toString() { + return registryName.toString(); + } + } ); ``` @@ -254,8 +530,8 @@ public class RightClickBlockRecipe implements Recipe { // other stuff here @Override - public RecipeType getType() { - return RIGHT_CLICK_BLOCK.get(); + public RecipeType> getType() { + return RIGHT_CLICK_BLOCK_TYPE.get(); } } ``` @@ -315,7 +591,7 @@ public class RightClickBlockRecipe implements Recipe { // other stuff here @Override - public RecipeSerializer getSerializer() { + public RecipeSerializer> getSerializer() { return RIGHT_CLICK_BLOCK.get(); } } @@ -325,46 +601,103 @@ public class RightClickBlockRecipe implements Recipe { Now that all parts of your recipe are complete, you can make yourself some recipe JSONs (see the [datagen] section for that) and then query the recipe manager for your recipes, like above. What you then do with the recipe is up to you. A common use case would be a machine that can process your recipes, storing the active recipe as a field. -In our case, however, we want to apply the recipe when an item is right-clicked on a block. We will do so using an [event handler][event]. Keep in mind that this is an example implementation, and you can alter this in any way you like (so long as you run it on the server). +In our case, however, we want to apply the recipe when an item is right-clicked on a block. We will do so using an [event handler][event]. Keep in mind that this is an example implementation, and you can alter this in any way you like (so long as you run it on the server). As we want the interaction state to match on both the client and server, we will also need to [sync any relevant input states across the network][networking]. ```java +// A basic packet class, must be registered +public record ClientboundRightClickBlockRecipesPayload( + Set inputStates, Set> inputItems +) implements CustomPacketPayload { + + // ... +} +// Packet stores data in an instance class +// Present on both server and client to do initial matching +public class RightClickBlockRecipeInputs { + // Only one instance + public static final RightClickBlockRecipeInputs INSTANCE = new RightClickBlockRecipeInputs(); + + private Set inputStates; + private Set> inputItems; + + private RightClickBlockRecipeInputs() {} + + public void setInputs(Set inputStates, Set> inputItems) { + this.inputStates = inputStates; + this.inputItems = inputItems; + } + + public boolean test(BlockState state, ItemStack stack) { + return this.inputStates.contains(state) && this.inputItems.contains(stack.getItemHolder()); + } +} + +// On the game event bus +@SubscribeEvent +public static void datapackSync(OnDatapackSyncEvent event) { + // Populate inputs + Set inputStates = new HashSet<>(); + Set> inputItems = new HashSet<>(); + + event.getPlayerList().getServer().getRecipeManager() + .recipeMap().byType(RIGHT_CLICK_BLOCK_TYPE.get()) + .forEach(holder -> { + var recipe = holder.value(); + inputStates.add(recipe.getInputState()); + inputItems.addAll(recipe.getInputItem().items()); + }); + + // Set server inputs + RightClickBlockRecipeInputs.INSTANCE.setInputs(inputStates, inputItems); + + // Send to client + ClientboundRightClickBlockRecipesPayload payload = new ClientboundRightClickBlockRecipesPayload(inputStates, inputItems); + event.getRelevantPlayers().forEach(player -> PacketDistributor.sendToPlayer(player, payload)); +} + +// On the game event bus @SubscribeEvent public static void useItemOnBlock(UseItemOnBlockEvent event) { // Skip if we are not in the block-dictated phase of the event. See the event's javadocs for details. if (event.getUsePhase() != UseItemOnBlockEvent.UsePhase.BLOCK) return; - // Get the parameters we need. - UseOnContext context = event.getUseOnContext(); - Level level = context.getLevel(); - BlockPos pos = context.getClickedPos(); + // Get parameters to check input first + Level level = event.getLevel(); + BlockPos pos = event.getPos(); BlockState blockState = level.getBlockState(pos); - ItemStack itemStack = context.getItemInHand(); - RecipeManager recipes = level.getRecipeManager(); - // Create an input and query the recipe. - RightClickBlockInput input = new RightClickBlockInput(blockState, itemStack); - Optional>> optional = recipes.getRecipeFor( + ItemStack itemStack = event.getItemStack(); + + // Check if the input can result in a recipe on both sides + if (!RightClickBlockRecipeInputs.INSTANCE.test(blockState, itemStack)) return; + + // If so, make sure on server before checking recipe + if (!level.isClientSide() && level instanceof ServerLevel serverLevel) { + // Create an input and query the recipe. + RightClickBlockInput input = new RightClickBlockInput(blockState, itemStack); + Optional>> optional = serverLevel.recipeAccess().getRecipeFor( // The recipe type. - RIGHT_CLICK_BLOCK, + RIGHT_CLICK_BLOCK_TYPE.get(), input, level - ); - ItemStack result = optional + ); + ItemStack result = optional .map(RecipeHolder::value) .map(e -> e.assemble(input, level.registryAccess())) .orElse(ItemStack.EMPTY); - // If there is a result, break the block and drop the result in the world. - if (!result.isEmpty()) { - level.removeBlock(pos, false); - // If the level is not a server level, don't spawn the entity. - if (!level.isClientSide()) { + + // If there is a result, break the block and drop the result in the world. + if (!result.isEmpty()) { + level.removeBlock(pos, false); ItemEntity entity = new ItemEntity(level, // Center of pos. pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, result); level.addFreshEntity(entity); } - // Cancel the event to stop the interaction pipeline. - event.cancelWithResult(ItemInteractionResult.sidedSuccess(level.isClientSide)); } + + // Cancel the event to stop the interaction pipeline regardless of side. + // Already made sure that there could be a result. + event.cancelWithResult(InteractionResult.SUCCESS_SERVER); } ``` @@ -378,23 +711,37 @@ The `ShapedRecipePattern` class, responsible for holding the in-memory represent ## Data Generation -Like most other JSON files, recipes can be datagenned. For recipes, we want to extend the `RecipeProvider` class and override `#buildRecipes`: +Like most other JSON files, recipes can be datagenned. For recipes, we want to extend the `RecipeProvider` class and override `#buildRecipes`, and extend the `RecipeProvider.Runner` class to pass to the data generator: ```java public class MyRecipeProvider extends RecipeProvider { - // Get the parameters from GatherDataEvent. - public MyRecipeProvider(PackOutput output, CompletableFuture lookupProvider) { - super(output, registries); - } + // Construct the provider to run + protected MyRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) { + super(provider, output); + } + @Override - protected void buildRecipes(RecipeOutput output) { + protected void buildRecipes() { // Add your recipes here. } + + // The runner to add to the data generator + public static class Runner extends RecipeProvider.Runner { + // Get the parameters from GatherDataEvent. + public Runner(PackOutput output, CompletableFuture lookupProvider) { + super(output, registries); + } + + @Override + protected abstract RecipeProvider createRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) { + return new MyRecipeProvider(provider, output); + } + } } ``` -Of note is the `RecipeOutput` parameter of `#buildRecipes`. Minecraft uses this object to automatically generate a recipe advancement for you. On top of that, NeoForge injects [conditions] support into `RecipeOutput`, which can be called on via `#withConditions`. +Of note is the `RecipeOutput` parameter. Minecraft uses this object to automatically generate a recipe advancement for you. On top of that, NeoForge injects [conditions] support into `RecipeOutput`, which can be called on via `#withConditions`. Recipes themselves are commonly added through subclasses of `RecipeBuilder`. Listing all vanilla recipe builders is beyond the scope of this article (they are explained in the [Built-In Recipe Types article][builtin]), however creating your own builder is explained [below][customdatagen]. @@ -410,7 +757,7 @@ public static void gatherData(GatherDataEvent event) { // other providers here generator.addProvider( event.includeServer(), - new MyRecipeProvider(output, lookupProvider) + new MyRecipeProvider.Runner(output, lookupProvider) ); } ``` @@ -479,19 +826,19 @@ public class RightClickBlockRecipeBuilder extends SimpleRecipeBuilder { this.inputItem = inputItem; } - // Saves a recipe using the given RecipeOutput and id. This method is defined in the RecipeBuilder interface. + // Saves a recipe using the given RecipeOutput and key. This method is defined in the RecipeBuilder interface. @Override - public void save(RecipeOutput output, ResourceLocation id) { + public void save(RecipeOutput output, ResourceKey> key) { // Build the advancement. Advancement.Builder advancement = output.advancement() - .addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(id)) - .rewards(AdvancementRewards.Builder.recipe(id)) + .addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(key)) + .rewards(AdvancementRewards.Builder.recipe(key)) .requirements(AdvancementRequirements.Strategy.OR); this.criteria.forEach(advancement::addCriterion); // Our factory parameters are the result, the block state, and the ingredient. RightClickBlockRecipe recipe = new RightClickBlockRecipe(this.inputState, this.inputItem, this.result); // Pass the id, the recipe, and the recipe advancement into the RecipeOutput. - output.accept(id, recipe, advancement.build(id.withPrefix("recipes/"))); + output.accept(key, recipe, advancement.build(key.location().withPrefix("recipes/"))); } } ``` @@ -507,7 +854,7 @@ protected void buildRecipes(RecipeOutput output) { Blocks.DIRT.defaultBlockState(), Ingredient.of(Items.APPLE) ) - .unlockedBy("has_apple", has(Items.APPLE)) + .unlockedBy("has_apple", this.has(Items.APPLE)) .save(output); // other recipe builders here } @@ -518,7 +865,7 @@ It is also possible to have `SimpleRecipeBuilder` be merged into `RightClickBloc ::: [advancement]: ../advancements.md -[brewing]: ../../../items/mobeffects.md +[brewing]: ../../../items/mobeffects.md#brewing [builtin]: builtin.md [cancel]: ../../../concepts/events.md#cancellable-events [codec]: ../../../datastorage/codecs.md @@ -529,6 +876,7 @@ It is also possible to have `SimpleRecipeBuilder` be merged into `RightClickBloc [event]: ../../../concepts/events.md [ingredients]: ingredients.md [manager]: #using-recipes -[registry]: ../../../concepts/registries.md +[networking]: ../../../networking/payload.md +[registry]: ../../../concepts/registries.md#methods-for-registering [streamcodec]: ../../../networking/streamcodecs.md [tags]: ../tags.md diff --git a/docs/resources/server/recipes/ingredients.md b/docs/resources/server/recipes/ingredients.md index 1679153a..25969f27 100644 --- a/docs/resources/server/recipes/ingredients.md +++ b/docs/resources/server/recipes/ingredients.md @@ -10,17 +10,21 @@ The simplest way to get an ingredient is using the `Ingredient#of` helpers. Seve - `Ingredient.of()` returns an empty ingredient. - `Ingredient.of(Blocks.IRON_BLOCK, Items.GOLD_BLOCK)` returns an ingredient that accepts either an iron or a gold block. The parameter is a vararg of [`ItemLike`s][itemlike], which means that any amount of both blocks and items may be used. -- `Ingredient.of(new ItemStack(Items.DIAMOND_SWORD))` returns an ingredient that accepts an item stack. Be aware that counts and data components are ignored. -- `Ingredient.of(Stream.of(new ItemStack(Items.DIAMOND_SWORD)))` returns an ingredient that accepts an item stack. Like the previous method, but with a `Stream` for if you happen to get your hands on one of those. -- `Ingredient.of(ItemTags.WOODEN_SLABS)` returns an ingredient that accepts any item from the specified [tag], for example any wooden slab. +- `Ingredient.of(Stream.of(Items.DIAMOND_SWORD))` returns an ingredient that accepts an item. Like the previous method, but with a `Stream` for if you happen to get your hands on one of those. +- `Ingredient.of(BuiltInRegistries.ITEM.getOrThrow(ItemTags.WOODEN_SLABS))` returns an ingredient that accepts any item from the specified [tag], for example any wooden slab. Additionally, NeoForge adds a few additional ingredients: - `BlockTagIngredient.of(BlockTags.CONVERTABLE_TO_MUD)` returns an ingredient similar to the tag variant of `Ingredient.of()`, but with a block tag instead. This should be used for cases where you'd use an item tag, but there is only a block tag available (for example `minecraft:convertable_to_mud`). +- `CustomDisplayIngredient.of(Ingredient.of(Items.DIRT), SlotDisplay.Empty.INSTANCE)` returns an ingredient with a custom [`SlotDisplay`][slotdisplay] defining how to render on the client. - `CompoundIngredient.of(Ingredient.of(Items.DIRT))` returns an ingredient with child ingredients, passed in the constructor (vararg parameter). The ingredient matches if any of its children matches. - `DataComponentIngredient.of(true, new ItemStack(Items.DIAMOND_SWORD))` returns an ingredient that, in addition to the item, also matches the data component. The boolean parameter denotes strict matching (true) or partial matching (false). Strict matching means the data components must match exactly, while partial matching means the data components must match, but other data components may also be present. Additional overloads of `#of` exist that allow specifying multiple `Item`s, or provide other options. -- `DifferenceIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))` returns an ingredient that matches everything in the first ingredient that doesn't also match the second ingredient. The given example only matches planks that can burn (i.e. all planks except crimson planks, warped planks and modded nether wood planks). -- `IntersectionIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))` returns an ingredient that matches everything that matches both sub-ingredients. The given example only matches planks that cannot burn (i.e. crimson planks, warped planks and modded nether wood planks). +- `DifferenceIngredient.of(Ingredient.of(BuiltInRegistries.ITEM.getOrThrow(ItemTags.PLANKS)), Ingredient.of(BuiltInRegistries.ITEM.getOrThrow(ItemTags.NON_FLAMMABLE_WOOD)))` returns an ingredient that matches everything in the first ingredient that doesn't also match the second ingredient. The given example only matches planks that can burn (i.e. all planks except crimson planks, warped planks and modded nether wood planks). +- `IntersectionIngredient.of(Ingredient.of(BuiltInRegistries.ITEM.getOrThrow(ItemTags.PLANKS)), Ingredient.of(BuiltInRegistries.ITEM.getOrThrow(ItemTags.NON_FLAMMABLE_WOOD)))` returns an ingredient that matches everything that matches both sub-ingredients. The given example only matches planks that cannot burn (i.e. crimson planks, warped planks and modded nether wood planks). + +:::note +If you are using data generation with ingreidents that take in a `HolderSet` for the tag instance (the ones that call `Registry#getOrThrow`), that should be obtained via the `HolderLookup.Provider`, using `HolderLookup.Provider#lookupOrThrow` to get the item registry and `HolderGetter#getOrThrow` with the `TagKey` to get the holder set. +::: Keep in mind that the NeoForge-provided ingredient types are `ICustomIngredient`s and must call `#toVanilla` before using them in vanilla contexts, as outlined in the beginning of this article. @@ -50,23 +54,13 @@ public class MinEnchantedIngredient implements ICustomIngredient { this.enchantments = enchantments; } - public MinEnchantedIngredient(TagKey tag) { - this(tag, new HashMap<>()); - } - - // Make this chainable for easy use. - public MinEnchantedIngredient with(Holder enchantment, int level) { - enchantments.put(enchantment, level); - return this; - } - // Check if the passed ItemStack matches our ingredient by verifying the item is in the tag // and by testing for presence of all required enchantments with at least the required level. @Override public boolean test(ItemStack stack) { return stack.is(tag) && enchantments.keySet() .stream() - .allMatch(ench -> stack.getEnchantmentLevel(ench) >= enchantments.get(ench)); + .allMatch(ench -> EnchantmentHelper.getEnchantmentsForCrafting(stack).getLevel(ench) >= enchantments.get(ench)); } // Determines whether this ingredient performs NBT or data component matching (false) or not (true). @@ -79,27 +73,14 @@ public class MinEnchantedIngredient implements ICustomIngredient { // Returns a stream of items that match this ingredient. Mostly for display purposes. // There's a few good practices to follow here: - // - Always include at least one item stack, to prevent accidental recognition as empty. + // - Always include at least one item, to prevent accidental recognition as empty. // - Include each accepted Item at least once. - // - If #isSimple is true, this should be exact and contain every item stack that matches. + // - If #isSimple is true, this should be exact and contain every item that matches. // If not, this should be as exact as possible, but doesn't need to be super accurate. - // In our case, we use all items in the tag, each with the required enchantments. + // In our case, we use all items in the tag. @Override - public Stream getItems() { - // Get a list of item stacks, one stack per item in the tag. - List stacks = BuiltInRegistries.ITEM - .getOrCreateTag(tag) - .stream() - .map(ItemStack::new) - .toList(); - // Enchant all stacks with all enchantments. - for (ItemStack stack : stacks) { - enchantments - .keySet() - .forEach(ench -> stack.enchant(ench, enchantments.get(ench))); - } - // Return stream variant of the list. - return stacks.stream(); + public Stream> getItems() { + return BuiltInRegistries.ITEM.getOrThrow(tag).stream(); } } ``` @@ -125,7 +106,7 @@ public class MinEnchantedIngredient implements ICustomIngredient { @Override public IngredientType getType() { - return MIN_ENCHANTED; + return MIN_ENCHANTED.get(); } } ``` @@ -140,7 +121,7 @@ Ingredients that specify a `type` are generally assumed to be non-vanilla. For e ```json5 { - "type": "neoforge:block_tag", + "neoforge:ingredient_type": "neoforge:block_tag", "tag": "minecraft:convertable_to_mud" } ``` @@ -149,7 +130,7 @@ Or another example using our own ingredient: ```json5 { - "type": "examplemod:min_enchanted", + "neoforge:ingredient_type": "examplemod:min_enchanted", "tag": "c:swords", "enchantments": { "minecraft:sharpness": 4 @@ -157,22 +138,18 @@ Or another example using our own ingredient: } ``` -If the `type` is unspecified, then we have a vanilla ingredient. Vanilla ingredients can specify one of two properties: `item` or `tag`. +If the `type` is unspecified, then we have a vanilla ingredient. Vanilla ingredients are strings that either represent an item, or a tag when prefixed with `#`. An example for a vanilla item ingredient: ```json5 -{ - "item": "minecraft:dirt" -} +"minecraft:dirt" ``` An example for a vanilla tag ingredient: ```json5 -{ - "tag": "c:ingots" -} +"#c:ingots" ``` [codec]: ../../../datastorage/codecs.md @@ -180,5 +157,6 @@ An example for a vanilla tag ingredient: [itemstack]: ../../../items/index.md#itemstacks [recipes]: index.md [registry]: ../../../concepts/registries.md +[slotdisplay]: ./index.md#slot-displays [streamcodec]: ../../../networking/streamcodecs.md [tag]: ../tags.md