数据组件
数据组件是映射中的键值对,用于在注册表对象的 Holder 上存储数据。烟花爆炸、工具等每一类数据都以实际对象存储,因此可以直接查看并操作这些值,无需把 CompoundTag、JsonElement 等通用编码形式动态转换为对象。
DataComponentType
每个数据组件都关联一个 DataComponentType<T>,其中 T 是组件值的类型。它既是引用已存储组件值的键,也可以包含处理磁盘和网络读写的 codec。
已有组件列表见 DataComponents。
创建自定义数据组件
与 DataComponentType 关联的值必须实现 hashCode 和 equals,并且存储后应视为不可变。
提示 组件值很适合用 record 实现。record 的字段不可变,并且自动实现
hashCode和equals。
// A record example
public record ExampleRecord(int value1, boolean value2) {}
// A class example
public class ExampleClass {
private final int value1;
// Can be mutable, but care needs to be taken when using
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.Builder#build 构建。构建器有四个设置:persistent、networkSynchronized、cacheEncoding 和 ignoreSwapAnimation。
persistent 指定组件值在磁盘上读写所用的 Codec;networkSynchronized 指定网络读写所用的 StreamCodec。若省略后者,会包装 persistent 中的 codec 作为 StreamCodec 使用。
警告 构建器必须至少提供
persistent或networkSynchronized之一,否则抛出NullPointerException。如果不需要在网络上传输数据,可将networkSynchronized设为StreamCodec#unit,并提供默认组件值。
cacheEncoding 会缓存 codec 的编码结果,后续编码时若组件值没有变化,就使用缓存。仅当组件值预计很少变化或永不变化时使用。
ignoreSwapAnimation 会在物品具有该组件时取消切换动画。即使未设置,根据客户端物品属性,动画仍可能被取消。
DataComponentType 是注册表对象,必须注册。
// Using ExampleRecord(int, boolean)
// Only one Codec and/or StreamCodec should be used below
// Multiple are provided for an example
// Basic 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
);
// Unit stream codec if nothing should be sent across the network
public static final StreamCodec<ByteBuf, ExampleRecord> UNIT_STREAM_CODEC = StreamCodec.unit(new ExampleRecord(0, false));
// In another class
// The specialized DeferredRegister.DataComponents simplifies data component registration and avoids some generic inference issues with the `DataComponentType.Builder` within a `Supplier`
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
// The codec to read/write the data to disk
.persistent(BASIC_CODEC)
// The codec to read/write the data across the network
.networkSynchronized(BASIC_STREAM_CODEC)
);
/// Component will not be saved to disk
public static final Supplier<DataComponentType<ExampleRecord>> TRANSIENT_EXAMPLE = REGISTRAR.registerComponentType(
"transient",
builder -> builder.networkSynchronized(BASIC_STREAM_CODEC)
);
// No data will be synced across the network
public static final Supplier<DataComponentType<ExampleRecord>> NO_NETWORK_EXAMPLE = REGISTRAR.registerComponentType(
"no_network",
builder -> builder
.persistent(BASIC_CODEC)
// Note we use a unit stream codec here
.networkSynchronized(UNIT_STREAM_CODEC)
);
组件映射
所有数据组件都存储在 DataComponentMap 中,以 DataComponentType 为键,以对象为值。它的行为类似只读 Map:可传入类型调用 #get,也可用 #getOrDefault 在组件不存在时返回默认值。
注册表对象的组件映射可通过 Holder#components 获得。
// For some Item item
// Will get dye color if component is present
// Otherwise null
@Nullable
DyeColor color = item.builtInRegistryHolder().components().get(DataComponents.BASE_COLOR);
PatchedDataComponentMap
默认的 DataComponentMap 仅支持读取,写入则由子类 PatchedDataComponentMap 提供,包括通过 #set 设置组件值,以及用 #remove 移除组件。
它用原型与补丁映射记录变化。原型是包含默认组件及其值的 DataComponentMap;补丁映射则从 DataComponentType 映射到 Optional 值,存储相对于默认组件的修改。
// For some PatchedDataComponentMap map
// Sets the base color to white
map.set(DataComponents.BASE_COLOR, DyeColor.WHITE);
// Removes the base color by
// - Removing the patch if no default is provided
// - Setting an empty optional if there is a default
map.remove(DataComponents.BASE_COLOR);
重要警告 原型和补丁映射都会参与
PatchedDataComponentMap的哈希值计算。因此,映射中的组件值应视为不可变。修改组件值后,必须调用#set或下文中间接调用它的方法。
组件读取接口
能够提供数据组件的实例通常实现 DataComponentGetter。它负责取得组件值,既可以从底层映射读取,也可以即时构建。
组件持有者
引用底层组件映射的实例实现 DataComponentHolder,该接口继承 DataComponentGetter,实质上把只读操作委托给 DataComponentMap。
// For some DataComponentHolder holder
// Delegates to 'DataComponentMap#get'
@Nullable
DyeColor color = holder.get(DataComponents.BASE_COLOR);
MutableDataComponentHolder
NeoForge 提供 MutableDataComponentHolder 接口,为组件映射增加写入方法。原版及 NeoForge 的所有实现均使用 PatchedDataComponentMap 存储组件,因此 #set 和 #remove 也会委托给同名方法。
它还提供 #update:先获取组件值(若未设置则取指定默认值),对值执行操作,再把结果设置回映射。操作器可以是 UnaryOperator,接收并返回组件值;也可以是 BiFunction,接收组件值和另一个对象,再返回组件值。
// For some ItemStack stack
FireworkExplosion explosion = stack.get(DataComponents.FIREWORK_EXPLOSION);
// Modifying the component value
explosion = explosion.withFadeColors(new IntArrayList(new int[] {1, 2, 3}));
// Since we modified the component value, 'set' should be called afterward
stack.set(DataComponents.FIREWORK_EXPLOSION, explosion);
// Update the component value (calls 'set' internally)
stack.update(
DataComponents.FIREWORK_EXPLOSION,
// Default value if no component value is present
FireworkExplosion.DEFAULT,
// Return a new FireworkExplosion to set
explosion -> explosion.withFadeColors(new IntArrayList(new int[] {4, 5, 6}))
);
stack.update(
DataComponents.FIREWORK_EXPLOSION,
// Default value if no component value is present
FireworkExplosion.DEFAULT,
// An object that is supplied to the function
new IntArrayList(new int[] {7, 8, 9}),
// Return a new FireworkExplosion to set
FireworkExplosion::withFadeColors
);
为物品添加默认数据组件
可变组件虽然存储于 ItemStack,但默认组件映射可通过 Item 设置,先存储于 Holder<Item>,再在构造堆叠时作为原型传入。可通过 Item.Properties#component 添加组件。对于依赖动态生成数据的组件,例如数据包注册表对象,应改用 Item.Properties#delayedComponent,根据注册表的 HolderLookup.Provider 构建值。
// For some DeferredRegister.Items REGISTRAR
public static final Item COMPONENT_EXAMPLE = REGISTRAR.register("component",
// register is used over other overloads as the DataComponentType has not been registered yet
registryName -> new Item(
new Item.Properties()
.setId(ResourceKey.create(Registries.ITEM, registryName))
// Passes in the direct component value.
.component(BASIC_EXAMPLE.get(), new ExampleRecord(24, true))
// Passes in a component factory, taking in the registry context and returning the value.
.delayedComponent(DataComponents.DAMAGE_RESISTANT, context -> new DamageResistant(context.getOrThrow(DamageTypeTags.IS_EXPLOSION)))
)
);
若要给原版或其他 Mod 的已有物品添加组件,应在 Mod 事件总线监听 ModifyDefaultComponentsEvent。事件的 modify 和 modifyMatching 方法允许修改对应物品的 DataComponentPatch.Builder。构建器可以通过 #set 设置现有组件;设为 null 则相当于移除组件。
@SubscribeEvent // on the mod event bus
public static void modifyComponents(ModifyDefaultComponentsEvent event) {
// Sets the component on melon seeds
event.modify(Items.MELON_SEEDS, builder ->
builder.set(BASIC_EXAMPLE.get(), new ExampleRecord(10, false))
);
// Removes the component for any items that have a crafting remainder
event.modifyMatching(
(item, components) -> item.getCraftingRemainder() != null,
builder -> builder.set(DataComponents.BUCKET_ENTITY_DATA, null)
);
}
使用自定义组件持有者
创建自定义组件持有者时,让对象实现 MutableDataComponentHolder 并补齐所需方法即可。对象必须包含表示 PatchedDataComponentMap 的字段,以实现对应操作。
public class ExampleHolder implements MutableDataComponentHolder {
private int data;
private final PatchedDataComponentMap components;
// Overloads can be provided to supply the map itself
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);
}
// Other methods
}
DataComponentPatch 与 codec
持久化组件或通过网络发送数据时,可以发送整个 DataComponentMap,但接收位置通常已有默认值,因此这样会传输多余信息。更合适的方式是使用 DataComponentPatch:只发送组件映射的补丁信息,不包含默认值,然后由接收方将补丁应用到原型。
调用 PatchedDataComponentMap#patch 可以创建 DataComponentPatch。反过来,PatchedDataComponentMap#fromPatch 可以根据原型 DataComponentMap 和 DataComponentPatch 构造映射。
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(
// The prototype map to apply to
DataComponentMap.EMPTY,
// The associated patches
patch
);
}
// ...
}
持有者数据的网络同步以及磁盘读写需要自行实现。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。