Skip to main content
Version: 1.21.4

数据组件(Data Components)

数据组件(Data Components)是存储在 ItemStack 上的键值对(key-value pairs)映射。每一项数据,比如烟花爆炸效果或工具属性,都会作为实际对象存储在栈(stack)上,这样可以直接读取和操作这些值,无需动态转换通用的编码实例(例如 CompoundTagJsonElement)。

DataComponentType

每个数据组件都有一个关联的 DataComponentType<T>,其中 T 是组件值的类型。DataComponentType 代表一个用于引用已存储组件值的键(key),并附带一些编解码器(codec),用于在需要时处理磁盘和网络的读写操作。

可以在 DataComponents 中找到现有组件的列表。

创建自定义数据组件(Creating Custom Data Components)

DataComponentType 关联的组件值必须实现 hashCodeequals 方法,并且在存储时应当被视为不可变(immutable)。

note

组件值非常容易通过 record 实现。record 字段是不可变的,并且自动实现了 hashCodeequals 方法。

// 一个 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 提供了三个设置项:persistentnetworkSynchronizedcacheEncoding

persistent 用于指定 Codec,负责将组件值读写到磁盘。networkSynchronized 用于指定 StreamCodec,负责在网络上传输组件。如果未指定 networkSynchronized,则会将 persistent 中提供的 Codec 包装后用作 StreamCodec

warning

builder 中必须提供 persistentnetworkSynchronized,否则会抛出 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);
danger

原型和补丁映射都参与了 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 事件。该事件提供了 modifymodifyMatching 方法,可以用来修改相关物品的 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
);
}

// ...
}

在网络中同步持有者数据 以及将数据读写到磁盘,都需要手动完成。