数据组件(Data Components)
数据组件(Data Components)是存储在 ItemStack 上的键值对(key-value pairs)映射。每一项数据,比如烟花爆炸效果或工具属性,都会作为实际对象存储在栈(stack)上,这样可以直接读取和操作这些值,无需动态转换通用的编码实例(例如 CompoundTag、JsonElement)。
DataComponentType
每个数据组件都有一个关联的 DataComponentType<T>,其中 T 是组件值的类型。DataComponentType 代表一个用于引用已存储组件值的键(key),并附带一些编解码器(codec),用于在需要时处理磁盘和网络的读写操作。
可以在 DataComponents 中找到现有组件的列表。
创建自定义数据组件(Creating Custom Data Components)
与 DataComponentType 关联的组件值必须实现 hashCode 和 equals 方法,并且在存储时应当被视为不可变(immutable)。
组件值非常容易通过 record 实现。record 字段 是不可变的,并且自动实现了 hashCode 和 equals 方法。
// 一个 record 示例
public record ExampleRecord(int value1, boolean value2) {}
// 一个类的示例
public class ExampleClass {
private final int value1;
// 可以是可变的,但使用时需格外小心
private boolean value2;
public ExampleClass(int value1, boolean value2) {
this.value1 = value1;
this.value2 = value2;
}
@Override
public int hashCode() {
return Objects.hash(this.value1, this.value2);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else {
return obj instanceof ExampleClass ex
&& this.value1 == ex.value1
&& this.value2 == ex.value2;
}
}
}
可以通过 DataComponentType#builder 创建一个标准的 DataComponentType,并使用 DataComponentType.Builder#build 进行构建。该 builder 提供了三个设置项:persistent、networkSynchronized、cacheEncoding。
persistent 用于指定 Codec,负责将组件值读写到磁盘。networkSynchronized 用于指定 StreamCodec,负责在网络上传输组件。如果未指定 networkSynchronized,则会将 persistent 中提供的 Codec 包装后用作 StreamCodec。
builder 中必须提供 persistent 或 networkSynchronized,否则会抛出 NullPointerException。如果不希望该数据通过网络同步,则可将 networkSynchronized 设置为 StreamCodec#unit,并提供默认的组件值。
cacheEncoding 会缓存 Codec 的编码结果,这样如果组件值没有发生变化,后续的编码操作会直接使用缓存值。只有当组件值预期很少或几乎不会变化时,才建议使用该选项。
DataComponentType 是注册表对象(registry objects),必须先进行注册。
// 使用 ExampleRecord(int, boolean)
// 下面只应使用一个 Codec 或 StreamCodec
// 这里提供多个仅作为示例
// 基本 codec
public static final Codec<ExampleRecord> BASIC_CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.INT.fieldOf("value1").forGetter(ExampleRecord::value1),
Codec.BOOL.fieldOf("value2").forGetter(ExampleRecord::value2)
).apply(instance, ExampleRecord::new)
);
public static final StreamCodec<ByteBuf, ExampleRecord> BASIC_STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.INT, ExampleRecord::value1,
ByteBufCodecs.BOOL, ExampleRecord::value2,
ExampleRecord::new
);
// 如果无需通过网络发送任何数据,可使用单位流 codec
public static final StreamCodec<ByteBuf, ExampleRecord> UNIT_STREAM_CODEC = StreamCodec.unit(new ExampleRecord(0, false));
// 在另一个类中
// 专门的 DeferredRegister.DataComponents 简化了数据组件(data component)的注册,并避免了在 `Supplier` 中使用 `DataComponentType.Builder` 时的一些泛型推断问题
public static final DeferredRegister.DataComponents REGISTRAR = DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, "examplemod");
public static final Supplier<DataComponentType<ExampleRecord>> BASIC_EXAMPLE = REGISTRAR.registerComponentType(
"basic",
builder -> builder
// 用于将数据读写到磁盘的 codec
.persistent(BASIC_CODEC)
// 用于在网络上传输数据的 codec
.networkSynchronized(BASIC_STREAM_CODEC)
);
/// 该组件不会被保存到磁盘
public static final Supplier<DataComponentType<ExampleRecord>> TRANSIENT_EXAMPLE = REGISTRAR.registerComponentType(
"transient",
builder -> builder.networkSynchronized(BASIC_STREAM_CODEC)
);
// 不会在网络上传同步数据
public static final Supplier<DataComponentType<ExampleRecord>> NO_NETWORK_EXAMPLE = REGISTRAR.registerComponentType(
"no_network",
builder -> builder
.persistent(BASIC_CODEC)
// 注意这里我们使用了单位流 codec
.networkSynchronized(UNIT_STREAM_CODEC)
);
组件映射(The Component Map)
所有数据组 件(data components)都存储在一个 DataComponentMap 中,使用 DataComponentType 作为键,具体对象作为值。DataComponentMap 的功能类似于只读的 Map。因此,你可以通过 #get 方法根据 DataComponentType 获取对应条目,或者如果不存在则通过 #getOrDefault 提供一个默认值。
// 对于某个 DataComponentMap map
// 如果组件存在,将获取染料颜色
// 否则返回 null
@Nullable
DyeColor color = map.get(DataComponents.BASE_COLOR);
PatchedDataComponentMap
由于默认的 DataComponentMap 只提供了基于读取的操作方法,若要进行写入操作,需要使用其子类 PatchedDataComponentMap。这包括对组件进行 #set 设置值,或通过 #remove 将其移除。
PatchedDataComponentMap 通过原型(prototype)和补丁映射(patch map)来存储变更。原型是一个 DataComponentMap,其中包含了该映射应有的默认组件及其值。补丁映射则是一个将 DataComponentType 映射到 Optional 值的映射,用于记录对默认组件所做的更改。
// 对于某个 PatchedDataComponentMap map
// 将底色设置为白色
map.set(DataComponents.BASE_COLOR, DyeColor.WHITE);
// 移除底色的方式:
// - 如果没有默认值则移除补丁
// - 如果有默认值则设置为空 optional
map.remove(DataComponents.BASE_COLOR);
原型和补丁映射都参与了 PatchedDataComponentMap 的哈希值计算。因此,映射中的所有组件值都应视为不可变。在修改数据组件的值后,务必调用 #set 或下文提及的相关方法。
组件持有者(The Component Holder)
所有可以持有数据组件的实例都实现了 DataComponentHolder 接口。DataComponentHolder 实际上是对 DataComponentMap 内只读方法的委托。
// 对于某个 ItemStack stack
// 委托给 'DataComponentMap#get'
@Nullable
DyeColor color = stack.get(DataComponents.BASE_COLOR);
MutableDataComponentHolder
MutableDataComponentHolder 是 NeoForge 提供的一个接口,用于支持对组件映射进行写入操作。在 Vanilla 和 NeoForge 的所有实现中,数据组件都是通过 PatchedDataComponentMap 存储的,因此 #set 和 #remove 方法也有同名的委托方法。
此外,MutableDataComponentHolder 还提供了一个 #update 方法,用于获取组件值(或在未设置时使用提供的默认值)、对该值进行操作,并将其重新设置回映射。操作符可以是 UnaryOperator(接受组件值并返回组件值),也可以是 BiFunction(接受组件值和另一个对象,并返回组件值)。
// 对于某个 ItemStack stack
FireworkExplosion explosion = stack.get(DataComponents.FIREWORK_EXPLOSION);
// 修改组件值
explosion = explosion.withFadeColors(new IntArrayList(new int[] {1, 2, 3}));
// 由于我们修改了组件值,因此需要在之后调用 'set'
stack.set(DataComponents.FIREWORK_EXPLOSION, explosion);
// 更新组件值(内部会调用 'set')
stack.update(
DataComponents.FIREWORK_EXPLOSION,
// 如果没有组件值时的默认值
FireworkExplosion.DEFAULT,
// 返回一个新的 FireworkExplosion 用于设置
explosion -> explosion.withFadeColors(new IntArrayList(new int[] {4, 5, 6}))
);
stack.update(
DataComponents.FIREWORK_EXPLOSION,
// 如果没有组件值时的默认值
FireworkExplosion.DEFAULT,
// 传递给函数的对象
new IntArrayList(new int[] {7, 8, 9}),
// 返回一个新的 FireworkExplosion 用于设置
FireworkExplosion::withFadeColors
);
为物品添加默认数据组件(Default Data Components)
虽然数据组件(data components)是存储在 ItemStack 上的,但可以在 Item 上设置一组默认组件映射(map of default components),在构造 ItemStack 时会作为原型传递。可以通过 Item.Properties#component 方法为 Item 添加组件。
// 对于某个 DeferredRegister.Items REGISTRAR
public static final Item COMPONENT_EXAMPLE = REGISTRAR.register("component",
// 由于 DataComponentType 还未注 册,这里使用 register 的重载方法
registryName -> new Item(
new Item.Properties()
.setId(ResourceKey.create(Registries.ITEM, registryName))
.component(BASIC_EXAMPLE.get(), new ExampleRecord(24, true))
)
);
如果你需要为属于原版(Vanilla)或其他模组的现有物品添加数据组件(data component),则应当在 模组事件总线 上监听 ModifyDefaultComponentEvent 事件。该事件提供了 modify 和 modifyMatching 方法,可以用来修改相关物品的 DataComponentPatch.Builder。这个 builder 可以通过 #set 方法设置组件,也可以通过 #remove 方法移除已有组件。
// 在模组事件总线监听
@SubscribeEvent
public void modifyComponents(ModifyDefaultComponentsEvent event) {
// 为西瓜种子(melon seeds)设置组件
event.modify(Items.MELON_SEEDS, builder ->
builder.set(BASIC_EXAMPLE.get(), new ExampleRecord(10, false))
);
// 移除所有拥有合成剩余物(crafting remainder)的物品的该组件
event.modifyMatching(
item -> !item.getCraftingRemainder().isEmpty(),
builder -> builder.remove(DataComponents.BUCKET_ENTITY_DATA)
);
}
使用自定义组件持有者(Custom Component Holders)
要创建自定义的数据组件持有者(data component holder),只需让持有者对象实现 MutableDataComponentHolder 接口,并实现缺失的方法。持有者对象必须包含一个代表 PatchedDataComponentMap 的字段,以实现相关方法 。
public class ExampleHolder implements MutableDataComponentHolder {
private int data;
private final PatchedDataComponentMap components;
// 可以通过重载方法直接传入组件映射
public ExampleHolder() {
this.data = 0;
this.components = new PatchedDataComponentMap(DataComponentMap.EMPTY);
}
@Override
public DataComponentMap getComponents() {
return this.components;
}
@Nullable
@Override
public <T> T set(DataComponentType<? super T> componentType, @Nullable T value) {
// 设置组件
return this.components.set(componentType, value);
}
@Nullable
@Override
public <T> T remove(DataComponentType<? extends T> componentType) {
// 移除组件
return this.components.remove(componentType);
}
@Override
public void applyComponents(DataComponentPatch patch) {
// 应用组件补丁
this.components.applyPatch(patch);
}
@Override
public void applyComponents(DataComponentMap components) {
// 应用整个组件映射
this.components.setAll(components);
}
// 其他方法
}
DataComponentPatch 与编解码器(Codecs)
为了将组件(components)持 久化到磁盘,或在网络上传输信息,持有者(holder)可以直接发送整个 DataComponentMap。然而,这通常会造成信息冗余,因为任何默认值在数据接收端本就已经存在。因此,我们更倾向于使用 DataComponentPatch 只发送关联的数据。DataComponentPatch 只包含组件映射的补丁信息,不包含任何默认值。这些补丁随后会应用到接收端的原型(prototype)上。
可以通过 #patch 方法从 PatchedDataComponentMap 创建一个 DataComponentPatch。同样地,PatchedDataComponentMap#fromPatch 可以根据原型 DataComponentMap 和一个 DataComponentPatch 构建出新的 PatchedDataComponentMap。
public class ExampleHolder implements MutableDataComponentHolder {
public static final Codec<ExampleHolder> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.INT.fieldOf("data").forGetter(ExampleHolder::getData),
DataCopmonentPatch.CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter(holder -> holder.components.asPatch())
).apply(instance, ExampleHolder::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, ExampleHolder> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.INT, ExampleHolder::getData,
DataComponentPatch.STREAM_CODEC, holder -> holder.components.asPatch(),
ExampleHolder::new
);
// ...
public ExampleHolder(int data, DataComponentPatch patch) {
this.data = data;
this.components = PatchedDataComponentMap.fromPatch(
// 要应用补丁的原型映射
DataComponentMap.EMPTY,
// 关联的补丁数据
patch
);
}
// ...
}
在网络中同步持有者数据 以及将数据读写到磁盘,都需要手动完成。