原料

原料

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。