原料
Ingredient 用于配方,检查给定 ItemStack 是否是有效输入。为此,Ingredient 实现了 Predicate<ItemStack>,可调用 #test 确认物品堆是否匹配。
遗憾的是,Ingredient 的许多内部实现较为混乱。NeoForge 尽可能绕开此类,引入 ICustomIngredient 接口来实现自定义原料。它并不直接替代常规 Ingredient,但可分别通过 ICustomIngredient#toVanilla 与 Ingredient#getCustomIngredient 在两者之间转换。
内置原料类型
最简单的创建方式是使用 Ingredient#of 辅助方法,它有多个变体:
Ingredient.of()返回空原料。Ingredient.of(Blocks.IRON_BLOCK, Items.GOLD_BLOCK)接受铁块或金块。参数是ItemLike可变参数,因此可使用任意数量的方块和物品。Ingredient.of(new ItemStack(Items.DIAMOND_SWORD))接受指定物品堆,但忽略数量与数据组件。Ingredient.of(Stream.of(new ItemStack(Items.DIAMOND_SWORD)))同样接受物品堆,但输入为Stream<ItemStack>,适合已经持有这类流的情况。Ingredient.of(ItemTags.WOODEN_SLABS)接受指定标签中的任意物品,例如任意木台阶。
NeoForge 另外添加了几种原料:
new BlockTagIngredient(BlockTags.CONVERTABLE_TO_MUD)类似Ingredient.of()的标签变体,但使用方块标签。它适用于需要标签而只有方块标签可用的情况,例如minecraft:convertable_to_mud。CompoundIngredient.of(Ingredient.of(Items.DIRT))包含通过可变参数传入的子原料,只要任意子原料匹配即匹配。DataComponentIngredient.of(true, new ItemStack(Items.DIAMOND_SWORD))除物品外还匹配数据组件。布尔参数表示严格匹配(true)或部分匹配(false)。严格匹配要求数据组件完全一致;部分匹配要求给定组件一致,但允许存在其他组件。#of还有多个重载,支持指定多个物品或其他选项。DifferenceIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))匹配第一个原料中不匹配第二个原料的所有内容。此例只匹配可燃木板,即不包括绯红、诡异和 Mod 添加的下界木板。IntersectionIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))匹配同时满足两个子原料的内容。此例只匹配不可燃木板,即绯红、诡异和 Mod 添加的下界木板。
注意,NeoForge 提供的这些类型是 ICustomIngredient,在原版上下文使用前必须调用 #toVanilla。
自定义原料类型
Mod 开发者可以通过 ICustomIngredient 系统添加自己的类型。下面创建一个附魔原料,它接受物品标签,以及附魔到最低等级的映射:
public class MinEnchantedIngredient implements ICustomIngredient {
private final TagKey<Item> tag;
private final Map<Holder<Enchantment>, Integer> enchantments;
// The codec for serializing the ingredient.
public static final MapCodec<MinEnchantedIngredient> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter(e -> e.tag),
Codec.unboundedMap(Enchantment.CODEC, Codec.INT)
.optionalFieldOf("enchantments", Map.of())
.forGetter(e -> e.enchantments)
).apply(inst, MinEnchantedIngredient::new));
// Create a stream codec from the regular codec. In some cases, it might make sense to define
// a new stream codec from scratch.
public static final StreamCodec<RegistryFriendlyByteBuf, MinEnchantedIngredient> STREAM_CODEC =
ByteBufCodecs.fromCodecWithRegistries(CODEC.codec());
// Allow passing in a pre-existing map of enchantments to levels.
public MinEnchantedIngredient(TagKey<Item> tag, Map<Holder<Enchantment>, Integer> enchantments) {
this.tag = tag;
this.enchantments = enchantments;
}
public MinEnchantedIngredient(TagKey<Item> tag) {
this(tag, new HashMap<>());
}
// Make this chainable for easy use.
public MinEnchantedIngredient with(Holder<Enchantment> enchantment, int level) {
enchantments.put(enchantment, level);
return this;
}
// Check if the passed ItemStack matches our ingredient by verifying the item is in the tag
// and by testing for presence of all required enchantments with at least the required level.
@Override
public boolean test(ItemStack stack) {
return stack.is(tag) && enchantments.keySet()
.stream()
.allMatch(ench -> stack.getEnchantmentLevel(ench) >= enchantments.get(ench));
}
// Determines whether this ingredient performs NBT or data component matching (false) or not (true).
// Also determines whether a stream codec is used for syncing, more on this later.
// We query enchantments on the stack, therefore our ingredient is not simple.
@Override
public boolean isSimple() {
return false;
}
// Returns a stream of items that match this ingredient. Mostly for display purposes.
// There's a few good practices to follow here:
// - Always include at least one item stack, to prevent accidental recognition as empty.
// - Include each accepted Item at least once.
// - If #isSimple is true, this should be exact and contain every item stack that matches.
// If not, this should be as exact as possible, but doesn't need to be super accurate.
// In our case, we use all items in the tag, each with the required enchantments.
@Override
public Stream<ItemStack> getItems() {
// Get a list of item stacks, one stack per item in the tag.
List<ItemStack> stacks = BuiltInRegistries.ITEM
.getOrCreateTag(tag)
.stream()
.map(ItemStack::new)
.toList();
// Enchant all stacks with all enchantments.
for (ItemStack stack : stacks) {
enchantments
.keySet()
.forEach(ench -> stack.enchant(ench, enchantments.get(ench)));
}
// Return stream variant of the list.
return stacks.stream();
}
}
自定义原料属于注册表,因此需要注册。NeoForge 提供 IngredientType 类,基本上是对 MapCodec 和可选 StreamCodec 的包装:
public static final DeferredRegister<IngredientType<?>> INGREDIENT_TYPES =
DeferredRegister.create(NeoForgeRegistries.Keys.INGREDIENT_TYPE, ExampleMod.MOD_ID);
public static final Supplier<IngredientType<MinEnchantedIngredient>> MIN_ENCHANTED =
INGREDIENT_TYPES.register("min_enchanted",
// The stream codec parameter is optional, a stream codec will be created from the codec
// using ByteBufCodecs#fromCodec or #fromCodecWithRegistries if the stream codec isn't specified.
() -> new IngredientType<>(MinEnchantedIngredient.CODEC, MinEnchantedIngredient.STREAM_CODEC));
然后还需要在原料类中重写 #getType:
public class MinEnchantedIngredient implements ICustomIngredient {
// other stuff here
@Override
public IngredientType<?> getType() {
return MIN_ENCHANTED;
}
}
这样,自定义原料类型就可以使用了。
JSON 表示
原版原料的功能较为有限,NeoForge 因此引入了全新的注册表。下面看看内置原料及自定义原料如何表示为 JSON。
指定了 type 的原料通常视为非原版原料。例如:
{
"type": "neoforge:block_tag",
"tag": "minecraft:convertable_to_mud"
}
使用自定义原料的另一个例子:
{
"type": "examplemod:min_enchanted",
"tag": "c:swords",
"enchantments": {
"minecraft:sharpness": 4
}
}
如果没有指定 type,则是原版原料。原版原料可以指定 item 或 tag 两个属性之一。
原版物品原料示例:
{
"item": "minecraft:dirt"
}
原版标签原料示例:
{
"tag": "c:ingots"
}
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。