Vanilla Rezepte erweitern 26.2
Lerne, wie du benutzerdefinierte Rezepte für bereits vorhandene Arbeitsplätze erstellen kannst.
Wenn du versuchst, ein Rezept zu einem vorhandenen Arbeitsplatz hinzuzufügen, wie beispielsweise einen Schmiedetisch, eine Werkbank oder eine Steinsäge, musst du lediglich eine Rezeptklasse erstellen, ihre Methoden implementieren, den Serialisierer registrieren und die Rezept-JSONs erstellen, da die Logik für Blöcke, Menüs und Oberflächen bereits (von Mojang) für dich fertiggestellt wurde. Lasst uns einmal einige Beispiele ansehen.
Überblick
Jeder Vanilla-Arbeitsplatz verfügt über einen eigenen RecipeType, der in dem Interface RecipeType definiert ist. Jeder Arbeitsplatz benötigt einen bestimmten Untertyp von Recipe, um zu funktionieren.
WARNING
Beachte bitte, dass deine Rezepte, außer du änderst das zugrunde liegende Menü, auf die Ein- und Ausgabe beschränkt sind, die das Menü bietet. Zum Beispiel hat ein Schmiedetisch drei Eingaben und eine Ausgabe (in Vanilla sind diese in der Regel ein Optional<Ingredient> template, Ingredient base, Optional<Ingredient> addition und ItemStackTemplate result). Allerdings hast du innerhalb der Klasse Recipe viele Freiheiten bei der Konfiguration der Eingaben, um die Ausgaben zu erzeugen.
Schmiedetisch
Lasst uns einen neuen Typ von Schmiederezept erstellen, bei dem auf die Basiseingabe Verzauberungen angewendet werden, um die Ausgabe herzustellen.
Der Schmiedetisch erwartet eine irgendeine Implementierung des Interface SmithingRecipe, die RecipeTypes.SMITHING zurückgibt. Wenn man ein neues SmithingRecipe erstellt, könnte man einfach eine neue Klasse anlegen und SmithingRecipe implementieren, aber ein weiterer gültiger Weg ist es, SimpleSmithingRecipe, eine Vanilla-Klasse, die SmithingRecipe bereits implementiert, zu erweitern.
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
87
88
89
90
91
92
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
Wow, schon wieder so viel Text. Dies scheint in den Rezeptdokumentationen ein häufiges Vorkommnis zu sein (haha). Lasst uns herausfinden, was da vor sich geht.
Die ersten Zeilen beinhalten unsere Codecs, MapCodecs und StreamCodecs zur Serialisierung und Synchronisierung der Rezeptdetails. Wir verwenden eine Object2IntOpenHashMap für die Verzauberungen, damit wir beliebige Verzauberungen einer Stufe zuordnen können.
Im Anschluss an den Abschnitt zur Serialisierung haben wir die bereits erwähnten template, base und addition, doch anstelle eines ItemStackTemplate result haben wir hier eine Object2IntOpenHashMap enchantments.
Die Methode assemble bildet den Kern des benutzerdefinierten Rezepts und bietet den ItemStack als Ergebnis, wenn das Rezept hergestellt wird. In diesem Fall verwenden wir eine Hilfsmethode aus EnchantmentHelper, um die Verzauberungen von unserer Karte anzuwenden.
Unsere PlacementInfo dient in erster Linie dazu, Rezepte über das Rezeptbuch zu platzieren, während unsere RecipeDisplay dabei hilft, die Rezepte im Rezeptbuch anzuzeigen.
Eine Randbemerkung: Slot Anzeigen
Wenn du versuchen würdest, deine eigene Überschreibung von display zu erstellen, würdest du schnell feststellen, dass du aus deinem Ergebnis kein SlotDisplay erstellen könntest, da es sich um ein dynamisches Ergebnis handelt, das auf deiner base basiert, was ein Ingredient ist, aus dem du nicht ohne Weiteres ItemStacks erhalten kannst. Allerdings haben wir in unserer Rezeptklasse eine gültige Überschreibung von display bereitgestellt. Was ist los?
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
Wir haben eine benutzerdefinierte Implementierung von SlotDisplay erstellt. Diese spezielle Implementierung ermöglicht es, das Ergebnis mit den gewünschten Verzauberungen anzuzeigen.
In unserer resolve-Methode erstellen wir zunächst eine RandomSource und einen BinaryOperator<ItemStack> und übergeben beide anschließend an SlotDisplay.applyDemoTransformation (da es sich um eine statische, aber private Methode handelt, benötigen wir einen Mixin-Aufrufer).
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 erlaubt Änderungen auf den im SlotDisplay angezeigten ItemStack anzuwenden. Es benötigt einen BinaryOperator<ItemStack>, damit man die Daten von base basierend auf material ändern kann. Dies ist beispielsweise bei Rüstungsbesatz-Rezepten nützlich, bei denen die Farbe des Besatzes je nach Material variiert. Allerdings wenden wir unsere Verzauberungen direkt auf den Basis-Stack an und ignorieren dabei das Material (das Rezept prüft einfach nur, ob das richtige Material vorhanden ist, bevor es die Zusammenstellung zulässt), sodass wir das Feld material in unserer Implementierung von SlotDisplay tatsächlich weglassen können (SlotDisplay.Empty.INSTANCE würde dann anstelle von material an applyDemoTransformation übergeben werden).
Schließlich müssen wir unseren Rezept-Serialisierer und den Slot-Anzeigetyp registrieren.
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
Dieses Rezept ist weiterhin datengetrieben.
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

Werkbank
Eine ähnliche Situation tritt auf, wenn man ein neues Handwerksrezept erstellt. Der erwartete Typ ist das Interface CraftingRecipe und wenn ShapedRecipe und ShapelessRecipe nicht ausreichen sollten, empfehlen wir, stattdessen CustomRecipe zu erweitern. Wir empfehlen dir, die Untertypen des Rezept Interface deines Ziel-Arbeitsplatz durchzuschauen, um zu prüfen, ob du einen findest, der deinen Anforderungen entspricht.
Lasst uns als Beispiel ein benutzerdefiniertes Handwerksrezept erstellen, mit dem man Tränke in eine seltsame Suppe umwandeln kann.
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);
} else {
return null;
}
}
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
121
122
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
121
122
Wie immer müssen wir unseren Rezept-Serialisierer registrieren.
java
Registry.register(BuiltInRegistries.RECIPE_SERIALIZER, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "stew_spiking"), StewSpikingCraftingRecipe.SERIALIZER);1
INFO
Dieses Rezept ist weiterhin datengetrieben.
json
{
"type": "example-mod:stew_spiking"
}1
2
3
2
3
Wir benötigen nur den Typ, damit Minecraft weiß, dass wir das Rezept laden möchten.
Pssst, sag es niemandem! >:)
Steinsäge
Rezepte für die Steinsäge sind im RecipeManager/RecipeAccess von anderen Recipes getrennt, da die Steinsäge anhand seiner einzigen Eingabe alle gültigen Rezepte anzeigen und auswählen muss (Menüs mit Rezeptbüchern werden über ClientRecipeBook behandelt, wobei der Server dem Client die erforderlichen Rezepte bereitstellt). Eine einfache Erweiterung von StonecutterRecipe (im Gegensatz zu den anderen handelt es sich hierbei nicht um eine Interface!) und das Überschreiben der Methode assemble sollte für die meisten Anwendungsfälle funktionieren, abgesehen von der reinen Erstellung von JSON für ein Steinsägerezept.


