Власні типи рецептів
Власні типи рецептів — це спосіб створювати керовані даними рецепти для власної механіки майстрування вашого мода. Як приклад, ми створимо тип рецепта для блока покращувача, подібного до ковальського стола.
Створення класу введення рецептів
Перш ніж ви зможете почати створювати наш рецепт, вам потрібна реалізація RecipeInput, яка може зберігати вхідні предмети в інвентарі нашого блока. Ми хочемо, щоб рецепт покращення мав два вхідні предмети: базовий покращуваний предмет та саме покращення.
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
Створення класу рецепта
Тепер, коли у нас є спосіб зберігати вхідні предмети, ми можемо створити нашу реалізацію 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
Зверніть увагу, як ми використовуємо об’єкти Ingredient для наших вхідних предметів. Це дозволяє нашому рецепту приймати кілька предметів взаємозамінно.
Реалізація методів
Далі запровадимо методи з інтерфейсу рецептів. Цікавими є методи matches і assemble. Метод matches перевіряє, чи вхідні предмети з нашої реалізації RecipeInput відповідають нашим інгредієнтам. Потім метод assemble створює кінцевий ItemStack.
Щоб перевірити, чи збігаються інгредієнти, ми можемо використати метод test наших інгредієнтів.
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
Створення серіалізатора рецептів
Серіалізатор рецептів використовує MapCodec для читання рецепта з JSON і StreamCodec для надсилання його через мережу.
Ми використаємо RecordCodecBuilder#mapCodec, щоб створити мапу кодека для нашого рецепта. Це дозволяє нам об’єднати наявні кодеки Minecraft у наші власні:
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
Кодек потоку можна створити подібним чином за допомогою 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
Використаймо ці кодеки для реалізації методів із 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
Тепер ми зареєструємо серіалізатор рецепта, а також тип рецепта. Ви можете зробити це в ініціалізаторі вашого мода або в окремому класі за допомогою методу, викликаного ініціалізатором вашого мода:
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
Повертаючись до нашого класу рецептів, тепер ми можемо додати методи, які повертають щойно зареєстровані об’єкти:
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 і 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;
}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
Створення рецепта
Наш тип рецепта зараз працює, але нам все ще бракує двох важливих речей: рецепта для нашого типу рецепта та способу його створення.
Спочатку створимо рецепт. У теці resources створіть файл у data/example-mod/recipe з розширенням .json. Кожен json-файл рецепта має ключ "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();
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
Тут багато чого розібрати! Це меню має два вхідні слоти та один вихідний.
Вхідний контейнер є анонімним підкласом SimpleContainer, який викликає метод slotsChanged меню, коли його предмети змінюються. У slotsChanged ми створюємо екземпляр нашого вхідного класу рецепта, заповнюючи його двома вхідними слотами.
Щоб перевірити, чи відповідає він будь-яким рецептам, ми спочатку переконаємося, що ми знаходимося на рівні сервера, оскільки клієнти не знають, які існують рецепти. Потім ми отримаємо RecipeManager через serverLevel.recipeAccess().
Ми викличемо serverLevel.recipeAccess().getRecipeFor з нашим уведенням рецепта, щоб отримати рецепт, який відповідає введеним даним. Якщо рецепт знайдено, ми можемо додати або видалити результат із результату контейнера.
Щоб визначити, коли користувач виймає результат, ми створюємо анонімний підклас Slot. Потім метод onTake нашого меню видаляє введені предмети.
Щоб запобігти видаленню предметів, важливо скинути введені дані, коли екран закрито, як показано в методі removed.
Синхронізація рецептів
INFO
Цей розділ необов’язковий і потрібен, лише якщо вам потрібно, щоб клієнти знали про рецепти.
Як згадувалося раніше, рецепти повністю обробляються на логічному сервері. Однак у деяких випадках клієнту може знадобитися знати, які існують рецепти — прикладом зі стандартної гри є каменеріз, яки повинен показувати доступні варіанти рецептів для певного інгредієнта. Крім того, плаґіни певних засобів перегляду рецептів, зокрема JEI, запускаються на логічному клієнті, вимагаючи від вас використання API синхронізації рецептів Fabric.
Щоб синхронізувати ваші рецепти, просто викличте RecipeSynchronization.synchronizeRecipeSerializer у своєму ініціалізаторі мода та надайте серіалізатор рецепта свого мода:
java
RecipeSynchronization.synchronizeRecipeSerializer(UPGRADING_RECIPE_SERIALIZER);1
Після синхронізації рецепти можна отримати в будь-який момент із менеджера рецептів рівня клієнта:
java
clientLevel.recipeAccess().getSynchronizedRecipes().getAllOfType(ExampleModRecipes.UPGRADING_RECIPE_TYPE);1


