Extending Vanilla Recipes 26.1.2
Learn how to make custom recipes for pre-existing workstations.
If you are attempting to add a recipe to an existing workstation, such as a Smithing Table, Crafting Table, or Stonecutter, you likely only need to create a recipe class, implement its methods, register the serializer, and create the recipe JSON(s), as the Block, Menu, and Screen logic have all been completed for you (by Mojang). Let's take a look at some examples.
Overview
Each Vanilla workstation has its own RecipeType, defined in the RecipeType interface. Each workstation expects a certain subtype of Recipe to function.
WARNING
Note that unless you modify the underlying menu, your recipes are limited by the inputs and outputs that the menu has to offer. For example, a Smithing Table has three inputs and one output (in Vanilla, these are usually an Optional<Ingredient> template, Ingredient base, Optional<Ingredient> addition, and ItemStackTemplate result). However, within the Recipe class, you have a lot of freedom in configuring the inputs to make the outputs.
Smithing Table
Let's create a new type of smithing recipe that applies enchantments to the base input to produce the output.
The Smithing Table expects any implementation of the SmithingRecipe interface, which returns RecipeTypes.SMITHING. When making a new SmithingRecipe, one might simply create a new class and implement SmithingRecipe, but another valid way is to extend SimpleSmithingRecipe, a vanilla class, which already implements SmithingRecipe.
java
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class EnchantingSmithingRecipe extends SimpleSmithingRecipe {
private static final Codec<Integer> ENCHANTMENT_LEVEL_CODEC = Codec.intRange(1, 255);
public static final Codec<Object2IntOpenHashMap<Holder<Enchantment>>> ENCHANTMENTS_CODEC = Codec.unboundedMap(Enchantment.CODEC, ENCHANTMENT_LEVEL_CODEC)
.xmap(Object2IntOpenHashMap::new, Function.identity()
);
public static final StreamCodec<RegistryFriendlyByteBuf, Object2IntOpenHashMap<Holder<Enchantment>>> ENCHANTMENTS_STREAM_CODEC = ByteBufCodecs.map(
Object2IntOpenHashMap::new,
Enchantment.STREAM_CODEC,
ByteBufCodecs.VAR_INT
);
public static final MapCodec<EnchantingSmithingRecipe> MAP_CODEC = RecordCodecBuilder.mapCodec(
(instance) -> instance.group(
CommonInfo.MAP_CODEC.forGetter((o) -> o.commonInfo),
Ingredient.CODEC.optionalFieldOf("template").forGetter(recipe -> recipe.template),
Ingredient.CODEC.fieldOf("base").forGetter(recipe -> recipe.base),
Ingredient.CODEC.optionalFieldOf("addition").forGetter(recipe -> recipe.addition),
ENCHANTMENTS_CODEC.fieldOf("enchantments").forGetter(recipe -> recipe.enchantments)
).apply(instance, EnchantingSmithingRecipe::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, EnchantingSmithingRecipe> STREAM_CODEC = StreamCodec.composite(
CommonInfo.STREAM_CODEC, recipe -> recipe.commonInfo,
Ingredient.OPTIONAL_CONTENTS_STREAM_CODEC, recipe -> recipe.template,
Ingredient.CONTENTS_STREAM_CODEC, recipe -> recipe.base,
Ingredient.OPTIONAL_CONTENTS_STREAM_CODEC, recipe -> recipe.addition,
ENCHANTMENTS_STREAM_CODEC, recipe -> recipe.enchantments,
EnchantingSmithingRecipe::new
);
public static final RecipeSerializer<EnchantingSmithingRecipe> SERIALIZER = new RecipeSerializer<>(MAP_CODEC, STREAM_CODEC);
private final Optional<Ingredient> template;
private final Ingredient base;
private final Optional<Ingredient> addition;
private final Object2IntOpenHashMap<Holder<Enchantment>> enchantments;
public EnchantingSmithingRecipe(final Recipe.CommonInfo commonInfo, final Optional<Ingredient> template, final Ingredient base, final Optional<Ingredient> addition, final Object2IntOpenHashMap<Holder<Enchantment>> enchantments) {
super(commonInfo);
this.template = template;
this.base = base;
this.addition = addition;
this.enchantments = enchantments;
}
@Override
public ItemStack assemble(SmithingRecipeInput input) {
return applyEnchantments(input.base(), this.enchantments);
}
public static ItemStack applyEnchantments(ItemStack base, Object2IntOpenHashMap<Holder<Enchantment>> enchantments) {
ItemStack result = base.copy();
EnchantmentHelper.updateEnchantments(result, mutable -> enchantments.forEach(mutable::upgrade));
return result;
}
@Override
public RecipeSerializer<? extends SimpleSmithingRecipe> getSerializer() {
return SERIALIZER;
}
@Override
public Optional<Ingredient> templateIngredient() {
return this.template;
}
@Override
public Ingredient baseIngredient() {
return this.base;
}
@Override
public Optional<Ingredient> additionIngredient() {
return this.addition;
}
@Override
protected PlacementInfo createPlacementInfo() {
return PlacementInfo.createFromOptionals(List.of(this.template, Optional.of(this.base), this.addition));
}
@Override
public List<RecipeDisplay> display() {
SlotDisplay base = this.base.display();
SlotDisplay material = Ingredient.optionalIngredientToDisplay(this.addition);
SlotDisplay template = Ingredient.optionalIngredientToDisplay(this.template);
return List.of(new SmithingRecipeDisplay(template, base, material, new EnchantingSmithingDemoSlotDisplay(base, material, this.enchantments), new SlotDisplay.ItemSlotDisplay(Items.SMITHING_TABLE)));
}
}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
76
77
78
79
80
81
82
83
84
85
86
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
76
77
78
79
80
81
82
83
84
85
86
Wow, a lot of text again. This seems to be a common reoccurrence in the recipe docs (haha). Let's try to figure out what's going on.
The first few lines include our Codecs, MapCodecs, and StreamCodecs for serializing and syncing the recipe's details. We use an Object2IntOpenHashMap for the enchantments so we can map arbitrary enchantments to a level.
Following the serialization section, we have the aforementioned template, base, and addition, but instead of an ItemStackTemplate result, we have an Object2IntOpenHashMap enchantments.
The assemble method is the core of the custom recipe, and provides the output ItemStack for when the recipe is crafted. In this case, we use a helper method from EnchantmentHelper to apply the enchantments from our map.
Our PlacementInfo mainly assists in recipe placing via the Recipe Book, while our RecipeDisplay helps display the recipes in the Recipe Book.
An Aside: Slot Displays
If you tried to make your own override of display, you would quickly notice that you wouldn't be able to make a SlotDisplay of your result, because you have a dynamic result based off your base, which is an Ingredient that you can't easily get ItemStacks from. However, we have given a valid override of display in our recipe class. What's going on?
java
public record EnchantingSmithingDemoSlotDisplay(SlotDisplay base, SlotDisplay material, Object2IntOpenHashMap<Holder<Enchantment>> enchantments) implements SlotDisplay {
public static final MapCodec<EnchantingSmithingDemoSlotDisplay> MAP_CODEC = RecordCodecBuilder.mapCodec(
(instance) -> instance.group(
SlotDisplay.CODEC.fieldOf("base").forGetter(display -> display.base),
SlotDisplay.CODEC.fieldOf("material").forGetter(display -> display.material),
EnchantingSmithingRecipe.ENCHANTMENTS_CODEC.fieldOf("enchantments").forGetter(display -> display.enchantments)
).apply(instance, EnchantingSmithingDemoSlotDisplay::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, EnchantingSmithingDemoSlotDisplay> STREAM_CODEC = StreamCodec.composite(
SlotDisplay.STREAM_CODEC, display -> display.base,
SlotDisplay.STREAM_CODEC, display -> display.material,
EnchantingSmithingRecipe.ENCHANTMENTS_STREAM_CODEC, display -> display.enchantments,
EnchantingSmithingDemoSlotDisplay::new
);
public static final Type<EnchantingSmithingDemoSlotDisplay> TYPE = new Type<>(MAP_CODEC, STREAM_CODEC);
@Override
public <T> Stream<T> resolve(ContextMap context, DisplayContentsFactory<T> factory) {
RandomSource randomSource = RandomSource.createThreadLocalInstance(System.identityHashCode(this));
BinaryOperator<ItemStack> transformation = (base, material) -> EnchantingSmithingRecipe.applyEnchantments(base, this.enchantments);
return SlotDisplayAccessor.applyDemoTransformation(context, factory, this.base, this.material, randomSource, transformation);
}
@Override
public Type<? extends SlotDisplay> type() {
return TYPE;
}
}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
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
We have created a custom implementation of SlotDisplay. This particular implementation allows for displaying the result with the desired enchantments.
In our resolve method, we first create a RandomSource and a BinaryOperator<ItemStack>, then we pass both into SlotDisplay.applyDemoTransformation (it's static but private so we need a mixin invoker).
java
@Mixin(SlotDisplay.class)
public interface SlotDisplayAccessor {
@Invoker("applyDemoTransformation")
static <T> Stream<T> applyDemoTransformation(final ContextMap context, final DisplayContentsFactory<T> factory, final SlotDisplay firstDisplay, final SlotDisplay secondDisplay, final RandomSource randomSource, final BinaryOperator<ItemStack> operation) {
throw new AssertionError("Untransformed @Accessor");
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
applyDemoTransformation allows for applying changes to the ItemStack being displayed in the SlotDisplay. It takes a BinaryOperator<ItemStack> so one can modify the base's data based on the material. This is useful for recipes such as trim recipes, where the trim color of the result varies depending on the material. However, we directly apply our enchantments to the base stack while ignoring the material (the recipe simply checks if the correct material is present before allowing the assembly), so we can actually omit the material field in our SlotDisplay implementation (SlotDisplay.Empty.INSTANCE would then be passed into applyDemoTransformation in place of the material).
Finally, we need to register our recipe serializer and slot display type.
java
Registry.register(BuiltInRegistries.RECIPE_SERIALIZER, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "smithing_enchanting"), EnchantingSmithingRecipe.SERIALIZER);
Registry.register(BuiltInRegistries.SLOT_DISPLAY, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "enchanting_smithing"), EnchantingSmithingDemoSlotDisplay.TYPE);1
2
2
INFO
This recipe is still data-driven.
json
{
"type": "example-mod:smithing_enchanting",
"template": "minecraft:netherite_upgrade_smithing_template",
"base": "minecraft:netherite_sword",
"addition": "minecraft:nether_star",
"enchantments": {
"minecraft:sharpness": 10,
"minecraft:smite": 10,
"minecraft:bane_of_arthropods": 10
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11

Crafting Table
A similar situation occurs when making a new crafting recipe. The expected type is the interface CraftingRecipe, and if ShapedRecipe and ShapelessRecipe aren't enough, then we recommend you extend CustomRecipe instead. We encourage you to look through the subtypes of the recipe interface of your target workstation to see if you can find one that fits your needs.
As an example, let's create a custom crafting recipe that allows potions to be infused into suspicious stew.
java
// Thank you to lynndova for the recipe name
public class StewSpikingCraftingRecipe extends CustomRecipe {
public static final StewSpikingCraftingRecipe INSTANCE = new StewSpikingCraftingRecipe();
public static final MapCodec<StewSpikingCraftingRecipe> CODEC = MapCodec.unit(INSTANCE);
public static final StreamCodec<RegistryFriendlyByteBuf, StewSpikingCraftingRecipe> STREAM_CODEC = StreamCodec.unit(INSTANCE);
public static final RecipeSerializer<StewSpikingCraftingRecipe> SERIALIZER = new RecipeSerializer<>(CODEC, STREAM_CODEC);
@Override
public boolean matches(CraftingInput input, Level level) {
return getItemsFromInput(input) != null;
}
@Override
public ItemStack assemble(CraftingInput input) {
StewAndPotions ingredients = getItemsFromInput(input);
if (ingredients == null) {
return ItemStack.EMPTY;
}
ItemStack suspiciousStew = ingredients.stew().copy();
SuspiciousStewEffects originalEffects = suspiciousStew.getOrDefault(DataComponents.SUSPICIOUS_STEW_EFFECTS, SuspiciousStewEffects.EMPTY);
// We use a TreeMap to hopefully correctly compare Holder<MobEffect>s.
Map<Holder<MobEffect>, Integer> effects = new TreeMap<>(
(one, two) -> {
//noinspection deprecation
if (one == two || one.is(two)) {
return 0;
}
return one.toString().compareTo(two.toString());
}
);
originalEffects.effects().forEach(entry -> effects.put(entry.effect(), entry.duration()));
for (ItemStack potion : ingredients.potions()) {
float durationScale = potion.getOrDefault(DataComponents.POTION_DURATION_SCALE, 1F);
PotionContents potionContents = potion.get(DataComponents.POTION_CONTENTS);
// This is fine because we checked for the presence of the component in getItemsFromInput.
//noinspection DataFlowIssue
potionContents.getAllEffects().forEach(instance -> {
Holder<MobEffect> effect = instance.getEffect();
int duration = effects.getOrDefault(effect, 0);
if (duration == MobEffectInstance.INFINITE_DURATION) {
return;
}
duration = Math.max(Mth.floor(instance.getDuration() * (instance.getAmplifier() + 1) * durationScale), duration);
effects.put(effect, duration);
});
}
suspiciousStew.set(
DataComponents.SUSPICIOUS_STEW_EFFECTS,
new SuspiciousStewEffects(
effects.entrySet()
.stream()
.map(entry ->
new SuspiciousStewEffects.Entry(
entry.getKey(), entry.getValue()
)
)
.toList()
)
);
return suspiciousStew;
}
@Nullable
public static StewAndPotions getItemsFromInput(CraftingInput input) {
List<ItemStack> items = input.items();
if (items.size() <= 1) {
return null;
}
ItemStack stew = ItemStack.EMPTY;
ImmutableList.Builder<ItemStack> builder = ImmutableList.builder();
for (ItemStack stack : input.items()) {
if (stack.is(Items.SUSPICIOUS_STEW)) {
if (!stew.isEmpty()) {
/*
If the stew is not empty, then there are two suspicious stews in our input.
Therefore, we return null, as this is no longer a valid input.
*/
return null;
}
stew = stack;
} else if (stack.has(DataComponents.POTION_CONTENTS)) {
builder.add(stack);
}
}
if (stew.isEmpty()) {
return null;
}
List<ItemStack> potions = builder.build();
if (potions.isEmpty()) {
return null;
}
return new StewAndPotions(stew, potions);
}
@Override
public RecipeSerializer<? extends CustomRecipe> getSerializer() {
return SERIALIZER;
}
public record StewAndPotions(ItemStack stew, List<ItemStack> potions) {
}
}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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
As always, we need to register our recipe serializer.
java
Registry.register(BuiltInRegistries.RECIPE_SERIALIZER, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "stew_spiking"), StewSpikingCraftingRecipe.SERIALIZER);1
INFO
This recipe is still data-driven.
json
{
"type": "example-mod:stew_spiking"
}1
2
3
2
3
We only need the type so Minecraft knows we want to load the recipe.
Shhh, don't tell anyone! >:)
Stonecutter
Stonecutter recipes are separated from other Recipes in the RecipeManager/RecipeAccess because the Stonecutter needs to display and select any/all of its valid recipes given its one input (menus with recipe books are handled through ClientRecipeBook, where the server gives the necessary recipes to the client). Simply extending StonecutterRecipe (unlike the others, this is not an interface!) and overriding the assemble method should work for most use cases outside just making a stonecutting recipe JSON.


