Custom recipe types are a way to create data-driven recipes for your mod's custom crafting mechanics. As an example, we will create a recipe type for an upgrader block, similar to a Smithing Table.
Creating a Recipe Input Class
Before you can start creating our recipe, you need an implementation of RecipeInput that can hold the input items in our block's inventory. We want an upgrading recipe to have two input items: a base item to be upgraded, and the upgrade itself.
java
public record UpgradingRecipeInput(ItemStack baseItem, ItemStack upgradeItem) implements RecipeInput {
@Override
public ItemStack getItem(int index) {
return switch (index) {
case 0 -> baseItem;
case 1 -> upgradeItem;
default -> ItemStack.EMPTY;
};
}
@Override
public int size() {
return 2;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Creating the Recipe Class
Now that we have a way to store the input items, we can create our Recipe implementation. Implementations of this class represent a singular recipe defined in a datapack. They are responsible for checking the ingredients and requirements of the recipe and assembling it into a result.
Let's start by defining the result and the ingredients of the recipe.
java
public class UpgradingRecipe implements Recipe<UpgradingRecipeInput> {
private final ItemStack result;
private final Ingredient baseItem;
private final Ingredient upgradeItem;
public UpgradingRecipe(ItemStack result, Ingredient baseItem, Ingredient upgradeItem) {
this.baseItem = baseItem;
this.upgradeItem = upgradeItem;
this.result = result;
}
public ItemStack getResult() {
return result;
}
public Ingredient getBaseItem() {
return baseItem;
}
public Ingredient getUpgradeItem() {
return upgradeItem;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Notice how we're using Ingredient objects for our inputs. This allows our recipe to accept multiple items interchangeably.
Implementing the Methods
Next, let's implement the methods from the recipe interface. The interesting ones are matches and assemble. The matches method tests if the input items from our RecipeInput implementation match our ingredients. The assemble method then creates the resulting ItemStack.
To test if the ingredients match, we can use the test method of our ingredients.
java
@Override
public boolean matches(UpgradingRecipeInput recipeInput, Level level) {
return baseItem.test(recipeInput.baseItem()) && upgradeItem.test(recipeInput.upgradeItem());
}
@Override
public ItemStack assemble(UpgradingRecipeInput recipeInput, HolderLookup.Provider provider) {
return result.copy();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Creating a Recipe Serializer
The recipe serializer uses a MapCodec to read the recipe from JSON and a StreamCodec to send it over the network.
We'll use RecordCodecBuilder#mapCodec to build a map codec for our recipe. It allows us to combine Minecraft's existing codecs into our own:
java
public class UpgradingRecipeSerializer implements RecipeSerializer<UpgradingRecipe> {
public static final MapCodec<UpgradingRecipe> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ItemStack.STRICT_CODEC.fieldOf("result").forGetter(UpgradingRecipe::getResult),
Ingredient.CODEC.fieldOf("baseItem").forGetter(UpgradingRecipe::getBaseItem),
Ingredient.CODEC.fieldOf("upgradeItem").forGetter(UpgradingRecipe::getUpgradeItem)
).apply(instance, UpgradingRecipe::new)
);
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
The stream codec can be created in a similar way using StreamCodec#composite:
java
public static final StreamCodec<RegistryFriendlyByteBuf, UpgradingRecipe> STREAM_CODEC = StreamCodec.composite(
ItemStack.STREAM_CODEC,
UpgradingRecipe::getResult,
Ingredient.CONTENTS_STREAM_CODEC,
UpgradingRecipe::getBaseItem,
Ingredient.CONTENTS_STREAM_CODEC,
UpgradingRecipe::getUpgradeItem,
UpgradingRecipe::new
);1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Let's use these codecs to implement the methods from RecipeSerializer:
java
@Override
public MapCodec<UpgradingRecipe> codec() {
return CODEC;
}
@Override
public StreamCodec<RegistryFriendlyByteBuf, UpgradingRecipe> streamCodec() {
return STREAM_CODEC;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Now we'll register the recipe serializer as well as a recipe type. You can do this in your mod's initializer, or in a separate class, with a method invoked by your mod's initializer:
java
public static final UpgradingRecipeSerializer UPGRADING_RECIPE_SERIALIZER = Registry.register(
BuiltInRegistries.RECIPE_SERIALIZER,
Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "upgrading"),
new UpgradingRecipeSerializer()
);
public static final RecipeType<UpgradingRecipe> UPGRADING_RECIPE_TYPE = Registry.register(
BuiltInRegistries.RECIPE_TYPE,
Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "upgrading"),
new RecipeType<UpgradingRecipe>() { }
);1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Back to our recipe class, we can now add the methods that return the objects we just registered:
java
@Override
public RecipeSerializer<? extends Recipe<UpgradingRecipeInput>> getSerializer() {
return ExampleModRecipes.UPGRADING_RECIPE_SERIALIZER;
}
@Override
public RecipeType<? extends Recipe<UpgradingRecipeInput>> getType() {
return ExampleModRecipes.UPGRADING_RECIPE_TYPE;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
To complete our custom recipe type, we just need to implement the remaining placementInfo and recipeBookCategory methods, which are used by the recipe book to place our recipe in a screen. For now, we'll just return PlacementInfo.NOT_PLACEABLE and null, as the recipe book cannot be easily expanded to modded workstations. We'll also override isSpecial to return true, to prevent some other recipe-book-related logic from running and logging errors.
java
@Override
public @Nullable RecipeBookCategory recipeBookCategory() {
return null;
}
@Override
public PlacementInfo placementInfo() {
return PlacementInfo.NOT_PLACEABLE;
}
@Override
public boolean isSpecial() {
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Creating a Recipe
Our recipe type is now working, but we are still missing two important things: a recipe for our recipe type, and a way to craft it.
First, let's create a recipe. In your resources folder, create a file in data/example-mod/recipe, with file extension .json. Each recipe json file has a "type" key referring to the recipe serializer of the recipe. The other keys are defined by the codec of that recipe serializer.
In our case, a valid recipe file looks like this:
json
{
"type": "example-mod:upgrading",
"baseItem": "minecraft:iron_pickaxe",
"upgradeItem": "minecraft:diamond",
"result": {
"id": "minecraft:diamond_pickaxe"
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Creating a Menu
INFO
For more details on creating menus, see Container Menus.
To allow us to craft our recipe in the GUI, we will create a block with a Menu:
java
public class UpgradingMenu extends AbstractContainerMenu {
private final Container input = new SimpleContainer(2) {
@Override
public void setChanged() {
super.setChanged();
slotsChanged(this);
}
};
private final ResultContainer output = new ResultContainer();
private final Level level;
public UpgradingMenu(int i, Inventory inventory) {
super(ExampleModRecipes.UPGRADING_MENU_TYPE, i);
this.level = inventory.player.level();
addSlot(new Slot(input, 0, 27, 47));
addSlot(new Slot(input, 1, 76, 47));
addSlot(new Slot(output, 0, 134, 47) {
@Override
public void onTake(Player player, ItemStack itemStack) {
UpgradingMenu.this.onTake(player, itemStack);
}
});
addStandardInventorySlots(inventory, 8, 84);
}
@Override
public void slotsChanged(Container container) {
super.slotsChanged(container);
if (container == input) {
if (level instanceof ServerLevel serverLevel) {
UpgradingRecipeInput recipeInput = new UpgradingRecipeInput(input.getItem(0), input.getItem(1));
Optional<RecipeHolder<UpgradingRecipe>> recipe = serverLevel.recipeAccess().getRecipeFor(ExampleModRecipes.UPGRADING_RECIPE_TYPE, recipeInput, serverLevel);
if (recipe.isPresent()) {
output.setItem(0, recipe.get().value().assemble(recipeInput, serverLevel.registryAccess()));
output.setRecipeUsed(recipe.get());
} else {
output.clearContent();
output.setRecipeUsed(null);
}
}
}
}
public void onTake(Player player, ItemStack stack) {
stack.onCraftedBy(player, stack.getCount());
output.awardUsedRecipes(player, List.of(input.getItem(0), input.getItem(1)));
input.removeItem(0, stack.getCount());
input.removeItem(1, stack.getCount());
}
@Override
public ItemStack quickMoveStack(Player player, int i) {
return ItemStack.EMPTY;
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public void removed(Player player) {
super.removed(player);
clearContainer(player, input);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
A lot to unpack here! This menu has two input slots and an output slot.
The input container is an anonymous subclass of SimpleContainer, which calls the menu's slotsChanged method when its items change. In slotsChanged, we then create an instance of our recipe input class, filling it with the two input slots.
In order to see if it matches any recipes, we'll first ensure we are on the server level, since clients do not know what recipes exist. Then, we'll retrieve the RecipeManager via serverLevel.recipeAccess().
We'll call serverLevel.recipeAccess().getRecipeFor with our recipe input to get a recipe that matches the inputs. If a recipe was found, we can add or remove the result from the result container.
To detect when the user takes the result out, we create an anonymous subclass of Slot. The onTake method of our menu then removes the input items.
To prevent deleting items, it is important to drop the inputs back when the screen is closed, as shown in the removed method.
Recipe Synchronization
INFO
This section is optional, and only needed if you need clients to know about recipes.
As mentioned earlier, recipes are handled entirely on the logical server. However, in some cases a client may need to know what recipes exist - an example from vanilla is Stonecutters, which have to display the available recipe options for a given ingredient. Also, the plugins of certain recipe viewers, including JEI, run on the logical client, requiring you to use Fabric's recipe synchronization API.
To synchronize your recipes, just call RecipeSynchronization.synchronizeRecipeSerializer in your mod initializer, and provide your mod's recipe serializer:
java
RecipeSynchronization.synchronizeRecipeSerializer(UPGRADING_RECIPE_SERIALIZER);1
Once synchronized, recipes can be retrieved at any point from the client level's recipe manager:
java
clientLevel.recipeAccess().getSynchronizedRecipes().getAllOfType(ExampleModRecipes.UPGRADING_RECIPE_TYPE);1


