数据组件

数据组件

数据组件以映射中的键值对形式,在 ItemStack 上存储数据。烟花爆炸、工具等每项数据都以实际对象保存在物品堆上,因此无需动态转换通用编码实例(如 CompoundTag、JsonElement),就能查看和操作值。

DataComponentType

每个数据组件都有对应的 DataComponentType<T>,其中 T 为组件值类型。DataComponentType 是引用所存组件值的键,也可按需包含编解码器,负责磁盘和网络读写。

现有组件列表可查看 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 通过 DataComponentType#builder 创建构建器,再调用 DataComponentType.Builder#build 构建。构建器有三个设置:persistent、networkSynchronized 和 cacheEncoding。

persistent 指定向磁盘读写组件值的 Codec;networkSynchronized 指定网络读写使用的 StreamCodec。未设置后者时,会包装 persistent 提供的 Codec,将其作为流编解码器使用。

警告 构建器至少要提供 persistent 或 networkSynchronized 之一,否则会抛出 NullPointerException。若不应通过网络发送数据,请将 networkSynchronized 设为提供默认组件值的 StreamCodec#unit。

cacheEncoding 缓存 Codec 的编码结果。组件值未改变时,后续编码会使用缓存。仅应在预期组件值很少或从不改变时启用。

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 DeferredHolder<DataComponentType<?>, 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 DeferredHolder<DataComponentType<?>, DataComponentType<ExampleRecord>> TRANSIENT_EXAMPLE = REGISTRAR.registerComponentType(
    "transient",
    builder -> builder.networkSynchronized(BASIC_STREAM_CODEC)
);

// No data will be synced across the network
public static final DeferredHolder<DataComponentType<?>, 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)
);

[21.0.0, 21.1.48]

// 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("examplemod");

public static final DeferredHolder<DataComponentType<?>, 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 DeferredHolder<DataComponentType<?>, DataComponentType<ExampleRecord>> TRANSIENT_EXAMPLE = REGISTRAR.registerComponentType(
    "transient",
    builder -> builder.networkSynchronized(BASIC_STREAM_CODEC)
);

// No data will be synced across the network
public static final DeferredHolder<DataComponentType<?>, 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 在不存在时提供默认值。

// For some DataComponentMap map

// Will get dye color if component is present
// Otherwise null
@Nullable
DyeColor color = map.get(DataComponents.BASE_COLOR);

PatchedDataComponentMap

默认 DataComponentMap 只支持读取;子类 PatchedDataComponentMap 则支持写入,包括 #set 设置组件值和 #remove 移除组件。

PatchedDataComponentMap 使用原型与补丁映射记录变化。原型是包含默认组件及其值的 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 或下文介绍的相关方法。

组件持有者

所有可持有数据组件的实例都实现 DataComponentHolder,它实际上将调用委托给 DataComponentMap 的只读方法。

// For some ItemStack stack

// Delegates to 'DataComponentMap#get'
@Nullable
DyeColor color = stack.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 上设置默认组件映射,在构造物品堆时作为原型传入。通过 Item.Properties#component 向物品添加组件。

// 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
    () -> new Item(
        new Item.Properties()
        .component(BASIC_EXAMPLE.value(), new ExampleRecord(24, true))
    )
);

若要向原版或其他 Mod 的现有物品添加数据组件,应在 Mod 事件总线监听 ModifyDefaultComponentsEvent。事件提供 modify 和 modifyMatching,用于修改相应物品的 DataComponentPatch.Builder。构建器可以 #set 组件,也可以 #remove 已有组件。

@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.value(), new ExampleRecord(10, false))
    );

    // Removes the component for any items that have a crafting item
    event.modifyMatching(
        item -> item.hasCraftingRemainingItem(),
        builder -> builder.remove(DataComponents.BUCKET_ENTITY_DATA)
    );
}

使用自定义组件持有者

要创建自定义数据组件持有者,只需实现 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(p_330402_);
    }

    // Other methods
}

DataComponentPatch 与编解码器

要将组件持久化到磁盘或通过网络发送,可以传输整个 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(1.21.1)。中文翻译依据提交 4a680cdd4d15。