配方中的流体

配方中的流体

Mod 经常需要在配方中使用流体。NeoForge 为此提供 FluidStack 和 FluidIngredient 系统,它们分别尽量沿用 ItemStack 和 Ingredient 的设计。如果使用过后两者,本页的大多数概念都很容易理解。

FluidStack

与 ItemStack 类似,FluidStack 由三个主要部分组成:

  • 它表示的 Fluid。
    • 虽然源流体和流动流体都可使用,但通常只应使用源流体,避免让玩家困惑。
  • 数量。
  • 数据组件映射。

这些部分的工作方式大体与 ItemStack 一致:Fluid 对应 ItemStack 中的 Item,流体数量对应物品堆数量。

警告 与 ItemStack 不同,FluidStack 必须设置数量。流体单位为毫桶(mB),1 桶(B)等于 1000 mB;可通过常量 FluidType.BUCKET_VOLUME 获取这一数值。

此外,下列行为也与 ItemStack 相似:

  • 使用 new FluidStack(fluid, amount) 或 new FluidStack(fluid, amount, dataComponents) 创建。
  • FluidStack 是可变对象。
  • 提供 FluidStack#copy() 和 #copyWithAmount()。
  • 需要空值时应使用 FluidStack.EMPTY。
  • 数据生成中可以使用 FluidStackTemplate,用法与 ItemStackTemplate 类似。
  • FluidStackTemplate 有对应的 JSON 表示:
{
    // The fluid ID. Required.
    "id": "minecraft:water",
    // The fluid stack amount. 1000 is one bucket.
    "amount": 1000,
    // A map of data components. Optional, defaults to an empty map.
    "components": {
        "minecraft:enchantment_glint_override": true
    }
}

FluidIngredient

FluidIngredient 对 Fluid 和 FluidStack 的作用,与 Ingredient 对 Item 和 ItemStack 的作用一致。它实现 Predicate<FluidStack>,可以调用 #test(FluidStack) 检查给定流体堆是否匹配。

与物品 Ingredient 一样,调用 FluidIngredient#of() 的重载即可创建简单的流体原料:

  • FluidIngredient.of() 返回空流体原料。
  • FluidIngredient.of(Fluids.WATER, Fluids.LAVA) 接受水或熔岩。参数为可变参数 Fluid...,可以传入任意数量的 Fluid。
  • FluidIngredient.of(Stream.of(Fluids.WATER)) 与上一个方法作用相同,但接受 Stream<Fluid> 而非 Fluid...。
  • FluidIngredient.of(new FluidStack(Fluids.WATER), new FluidStack(Fluids.LAVA)) 与 Fluid... 版本相同,只是改为接受 FluidStack...。这是从流体堆中提取 Fluid 的便捷方法;如果还需要匹配数据组件,应改用下文的 DataComponentFluidIngredient。
  • FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)) 接受指定标签中的任意流体。

同样,还有一些与物品 Ingredient 相似的专用实现:

  • CustomDisplayFluidIngredient.of(FluidIngredient.of(Fluids.WATER), SlotDisplay.Empty.INSTANCE) 创建带有自定义 SlotDisplay 的原料,由你提供的显示对象决定客户端如何处理槽位渲染。
  • CompoundFluidIngredient.of(FluidIngredient.of(Fluids.WATER)) 创建包含多个子原料的组合原料,子原料通过构造方法的可变参数传入。任意子原料匹配即视为匹配。
  • DataComponentFluidIngredient.of(true, new FluidStack(Fluids.WATER)) 除流体外还匹配数据组件。布尔参数为 true 时严格匹配,为 false 时部分匹配。严格匹配要求组件完全一致;部分匹配只要求指定组件一致,允许存在其他组件。#of 还有其他重载,可指定多个 Fluid 或使用其他选项。
  • DifferenceFluidIngredient.of(FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)), FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.LAVA))) 匹配第一个原料接受、但第二个原料不接受的流体。
  • IntersectionFluidIngredient.of(FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)), FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.LAVA))) 匹配两个子原料同时接受的流体。

自定义流体原料

可以继承 FluidIngredient 添加自己的流体原料类型。沿用原料页面的示例,下面添加一种流体原料:仅当 FluidStack 属于指定标签且具有指定附魔时才匹配:

public class MinEnchantedFluidIngredient extends FluidIngredient {
    private final TagKey<Fluid> tag;
    private final Map<Holder<Enchantment>, Integer> enchantments;
    // The codec for serializing the ingredient.
    public static final MapCodec<MinEnchantedFluidIngredient> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
            TagKey.codec(Registries.FLUID).fieldOf("tag").forGetter(e -> e.tag),
            Codec.unboundedMap(Enchantment.CODEC, Codec.INT)
                    .optionalFieldOf("enchantments", Map.of())
                    .forGetter(e -> e.enchantments)
    ).apply(inst, MinEnchantedFluidIngredient::new));
    // Create a stream codec for the ingredient. For our use case, creating one from the regular codec will suffice.
    public static final StreamCodec<RegistryFriendlyByteBuf, MinEnchantedFluidIngredient> STREAM_CODEC =
            ByteBufCodecs.fromCodecWithRegistries(CODEC.codec());

    // Constructor that initializes the fields. You may also use a #of() pattern or similar instead.
    public MinEnchantedFluidIngredient(TagKey<Fluid> tag, Map<Holder<Enchantment>, Integer> enchantments) {
        this.tag = tag;
        this.enchantments = enchantments;
    }

    // Check if the passed FluidStack matches our requirements.
    @Override
    public boolean test(FluidStack stack) {
        return stack.is(tag) && enchantments.keySet()
                .stream()
                .allMatch(ench -> stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY)
                        .getLevel(ench) >= enchantments.get(ench));
    }

    // Determines whether this fluid ingredient performs 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 fluids that match this ingredient. Mostly for display purposes.
    // See the Ingredient docs for things to consider here, they apply 1:1.
    @Override
    protected Stream<Holder<Fluid>> generateFluids() {
        return BuiltInRegistries.FLUID.getOrThrow(tag).stream();
    }

    // FluidIngredient requires implementations of hashCode() and equals().
    @Override
    public int hashCode() {
        return Objects.hash(tag, enchantments);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof MinEnchantedFluidIngredient that)) return false;
        return Objects.equals(this.tag, that.tag)
            && Objects.equals(this.enchantments, that.enchantments);
    }
}

然后注册 FluidIngredientType:

public static final DeferredRegister<FluidIngredientType<?>> FLUID_INGREDIENT_TYPES =
        DeferredRegister.create(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPE, ExampleMod.MOD_ID);

public static final Supplier<FluidIngredientType<MinEnchantedFluidIngredient>> MIN_ENCHANTED =
        FLUID_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 FluidIngredientType<>(
                    MinEnchantedFluidIngredient.CODEC,
                    MinEnchantedFluidIngredient.STREAM_CODEC));

最后,还需要在原料类中重写 #getType:

public class MinEnchantedFluidIngredient extends FluidIngredient {
    // other stuff here

    @Override    
    public FluidIngredientType<?> getType() {
        return MIN_ENCHANTED.get();
    }
}

JSON 表示

流体原料的 JSON 表示也沿用了物品原料的设计。以自定义原料为例:

{
    "neoforge:fluid_ingredient_type": "examplemod:min_enchanted",
    "tag": "c:water",
    "enchantments": {
        "minecraft:sharpness": 4
    }
}

“普通”流体原料同样沿用原版格式:不指定 type,而是直接序列化为字符串:

"minecraft:water" // fluid
"#c:water"        // fluid tag

这些“普通”流体原料在代码中由 SimpleFluidIngredient 表示。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。