Розширення стандартних рецептів 26.2
Дізнайтеся, як створювати власні рецепти для вже наявних робочих станків.
Якщо ви намагаєтеся додати рецепт для наявного робочого станка(наприклад, ковальського стола, верстака чи каменеріза), вам, ймовірно, потрібно лише створити клас рецепта, реалізувати його методи, зареєструвати серіалізатор і створити JSON-файл(и) рецепта, оскільки логіку блока, меню та екрана вже реалізовано Mojang. Розгляньмо кілька прикладів.
Огляд
Кожен стандартний робочий станок має власний RecipeType, визначений в інтерфейсі RecipeType. Для функціонування кожен робочий станок потребує певного підтипу Recipe.
WARNING
Зауважте: якщо ви не зміните базове меню, ваші рецепти обмежуватимуться вхідними та вихідними даними, які воно передбачає. Наприклад, ковальський стіл має три вхідні слоти й один вихідний (у стандартній грі це зазвичай Optional<Ingredient> template, Ingredient base, Optional<Ingredient> addition та ItemStackTemplate result). Однак у межах класу Recipe ви маєте значну свободу в налаштуванні вхідних даних для отримання результатів.
Ковальський стіл
Створімо новий тип ковальського рецепта, який накладає зачарування на вихідний предмет для отримання кінцевого результату.
Ковальський стіл очікує на будь-яку реалізацію інтерфейсу SmithingRecipe, яка повертає RecipeTypes.SMITHING. Створюючи новий SmithingRecipe, можна просто створити новий клас і реалізувати інтерфейс SmithingRecipe, але іншим прийнятним варіантом є успадкування від SimpleSmithingRecipe — стандартного класу гри, який уже реалізує 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
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
Ого, знову купа тексту. Схоже, це часто повторюється в документації до рецептів (ха-ха). Спробуймо розібратися, що відбувається.
Перші кілька рядків містять наші Codec, MapCodec та StreamCodec для серіалізації та синхронізації даних рецепта. Для зачарувань ми використовуємо Object2IntOpenHashMap, щоб можна було зіставити довільні зачарування з рівнем.
Після розділу серіалізації йдуть згадані раніше template, base та addition, але замість ItemStackTemplate result ми маємо Object2IntOpenHashMap enchantments.
Метод assemble є основою власного рецепта та забезпечує отримання вихідного ItemStack під час його виконання. У цьому випадку ми використовуємо допоміжний метод із класу EnchantmentHelper, щоб застосувати зачарування з нашої мапи.
Наш PlacementInfo переважно допомагає розміщувати рецепти в книзі рецептів, тоді як RecipeDisplay допомагає показувати їх у цій книзі.
Відступ: Покази слота
Якби ви спробували створити власну реалізацію методу display, то швидко помітили б, що не зможете створити SlotDisplay для отриманого результату. Це пов’язано з тим, що ваш результат є динамічним і залежить від base з Ingredient, з якого не так просто отримати ItemStack. Однак у нашому класі рецепта ми надали коректне перевизначення методу display. Що тут діється?
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
Ми створили власну реалізацію SlotDisplay. Ця конкретна реалізація дозволяє показувати результат із бажаними зачаруваннями.
У нашому методі resolve ми спочатку створюємо RandomSource і BinaryOperator<ItemStack>, а потім передаємо їх обидва до SlotDisplay.applyDemoTransformation (цей метод є статичним, але приватним, тому нам потрібен викликач міксина).
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 дозволяє застосовувати зміни до ItemStack, що показуються в SlotDisplay. Він приймає BinaryOperator<ItemStack>, що дозволяє змінювати дані base на основі material. Це корисно для таких рецептів, як рецепти орнаментів, де колір орнаменту готового виробу змінюється залежно від матеріалу. Однак ми застосовуємо наші зачарування безпосередньо до базового стосу, ігноруючи матеріал (рецепт лише перевіряє наявність потрібного матеріалу перед дозволом на створення), тому ми фактично можемо опустити поле material у нашій реалізації SlotDisplay (у такому разі замість material до методу applyDemoTransformation передаватиметься SlotDisplay.Empty.INSTANCE).
Нарешті, нам потрібно зареєструвати наш серіалізатор рецептів і тип показу слота.
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
Цей рецепт і досі керується даними.
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

Верстак
Подібна ситуація виникає під час створення нового рецепта для майстрування. Очікуваним типом є інтерфейс CraftingRecipe, і якщо ShapedRecipe та ShapelessRecipe недостатньо, ми рекомендуємо натомість розширити клас CustomRecipe. Радимо переглянути підтипи інтерфейсу рецептів на цільовому робочому станку, щоб знайти той, що відповідає вашим потребам.
Як приклад, створимо власний рецепт, що дозволяє додавати зілля до підозрілої юшки.
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
Як завжди, нам потрібно зареєструвати наш серіалізатор рецептів.
java
Registry.register(BuiltInRegistries.RECIPE_SERIALIZER, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "stew_spiking"), StewSpikingCraftingRecipe.SERIALIZER);1
INFO
Цей рецепт і досі керується даними.
json
{
"type": "example-mod:stew_spiking"
}1
2
3
2
3
Нам потрібен лише тип, щоб Minecraft знав, що ми хочемо завантажити рецепт.
Тс-с-с, нікому не кажи! >:)
Каменеріз
Рецепти для каменеріза відокремлені від інших рецептів у класах RecipeManager та RecipeAccess, оскільки каменетес повинен показувати та дозволяти вибір будь-якого з доступних для нього рецептів на основі одного вхідного предмета (обробка меню з книгами рецептів здійснюється через ClientRecipeBook, де сервер передає клієнту необхідні рецепти). Просте успадкування від StonecutterRecipe (на відміну від інших, це не інтерфейс!) та перевизначення методу assemble має спрацювати для більшості сценаріїв використання, що виходять за межі простого створення JSON-файлу рецепта для каменеріза.


