Benutzerdefinierte Rezepttypen
Benutzerdefinierte Rezepttypen bieten die Möglichkeit, datengesteuerte Rezepte für die benutzerdefinierten Herstellungsmechaniken deines Mods zu erstellen. Als Beispiel erstellen wir einen Rezepttyp für einen Aufwertungsblock, ähnlich wie bei einem Schmiedetisch.
Erstellen einer Eingabeklasse für das Rezept
Bevor wir mit der Erstellung unseres Rezepts beginnen können, benötigen wir eine Implementierung von RecipeInput, die die Eingabeitems im Inventar unseres Blocks aufnehmen kann. Wir möchten, dass ein Aufwertungsrezept zwei Eingabeitems enthält: Ein Basisitem, das aufgewertet werden soll, und die Aufwertung selbst.
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
Die Rezeptklasse erstellen
Da wir nun eine Möglichkeit haben, die Eingabeitems zu speichern, können wir unsere Recipe-Implementierung erstellen. Implementierungen dieser Klasse stellen ein einzelnes Rezept dar, das in einem Datenpaket definiert ist. Sie sind dafür verantwortlich, die Zutaten und Vorgaben des Rezepts zu überprüfen und das in das Ergebnis zusammenzubauen.
Beginnen wir damit, das Ergebnis und die Zutaten des Rezepts zu definieren.
java
public class UpgradingRecipe implements Recipe<UpgradingRecipeInput> {
private final ItemStackTemplate result;
private final Ingredient baseItem;
private final Ingredient upgradeItem;
public UpgradingRecipe(ItemStackTemplate result, Ingredient baseItem, Ingredient upgradeItem) {
this.baseItem = baseItem;
this.upgradeItem = upgradeItem;
this.result = result;
}
public ItemStackTemplate 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
Beachte, dass wir für unsere Eingaben Ingredient-Objekte verwenden. Dadurch kann unser Rezept mehrere Items beliebig austauschbar verarbeiten.
Die Methoden implementieren
Als Nächstes implementieren wir die Methoden aus dem Rezept-Interface. Die Interessanten sind matches und assemble. Die Methode matches prüft, ob die Eingabeitems aus unserer RecipeInput-Implementierung mit unseren Zutaten übereinstimmen. Die Methode assemble erstellt dann den resultierenden ItemStack.
Um zu prüfen, ob die Zutaten übereinstimmen, können wir die Methode test unserer Zutaten verwenden.
java
@Override
public boolean matches(UpgradingRecipeInput recipeInput, Level level) {
return baseItem.test(recipeInput.baseItem()) && upgradeItem.test(recipeInput.upgradeItem());
}
@Override
public ItemStack assemble(UpgradingRecipeInput recipeInput) {
return result.create().copy();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Erstellen eines Rezept-Serialisierers
Der Rezept-Serializer verwendet einen MapCodec, um das Rezept aus JSON zu lesen, und einen StreamCodec, um es über das Netzwerk zu senden.
Wir werden RecordCodecBuilder#mapCodec verwenden, um einen Map-Codec für unser Rezept zu erstellen. Dadurch können wir die vorhandenen Codecs von Minecraft in unsere eigenen integrieren:
java
public static final MapCodec<UpgradingRecipe> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ItemStackTemplate.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
2
3
4
5
6
7
Der Stream-Codec kann auf ähnliche Weise mit StreamCodec#composite erstellt werden:
java
public static final StreamCodec<RegistryFriendlyByteBuf, UpgradingRecipe> STREAM_CODEC = StreamCodec.composite(
ItemStackTemplate.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
Nun registrieren wir den Rezept-Serializer sowie einen Rezepttyp. Das kannst du im Initialisierer deines Mods tun oder in einer separaten Klasse, deren Methode vom Initialisierer deines Mods aufgerufen wird:
java
public static final RecipeSerializer<UpgradingRecipe> UPGRADING_RECIPE_SERIALIZER = Registry.register(
BuiltInRegistries.RECIPE_SERIALIZER,
Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "upgrading"),
new RecipeSerializer<>(UpgradingRecipe.CODEC, UpgradingRecipe.STREAM_CODEC)
);
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
Zurück zu unserer Rezeptklasse: Wir können nun die Methoden hinzufügen, die die soeben registrierten Objekte zurückgeben:
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
Um unseren benutzerdefinierten Rezepttyp zu vervollständigen, müssen wir nur noch die verbleibenden Methoden placementInfo, showNotification, group und recipeBookCategory implementieren, die vom Rezeptbuch verwendet werden, um unser Rezept auf einem Bildschirm zu platzieren. Vorerst geben wir einfach PlacementInfo.NOT_PLACEABLE und null zurück, da das Rezeptbuch nicht ohne Weiteres auf modifizierte Arbeitsplätze ausgeweitet werden kann. Außerdem werden wir die Methode isSpecial überschreiben, sodass sie true zurückgibt, um zu verhindern, dass bestimmte andere, mit dem Rezeptbuch zusammenhängende Logik ausgeführt wird und Fehler protokolliert.
java
@Override
public @Nullable RecipeBookCategory recipeBookCategory() {
return null;
}
@Override
public PlacementInfo placementInfo() {
return PlacementInfo.NOT_PLACEABLE;
}
@Override
public boolean isSpecial() {
return true;
}
@Override
public boolean showNotification() {
return true;
}
@Override
public String group() {
return "upgrading";
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Ein Rezept erstellen
Unser Rezepttyp funktioniert jetzt, aber es fehlen noch zwei wichtige Dinge: Ein Rezept für unseren Rezepttyp und eine Möglichkeit, es herzustellen.
Lasst uns zuerst ein Rezept erstellen. Erstelle in deinem Ordner resources im Verzeichnis data/example-mod/recipe eine Datei mit der Dateiendung .json. Jede JSON-Datei für ein Rezept enthält einen Schlüssel "type", der auf den Rezept-Serialisierer des jeweiligen Rezepts verweist. Die übrigen Schlüssel werden durch den Codec des jeweiligen Rezept-Serialisierers definiert.
In unserem Fall sieht eine gültige Rezeptdatei wie folgt aus:
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
Ein Menü erstellen
INFO
Weitere Informationen zum Erstellen von Menüs findest du unter unter Container-Menüs.
Damit wir unser Rezept in dem GUI erstellen können, erstellen wir einen Block mit einem Menü:
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));
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
Da gibt es eine Menge zu besprechen! Dieses Menü verfügt über zwei Eingabefelder und ein Ausgabefeld.
Der Eingabecontainer ist eine anonyme Unterklasse von SimpleContainer, die bei einer Änderung ihrer Items die Methode slotsChanged des Menüs aufruft. In slotsChanged erstellen wir dann eine Instanz unserer Rezept-Eingabeklasse und füllen sie mit den beiden Eingabefeldern.
Um zu prüfen, ob es mit einem Rezept übereinstimmt, stellen wir zunächst sicher, dass wir uns auf der Serverebene befinden, da Clients nicht wissen, welche Rezepte vorhanden sind. Anschließend rufen wir den RecipeManager über serverLevel.recipeAccess() ab.
Wir rufen serverLevel.recipeAccess().getRecipeFor mit unseren Rezept-Eingaben auf, um ein Rezept zu erhalten, das den Eingaben entspricht. Wenn ein Rezept gefunden wurde, können wir das Ergebnis zum Ergebniscontainer hinzufügen oder daraus entfernen.
Um zu erkennen, wann der Benutzer das Ergebnis entnimmt, erstellen wir eine anonyme Unterklasse von Slot. Die Methode onTake unseres Menüs entfernt dann die Eingabeitems.
Um zu verhindern, dass Items gelöscht werden, ist es wichtig, die Eingaben beim Schließen des Bildschirms wieder zurückzusetzen, wie in der Methode removed gezeigt.
Rezeptsynchronisierung
INFO
Dieser Abschnitt ist optional und nur erforderlich, wenn du möchtest, dass die Clients über Rezepte informiert werden.
Wie bereits erwähnt, werden Rezepte vollständig auf dem logischen Server verarbeitet. In manchen Fällen muss ein Client jedoch wissen, welche Rezepte es gibt - ein Beispiel aus Vanilla sind Steinsägen, das die verfügbaren Rezeptoptionen für eine bestimmte Zutat anzeigen muss. Außerdem laufen die Plugins bestimmter Rezeptbetrachter, darunter JEI, auf dem logischen Client, sodass du die Rezept-Synchronisations-API von Fabric verwenden musst.
Um deine Rezepte zu synchronisieren, rufe einfach RecipeSynchronization.synchronizeRecipeSerializer in deinem Mod-Initialisierer auf und gib den Rezept-Serialisierer deines Mods an:
java
RecipeSynchronization.synchronizeRecipeSerializer(UPGRADING_RECIPE_SERIALIZER);1
Nach der Synchronisierung können die Rezepte jederzeit über den Rezeptmanager auf Client-Ebene abgerufen werden:
java
clientLevel.recipeAccess().getSynchronizedRecipes().getAllOfType(ExampleModRecipes.UPGRADING_RECIPE_TYPE);1


