值输入输出

值输入输出

值输入输出(Value I/O)系统是一种标准化序列化方式,用于操作底层对象中的数据,例如 NBT 的 CompoundTag。

输入与输出

系统分为两部分:ValueOutput 在序列化时向对象写入数据,ValueInput 在反序列化时从对象读取数据。相关方法通常只接受一个 ValueOutput 或 ValueInput 参数,不返回值。系统要求底层对象是由字符串键映射到对象值的字典,再通过提供的方法读写其中的信息。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    // Write data to the output
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);
    // Read data from the input
}

// For some Entity subclass
@Override
protected void addAdditionalSaveData(ValueOutput output) {
    super.addAdditionalSaveData(output);
    // Write data to the output
}

@Override
protected void readAdditionalSaveData(ValueInput input) {
    super.readAdditionalSaveData(input);
    // Read data from the input
}

基本类型

系统提供一些基本类型的读写方法。ValueOutput 方法以 put* 命名,接受键和值;ValueInput 方法通常以 get*Or 命名,接受键以及数据不存在时使用的默认值。

Java 类型 ValueOutput ValueInput
boolean putBoolean getBooleanOr
byte putByte getByteOr
short putShort getShortOr
int putInt getInt*、getIntOr
long putLong getLong*、getLongOr
float putFloat getFloatOr
double putDouble getDoubleOr
String putString getString*、getStringOr
int[] putIntArray getIntArray*

* 这些 ValueInput 方法返回以 Optional 包装的值,而不是接受并返回一个后备默认值。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output
    output.putBoolean(
        // The string key
        "boolValue",
        // The value associated with this key
        true
    );
    output.putString("stringValue", "Hello world!");
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input

    // Defaults to false if not present
    boolean boolValue = input.getBooleanOr(
        // The string key to retrieve
        "boolValue",
        // The default value to return if the key is not present
        false
    );

    // Defaults to 'Dummy!' if not present
    String stringValue = input.getStringOr("stringValue", "Dummy!");
    // Returns an optional-wrapped value
    Optional<String> stringValueOpt = input.getString("stringValue");
}

Codec

也可以通过 Codec 保存和读取数据。原版使用 RegistryOps 处理所有 codec,因此支持保存数据包条目。ValueOutput#store 和 storeNullable 接受键、用于写入的 codec,以及对象本身;对象为 null 时,storeNullable 不写入任何内容。ValueInput#read 接受键和 codec,返回以 Optional 包装的对象。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output
    output.storeNullable("codecValue", Rarity.CODEC, Rarity.EPIC);
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input
    Optional<Rarity> codecValue = input.read("codecValue", Rarity.CODEC);
}

ValueOutput 和 ValueInput 还为 MapCodec 提供 store/read 方法。与普通 Codec 相比,MapCodec 版本会将数值合并到当前根对象中。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output
    output.store(
        SingleFile.MAP_CODEC,
        new SingleFile(Identifier.fromNamespaceAndPath("examplemod", "example"))
    );
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input

    // No key is needed as they are stored on the root value access
    Optional<SingleFile> file = input.read(SingleFile.MAP_CODEC);
    // This is present as `SingleFile` writes the `resource` parameter
    String resource = input.getStringOr("resource", "Not present!");
}

警告 MapCodec 会直接向当前值访问对象写入键,可能覆盖已有数据。因此应确保其中的键与其他键不同。

列表

创建和读取列表有两种方式:使用子值输入输出,或使用 [Codec]。

调用 ValueOutput#childrenList 并传入键,可创建列表。返回的 ValueOutput.ValueOutputList 是只写的值对象列表。通过 ValueOutputList#addChild 添加子对象,它返回 ValueOutput,用于写入该子对象的数据。读取时使用 ValueInput#childrenList,或在不存在时默认返回空列表的 childrenListOrEmpty;返回的 ValueInput.ValueInputList 支持只读遍历,也可以通过 stream 获取流。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output

    // Create List
    ValueOutput.ValueOutputList listValue = output.childrenList("listValue");
    // Add elements
    ValueOutput childIdx0 = listValue.addChild();
    childIdx0.putBoolean("boolChild", false);
    ValueOutput childIdx1 = listValue.addChild();
    childIdx1.putInt("boolChild", true);
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input

    // Read values of list
    for (ValueInput childInput : input.childrenListOrEmpty("listValue")) {
        boolean boolChild = childInput.getBooleanOr("boolChild", false);
    }
}

ValueOutput#list 则提供基于 codec 的对象列表。传入键和 Codec 后,返回 ValueOutput.TypedOutputList。它与 ValueOutputList 类似,但直接操作数据对象,而非再操作另一层值输入输出。通过 TypedOutputList#add 添加元素,然后使用 ValueInput#list 或 listOrEmpty 读取,返回 TypedValueInput。

提示 TypedValueOutput/TypedValueInput 与 Codec#listOf 的主要区别在于错误处理。后者只要有一个元素失败,就会将整个对象标记为错误 DataResult;类型化值输入输出通常通过 ProblemReporter 处理错误。原版中,ProblemReporter 在创建值输入输出时已指定,因此 Codec#listOf 更灵活。自定义使用方式则可以按需求实现任一种行为。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output

    // Create List
    ValueOutput.TypedInputList<Rarity> listValue = output.list("listValue", Rarity.CODEC);
    // Add elements
    listValue.add(Rarity.COMMON);
    listValue.add(Rarity.EPIC);
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input

    // Read values of list
    for (Rarity rarity : input.listOrEmpty("listValue", Rarity.CODEC)) {
        // ...
    }
}

警告 列表即使为空,也会写入 ValueOutput。如果不希望写入空列表,应通过 TypedOutputList 或 ValueOutputList 的 isEmpty 检查,再将列表键传给 discard。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output

    // Create List
    ValueOutput.TypedInputList<Rarity> listValue = output.list("listValue", Rarity.CODEC);
    
    // Check if list is empty
    if (listValue.isEmpty()) {
        // Discard from output
        output.discard("listValue");
    }
}

对象

通过子对象可以创建和读取对象。ValueOutput#child 接受键并创建新的 ValueObject。读取时调用 ValueInput#child;如果不存在时希望得到底层值为空的 ValueInput,则使用 childOrEmpty。

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
    super.saveAdditional(output);
    
    // Write data to the output

    // Create object
    ValueOutput objectValue = output.child("objectValue");
    // Add data to object
    objectValue.putBoolean("boolChild", true);
    objectValue.putInt("intChild", 20);
}

@Override
protected void loadAdditional(ValueInput input) {
    super.loadAdditional(input);

    // Read data from the input

    // Read object
    ValueInput objectValue = input.childOrEmpty("objectValue");
    // Get data from object
    boolean boolChild = objectValue.getBooleanOr("boolChild", false);
    int intChild = objectValue.getIntOr("intChild", 0);
}

ValueIOSerializable

ValueIOSerializable 是 NeoForge 添加的接口,供可通过值输入输出序列化和反序列化的对象实现。NeoForge 用它处理数据附件。接口提供 serialize,将对象写入 ValueOutput,以及 deserialize,从 ValueInput 读取对象。

public class ExampleObject implements ValueIOSerializable {
    
    @Override
    public void serialize(ValueOutput output) {
        // Write the object data here
    }

    @Override
    public void deserialize(ValueInput input) {
        // Read the object data here
    }
}

也可以通过 NeoForge 添加的 ValueOutputExtension#putChild 和 ValueInputExtension#readChild 读写 ValueIOSerializable。

实现

NBT

NBT 的值输入输出由 TagValueOutput 和 TagValueInput 实现。

通过 createWithContext 或 createWithoutContext 创建 TagValueOutput。前者可访问 HolderLookup.Provider,因此能查找静态和数据包注册表的全部条目;后者不提供数据包访问。原版只使用前者。写入完毕后,通过 TagValueOutput#buildResult 获取 CompoundTag。TagValueInput 则通过 create 创建,传入 HolderLookup.Provider 和需要读取的 CompoundTag。

两者都接受 ProblemReporter,收集读写过程中的内部错误,目前只追踪 codec 错误。如何处理由 Mod 开发者决定;原版实现会在报告器非空时抛出异常。

// Assume we have access to a HolderLookup.Provider lookupProvider

TagValueOutput output = TagValueOutput.createWithContext(
    ProblemReporter.DISCARDING, // Choose to discard all errors
    lookupProvider
);

// Write to the output...

CompoundTag tag = output.buildResult();

// Collect the errors
ProblemReporter.Collector reporter = new ProblemReporter.Collector(
    // Optionally takes in the root path element
    // Some objects (e.g., block entities, entities) have a #problemPath() method that can be supplied
    new RootFieldPathElement("example_object")
);

TagValueInput input = TagValueInput.create(
    reporter,
    lookupProvider,
    tag
);

// Read from the input...

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。