值输入输出
值输入输出(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。