Пользовательские типы рецептов 26.1.2
Узнайте, как создать собственный тип рецепта.
Пользовательские типы рецептов позволяют реализовать управляемые данными рецепты для ваших собственных механик создания предмета (крафта). В качестве примера мы создадим тип рецепта для блока-улучшателя, похожего на "Кузнечный стол".
Создание класса входных данных рецепта
Прежде чем начать создавать рецепт, вам потребуется класс, реализующий интерфейс RecipeInput. Он позволит хранить предметы на входе в инвентаре нашего блока. Для рецепта улучшения нам нужно два входных предмета: базовый предмет, который улучшается, и само улучшение.
java
public record UpgradingRecipeInput(ItemStack baseItem, ItemStack upgradeItem) implements RecipeInput {
@Override
public ItemStack getItem(int index) {
return switch (index) {
case 0 -> this.baseItem;
case 1 -> this.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
Создание класса рецепта
Теперь, когда у нас есть способ хранения входных предметов, мы можем создать реализацию Recipe. Экземпляры этого класса представляют собой отдельный рецепт, определённый в датапаке. Они отвечают за проверку ингредиентов и условий рецепта, а также за формирование результата.
Начнём с определения результата и ингредиентов рецепта:
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 this.result;
}
public Ingredient getBaseItem() {
return this.baseItem;
}
public Ingredient getUpgradeItem() {
return this.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
Обратите внимание, что мы используем объекты Ingredient для входных данных. Это позволяет нашему рецепту принимать несколько разных предметов взаимозаменяемо.
Реализация методов
Далее реализуем методы интерфейса Recipe. Наиболее интересные из них — matches и assemble. Метод matches проверяет, соответствуют ли входные предметы из нашей реализации RecipeInput ингредиентам. Метод assemble создаёт результирующий ItemStack.
Для проверки соответствия ингредиентов используем метод test наших ингредиентов.
java
@Override
public boolean matches(UpgradingRecipeInput recipeInput, Level level) {
return this.baseItem.test(recipeInput.baseItem()) && this.upgradeItem.test(recipeInput.upgradeItem());
}
@Override
public ItemStack assemble(UpgradingRecipeInput recipeInput) {
return this.result.create();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Создание сериализатора рецепта
Сериализатор рецепта использует MapCodec для чтения рецепта из JSON и StreamCodec для его передачи по сети.
Для построения кодека карты нашего рецепта мы воспользуемся методом RecordCodecBuilder#mapCodec. Он позволяет объединить существующие кодеки Minecraft в наш собственный:
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
Потоковый кодек создаётся аналогично с помощью StreamCodec#composite:
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
Теперь зарегистрируем сериализатор рецепта, а также сам тип рецепта. Это можно сделать в инициализаторе вашего мода или в отдельном классе, метод которого вызывается из инициализатора:
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
2
3
4
5
6
7
8
9
10
11
Вернёмся к нашему классу рецепта и добавим методы, возвращающие зарегистрированные объекты:
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
Чтобы завершить наш пользовательский тип рецепта, осталось реализовать методы placementInfo, showNotification, group и recipeBookCategory, которые используются книгой рецептов для размещения рецепта на экране. Пока мы просто вернём PlacementInfo.NOT_PLACEABLE и null, так как реализацию книги рецептов сложно расширить на модифицированные верстаки. Также переопределим isSpecial, возвращая true, чтобы предотвратить выполнение некоторой логики, связанной с книгой рецептов, и избежать ошибок в логах.
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
Создание рецепта
Наш тип рецепта теперь работает, но нам всё ещё не хватает двух важных вещей: самого рецепта для нашего типа и способа его создания.
Сначала создадим рецепт. В папке resources создайте JSON-файл в data/example-mod/recipe. Каждый такой файл рецепта имеет ключ "type", ссылающийся на сериализатор рецепта. Остальные ключи определяются кодеком этого сериализатора.
В нашем случае корректный файл рецепта выглядит так:
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
Создание меню
INFO
Подробнее о создании меню см. в разделе «Меню контейнеров».
Чтобы дать возможность создавать наш рецепт через графический интерфейс, мы создадим блок с меню:
java
public class UpgradingMenu extends AbstractContainerMenu {
private final Container input = new SimpleContainer(2) {
@Override
public void setChanged() {
super.setChanged();
UpgradingMenu.this.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(this.input, 0, 27, 47));
addSlot(new Slot(this.input, 1, 76, 47));
addSlot(new Slot(this.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 == this.input) {
if (this.level instanceof ServerLevel serverLevel) {
UpgradingRecipeInput recipeInput = new UpgradingRecipeInput(this.input.getItem(0), this.input.getItem(1));
Optional<RecipeHolder<UpgradingRecipe>> recipe = serverLevel.recipeAccess().getRecipeFor(ExampleModRecipes.UPGRADING_RECIPE_TYPE, recipeInput, serverLevel);
if (recipe.isPresent()) {
this.output.setItem(0, recipe.get().value().assemble(recipeInput));
this.output.setRecipeUsed(recipe.get());
} else {
this.output.clearContent();
this.output.setRecipeUsed(null);
}
}
}
}
public void onTake(Player player, ItemStack stack) {
stack.onCraftedBy(player, stack.getCount());
this.output.awardUsedRecipes(player, List.of(this.input.getItem(0), this.input.getItem(1)));
this.input.removeItem(0, stack.getCount());
this.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, this.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
Здесь много всего! Это меню имеет два входных слота и один выходной.
Входной контейнер — это анонимный подкласс SimpleContainer, который вызывает метод slotsChanged меню при изменении своих предметов. В slotsChanged мы создаём экземпляр нашего класса входных данных рецепта, заполняя его из двух входных слотов.
Чтобы проверить, соответствует ли это какому-либо рецепту, мы сначала убеждаемся, что находимся на серверном уровне, так как клиенты не знают о существовании рецептов. Затем получаем RecipeManager через serverLevel.recipeAccess().
Вызываем serverLevel.recipeAccess().getRecipeFor с нашим входным объектом, чтобы получить подходящий рецепт. Если рецепт найден, мы добавляем результат в выходной контейнер.
Чтобы отследить, когда игрок забирает результат, создаём анонимный подкласс Slot. В методе onTake нашего меню удаляем входные предметы.
Важно не забыть вернуть входные предметы обратно при закрытии экрана, как показано в методе removed, чтобы предметы не удалялись.
Также вам нужно зарегистрировать меню в реестре:
java
public static final MenuType<UpgradingMenu> UPGRADING_MENU_TYPE = Registry.register(
BuiltInRegistries.MENU,
Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "upgrading"),
new MenuType<>(UpgradingMenu::new, FeatureFlags.VANILLA_SET)
);
private static final Identifier UPGRADING_BLOCK_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "upgrading_block");
public static final UpgradingBlock UPGRADING_BLOCK = Registry.register(
BuiltInRegistries.BLOCK,
UPGRADING_BLOCK_ID,
new UpgradingBlock(BlockBehaviour.Properties.of()
.setId(ResourceKey.create(Registries.BLOCK, UPGRADING_BLOCK_ID))
)
);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
Синхронизация рецептов
INFO
Этот раздел необязателен и требуется только в том случае, если клиенту необходимо знать о рецептах.
Как упоминалось ранее, рецепты полностью обрабатываются на логическом сервере. Однако в некоторых случаях клиенту может потребоваться знать, какие рецепты существуют — например, в ванильной версии это нужно для камнерезов, которые должны отображать доступные варианты рецептов для данного ингредиента. Кроме того, плагины просмотрщиков рецептов, включая JEI, работают на логическом клиенте, поэтому вам потребуется использовать API синхронизации рецептов от Fabric.
Чтобы синхронизировать ваши рецепты, просто вызовите RecipeSynchronization.synchronizeRecipeSerializer в инициализаторе мода, передав ваш сериализатор рецепта:
java
RecipeSynchronization.synchronizeRecipeSerializer(UPGRADING_RECIPE_SERIALIZER);1
После синхронизации рецепты можно получить в любой момент через менеджер рецептов клиентского уровня:
java
clientLevel.recipeAccess().getSynchronizedRecipes().getAllOfType(ExampleModRecipes.UPGRADING_RECIPE_TYPE);1


