跳到主要内容
版本:26.1

值 I/O

值 I/O(Value I/O)系统是一种标准化的序列化方法,用于操作某个后备对象中的数据,例如用于 NBT 的 CompoundTag

输入与输出

值 I/O 系统由两部分组成:ValueOutput 在序列化时向对象写入数据,ValueInput 在反序列化时从对象读取数据。实现方法通常只接收 ValueOutputValueInput 作为唯一参数,并且不返回任何值。值 I/O 期望后备对象是一个由字符串键映射到对象值的字典。借助所提供的方法,值 I/O 便可从后备对象读取信息或向其写入信息。

// 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
}

基本类型

值 I/O 提供了读写某些基本类型的方法。ValueOutput 方法以 put* 为前缀,接收键与基本类型的值。ValueInput 方法命名为 get*Or,接收键与一个在不存在时使用的默认值。

Java TypeValueOutputValueInput
booleanputBooleangetBooleanOr
byteputBytegetByteOr
shortputShortgetShortOr
intputIntgetInt*, getIntOr
longputLonggetLong*, getLongOr
floatputFloatgetFloatOr
doubleputDoublegetDoubleOr
StringputStringgetString*, getStringOr
int[]putIntArraygetIntArray*

* 这些 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 也可用于向值 I/O 存储值以及从中读取值。在原版中,所有 Codec 都通过 RegistryOps 处理,从而支持存储数据包注册项。ValueOutput#storestoreNullable 接收键、用于写入对象的 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);
}

ValueOutputValueInput 还为 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 会向值访问写入任意键,可能覆盖已有数据。请确保 MapCodec 内的所有键与其他键互不相同。

列表

列表可以通过两种方法之一创建与读取:子值 I/O,或 [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);
}
}

Codec 通过 ValueOutput#list 为数据对象提供了一个列表变体。它接收一个键和某个 Codec,返回一个 ValueOutput.TypedOutputListTypedOutputListValueOutputList 相同,区别在于它直接操作数据对象,而不是使用另一个值 I/O。可以通过 TypedOutputList#add 向列表添加元素。随后同样地,可以通过 ValueInput#listlistOrEmpty 读取列表,返回一个 TypedValueInput

备注

TypedValueOutput / TypedValueInputCodec#listOf 的主要区别在于错误处理方式。对于 Codec#listOf,某个条目失败会导致整个对象被标记为错误的 DataResult。而带类型的值 I/O 通常通过 ProblemReporter 处理错误。在原版中,Codec#listOf 提供了更大的灵活性,因为 ProblemReporter 是在创建值 I/O 时指定的。不过,自定义的值 I/O 用法可以根据使用场景选择实现其中任一种。

// 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。如果你不想写入该列表,应让 TypedOutputListValueOutputList 检查其是否 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 读取该对象,或使用 childOrEmpty 在应当默认返回一个带空后备值的 ValueInput 时使用。

// 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 新增的接口,用于那些可以借助值 I/O 序列化和反序列化的对象。NeoForge 使用该 API 来处理数据附加。该接口提供两个方法:serialize 将对象写入 ValueOutputdeserializeValueInput 读取对象。

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
}
}

ValueIOSerializable 也可以通过 NeoForge 新增的方法 ValueOutputExtension#putChildValueInputExtension#readChild 进行写入和读取。

实现

NBT

针对 NBT 的值 I/O 由 TagValueOutputTagValueInput 处理。

TagValueOutput 可以通过 createWithContextcreateWithoutContext 创建,createWithContext 意味着该输出可以访问 HolderLookup.Provider,从而提供所有注册项(静态的和数据包的),而 createWithoutContext 不提供任何数据包访问权限。原版只使用 createWithContextValueOutput 使用完毕后,可以通过 TagValueOutput#buildResult 取得 CompoundTag。而 TagValueInput 可以通过 create 创建,接收 HolderLookup.Provider 以及该输入所访问的 CompoundTag

两种值 I/O 都还接收一个 ProblemReporterProblemReporter 用于收集读写过程中的所有内部错误。目前它只跟踪 Codec 错误。如何处理这些错误由 Mod 开发者决定。原版实现会在 ProblemReporter 非空时抛出异常。

// 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...