配方中的流体
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。