扩展原版配方 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 动态生成的,而 base 是一个无法轻松从中获取 ItemStack 的 Ingredient。 不过,我们在配方类中提供了一个有效的 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(它是静态的但为私有,所以我们需要一个 Mixin invoker)。
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 允许对在 SlotDisplay 中显示的 ItemStack 应用变更。 它接收一个 BinaryOperator<ItemStack>,以便可以根据 material 修改 base 的数据。 这对于像纹饰配方这样的情况很有用,在此类配方中,结果的纹饰颜色会根据材料的不同而有所变化。 但是,我们直接将魔咒应用到基础物品堆上,而忽略材料(配方仅在允许合成前检查是否存在正确的材料),因此我们实际上可以在 SlotDisplay 实现中省略 material 字段(此时会传入 SlotDisplay.Empty.INSTANCE 代替 material 传给 applyDemoTransformation)。
最后,我们需要注册配方序列化器和槽位显示类型。
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 中与其他 Recipe 是分开的,因为切石机需要在给定单个输入时显示并选择其所有有效的配方(带有配方书的菜单是通过 ClientRecipeBook 处理的,服务器将必要的配方发给客户端)。 只需继承 StonecutterRecipe(与其他类不同,这并不是一个接口!) 并重写 assemble 方法,除了单纯制作切石配方 JSON 外,就能适用于绝大多数使用场景。


