原文:MinecraftForge/Documentation · 目标版本:Forge 1.20.1
编解码器(Codecs)¶
编解码器(Codec)是来自 Mojang DataFixerUpper 的序列化工具,用于描述对象如何在不同的格式之间转换,例如用于 JSON 的 JsonElement 和用于 NBT 的 Tag。
使用编解码器(Using Codecs)¶
编解码器主要用于将 Java 对象编码(encode,即序列化)为某种数据格式类型,以及将格式化数据对象解码(decode,即反序列化)回其关联的 Java 类型。这通常分别通过 Codec#encodeStart 和 Codec#parse 完成。
DynamicOps¶
为了确定编码和解码所采用的中间文件格式,#encodeStart 和 #parse 都需要一个 DynamicOps 实例来在该格式内定义数据。
DataFixerUpper 库包含用于编解码存储在 Gson 的 JsonElement 实例中的 JSON 数据的 JsonOps。JsonOps 支持两种 JsonElement 序列化版本:JsonOps#INSTANCE 定义标准 JSON 文件,JsonOps#COMPRESSED 允许将数据压缩为单个字符串。
// 假设 exampleCodec 表示 Codec<ExampleJavaObject>
// 假设 exampleObject 是 ExampleJavaObject
// 假设 exampleJson 是 JsonElement
// 将 Java 对象编码为普通 JsonElement
exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject);
// 将 Java 对象编码为压缩后的 JsonElement
exampleCodec.encodeStart(JsonOps.COMPRESSED, exampleObject);
// 将 JsonElement 解码为 Java 对象
// 假设 JsonElement 是正常解析的
exampleCodec.parse(JsonOps.INSTANCE, exampleJson);
Minecraft 还提供了用于编解码存储在 Tag 实例中的 NBT 数据的 NbtOps。可以通过 NbtOps#INSTANCE 引用它。
// 假设 exampleCodec 表示 Codec<ExampleJavaObject>
// 假设 exampleObject 是 ExampleJavaObject
// 假设 exampleNbt 是 Tag
// 将 Java 对象编码为 Tag
exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject);
// 将 Tag 解码为 Java 对象
exampleCodec.parse(JsonOps.INSTANCE, exampleNbt);
格式转换(Format Conversion)¶
DynamicOps 也可以单独用于在两种不同的编码格式之间转换。这可以通过 #convertTo 并传入目标 DynamicOps 格式和要转换的编码对象来完成。
// 将 Tag 转换为 JsonElement
// 假设 exampleTag 是 Tag
JsonElement convertedJson = NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, exampleTag);
DataResult¶
使用编解码器编码或解码的数据会返回一个 DataResult,它根据转换是否成功,持有转换后的实例或一些错误数据。当转换成功时,#result 提供的 Optional 将包含成功转换的对象。如果转换失败,#error 提供的 Optional 将包含 PartialResult,它根据编解码器持有错误消息和一个部分转换的对象。
此外,DataResult 上还有许多方法可用于将结果或错误转换为所需格式。例如,#resultOrPartial 在成功时返回包含结果的 Optional,在失败时返回包含部分转换对象的 Optional。该方法接收一个字符串消费者(consumer),用于决定如何报告错误消息(如果存在)。
// 假设 exampleCodec 表示 Codec<ExampleJavaObject>
// 假设 exampleJson 是 JsonElement
// 将 JsonElement 解码为 Java 对象
DataResult<ExampleJavaObject> result = exampleCodec.parse(JsonOps.INSTANCE, exampleJson);
result
// 获取结果,或在出错时获取部分结果并报告错误消息
.resultOrPartial(errorMessage -> /* 对错误消息做些什么 */)
// 如果结果或部分结果存在,做些什么
.ifPresent(decodedObject -> /* 对解码后的对象做些什么 */);
现有编解码器(Existing Codecs)¶
基本类型(Primitives)¶
Codec 类包含某些已定义基本类型的编解码器静态实例。
| Codec | Java 类型 |
|---|---|
BOOL |
Boolean |
BYTE |
Byte |
SHORT |
Short |
INT |
Integer |
LONG |
Long |
FLOAT |
Float |
DOUBLE |
Double |
STRING |
String |
BYTE_BUFFER |
ByteBuffer |
INT_STREAM |
IntStream |
LONG_STREAM |
LongStream |
PASSTHROUGH |
Dynamic<?>* |
EMPTY |
Unit** |
* Dynamic 是持有以受支持的 DynamicOps 格式编码的值的对象。它们通常用于将一种编码对象格式转换为另一种编码对象格式。
** Unit 是用于表示 null 对象的对象。
原版与 Forge(Vanilla and Forge)¶
Minecraft 和 Forge 为许多经常被编码和解码的对象定义了编解码器。一些示例包括:用于 ResourceLocation 的 ResourceLocation#CODEC、用于以 DateTimeFormatter#ISO_INSTANT 格式表示的 Instant 的 ExtraCodecs#INSTANT_ISO8601,以及用于 CompoundTag 的 CompoundTag#CODEC。
Warning
CompoundTag 无法使用 JsonOps 从 JSON 解码数字列表。JsonOps 在转换时会将数字设置为其最窄的类型。ListTag 会强制其数据使用特定类型,因此类型不同的数字(例如 64 会是 byte,384 会是 short)会在转换时抛出错误。
原版和 Forge 的注册表(Registry)也有针对注册表所含对象类型的编解码器(例如 Registry#BLOCK 或 ForgeRegistries#BLOCKS 提供 Codec<Block>)。Registry#byNameCodec 和 IForgeRegistry#getCodec 会将注册表对象编码为其注册表名称,若压缩则编码为整数标识符。原版注册表还有一个 Registry#holderByNameCodec,它编码为注册表名称,解码为包装在 Holder 中的注册表对象。
创建编解码器(Creating Codecs)¶
可以为任何对象创建用于编码和解码的编解码器。为便于理解,下文将展示等价的编码后 JSON。
记录(Records)¶
编解码器可以通过记录(record)来定义对象。每个记录编解码器都以显式命名的字段定义对象。创建记录编解码器的方法有很多,最简单的是通过 RecordCodecBuilder#create。
RecordCodecBuilder#create 接收一个函数,该函数定义一个 Instance 并返回对象的 application(App)。这与创建类实例(instance)以及用于将类应用(apply)到构造对象的构造函数之间存在类比关系。
// 要为某个对象创建编解码器的对象
public class SomeObject {
public SomeObject(String s, int i, boolean b) { /* ... */ }
public String s() { /* ... */ }
public int i() { /* ... */ }
public boolean b() { /* ... */ }
}
字段(Fields)¶
Instance 可以使用 #group 定义最多 16 个字段。每个字段必须是一个 application,定义要构造的对象所属的实例以及对象的类型。满足这一要求的最简单方法是:取一个 Codec,设置要解码的字段名称,并设置用于编码该字段的 getter。
字段可以从 Codec 创建:如果字段是必需的,使用 #fieldOf;如果字段包装在 Optional 中或有默认值,使用 #optionalFieldOf。两种方法都需要一个包含编码对象中字段名称的字符串。然后可以使用 #forGetter 设置用于编码该字段的 getter,它接收一个函数:给定对象后返回字段数据。
之后,可以通过 #apply 应用所得结果,以定义实例应如何为 application 构造对象。为了方便起见,分组字段应按其在构造函数中出现的顺序排列,这样该函数就可以简单地是构造函数的方法引用。
public static final Codec<SomeObject> RECORD_CODEC = RecordCodecBuilder.create(instance -> // 给定一个 instance
instance.group( // 定义 instance 中的字段
Codec.STRING.fieldOf("s").forGetter(SomeObject::s), // String
Codec.INT.optionalFieldOf("i", 0).forGetter(SomeObject::i), // Integer,字段不存在时默认为 0
Codec.BOOL.fieldOf("b").forGetter(SomeObject::b) // Boolean
).apply(instance, SomeObject::new) // 定义如何创建对象
);
// 编码后的 SomeObject
{
"s": "value",
"i": 5,
"b": false
}
// 另一个编码后的 SomeObject
{
"s": "value2",
// i 被省略,默认为 0
"b": true
}
转换器(Transformers)¶
编解码器可以通过映射方法转换为等价或部分等价的表示。每个映射方法接收两个函数:一个将当前类型转换为新类型,另一个将新类型转换回当前类型。这通过 #xmap 函数完成。
// 一个类
public class ClassA {
public ClassB toB() { /* ... */ }
}
// 另一个等价类
public class ClassB {
public ClassA toA() { /* ... */ }
}
// 假设存在某个编解码器 A_CODEC
public static final Codec<ClassB> B_CODEC = A_CODEC.xmap(ClassA::toB, ClassB::toA);
如果类型是部分等价的,即转换过程中存在一些限制,则有返回 DataResult 的映射函数——当遇到异常或无效状态时,可以用它返回错误状态。
| A 与 B 是否完全等价 | B 与 A 是否完全等价 | 转换方法 |
|---|---|---|
| 是 | 是 | #xmap |
| 是 | 否 | #flatComapMap |
| 否 | 是 | #comapFlatMap |
| 否 | 否 | #flatXMap |
// 给定一个字符串编解码器以转换为整数
// 并非所有字符串都能变成整数(A 与 B 不完全等价)
// 所有整数都能变成字符串(B 与 A 完全等价)
public static final Codec<Integer> INT_CODEC = Codec.STRING.comapFlatMap(
s -> { // 返回包含错误的数据结果
try {
return DataResult.success(Integer.valueOf(s));
} catch (NumberFormatException e) {
return DataResult.error(s + " is not an integer.");
}
},
Integer::toString // 普通函数
);
范围编解码器(Range Codecs)¶
范围编解码器是 #flatXMap 的一种实现:如果值不在设定的最小值和最大值之间(含边界),则返回错误的 DataResult。超出边界时,该值仍会作为部分结果提供。整数、浮点数和双精度浮点数分别通过 #intRange、#floatRange 和 #doubleRange 实现。
默认值(Defaults)¶
如果编码或解码的结果失败,可以通过 Codec#orElse 或 Codec#orElseGet 提供默认值作为替代。
单位(Unit)¶
提供代码内值并编码为空的编解码器可以用 Codec#unit 表示。如果编解码器在数据对象中使用了不可编码的条目,这很有用。
public static final Codec<IForgeRegistry<Block>> UNIT_CODEC = Codec.unit(
() -> ForgeRegistries.BLOCKS // 也可以是原始值
);
列表(List)¶
对象列表的编解码器可以通过 Codec#listOf 从对象编解码器生成。
// BlockPos#CODEC 是 Codec<BlockPos>
public static final Codec<List<BlockPos>> LIST_CODEC = BlockPos.CODEC.listOf();
// 编码后的 List<BlockPos>
[
[1, 2, 3], // BlockPos(1, 2, 3)
[4, 5, 6], // BlockPos(4, 5, 6)
[7, 8, 9] // BlockPos(7, 8, 9)
]
使用列表编解码器解码出的列表对象存储在不可变(immutable)列表中。如果需要可变列表,应对列表编解码器应用转换器。
映射(Map)¶
键和值对象的映射编解码器可以通过 Codec#unboundedMap 从两个编解码器生成。无界映射(unbounded map)可以指定任何基于字符串或可转换为字符串的值作为键。
// BlockPos#CODEC 是 Codec<BlockPos>
public static final Codec<Map<String, BlockPos>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, BlockPos.CODEC);
// 编码后的 Map<String, BlockPos>
{
"key1": [1, 2, 3], // key1 -> BlockPos(1, 2, 3)
"key2": [4, 5, 6], // key2 -> BlockPos(4, 5, 6)
"key3": [7, 8, 9] // key3 -> BlockPos(7, 8, 9)
}
使用无界映射编解码器解码出的映射对象存储在不可变映射中。如果需要可变映射,应对映射编解码器应用转换器。
Warning
无界映射只支持编码/解码为字符串的键。可以使用键值配对列表编解码器来绕过这一限制。
配对(Pair)¶
对象配对的编解码器可以通过 Codec#pair 从两个编解码器生成。
配对编解码器解码对象时,首先解码配对中的左对象,然后取编码对象的剩余部分,从中解码右对象。因此,编解码器要么在解码后表达编码对象的某些信息(例如记录),要么必须增强为 MapCodec 并通过 #codec 转换为普通编解码器。这通常可以通过将编解码器设为某个对象的字段来完成。
public static final Codec<Pair<Integer, String>> PAIR_CODEC = Codec.pair(
Codec.INT.fieldOf("left").codec(),
Codec.STRING.fieldOf("right").codec()
);
// 编码后的 Pair<Integer, String>
{
"left": 5, // fieldOf 查找 'left' 键作为左对象
"right": "value" // fieldOf 查找 'right' 键作为右对象
}
Tip
具有非字符串键的映射编解码器,可以通过应用了转换器的键值对列表进行编码/解码。
二选一(Either)¶
针对同一对象数据的两种不同编码/解码方法的编解码器,可以通过 Codec#either 从两个编解码器生成。
二选一编解码器先尝试使用第一个编解码器解码对象。如果失败,则尝试使用第二个编解码器解码。如果也失败了,DataResult 将只包含第二个编解码器失败的错误。
public static final Codec<Either<Integer, String>> EITHER_CODEC = Codec.either(
Codec.INT,
Codec.STRING
);
Tip
这可以与转换器结合使用,从两种不同的编码方法中获得特定对象。
分发(Dispatch)¶
编解码器可以拥有子编解码器,通过 Codec#dispatch 根据某个指定的类型解码特定对象。这通常用于包含编解码器的注册表,例如规则测试(rule test)或方块放置器(block placer)。
分发编解码器首先尝试从某个字符串键(通常是 type)获取编码的类型。然后解码该类型,调用 getter 获取用于解码实际对象的特定编解码器。如果用于解码对象的 DynamicOps 会压缩其映射,或者对象编解码器本身没有增强为 MapCodec(例如记录或带字段的基本类型),则对象需要存储在 value 键中。否则,对象将与其余数据在同一层级解码。
// 定义我们的对象
public abstract class ExampleObject {
// 定义用于在编码时指定对象类型的方法
public abstract Codec<? extends ExampleObject> type();
}
// 创建存储字符串的简单对象
public class StringObject extends ExampleObject {
public StringObject(String s) { /* ... */ }
public String s() { /* ... */ }
public Codec<? extends ExampleObject> type() {
// 一个已注册的注册表对象
// "string":
// Codec.STRING.xmap(StringObject::new, StringObject::s)
return STRING_OBJECT_CODEC.get();
}
}
// 创建存储字符串和整数的复杂对象
public class ComplexObject extends ExampleObject {
public ComplexObject(String s, int i) { /* ... */ }
public String s() { /* ... */ }
public int i() { /* ... */ }
public Codec<? extends ExampleObject> type() {
// 一个已注册的注册表对象
// "complex":
// RecordCodecBuilder.create(instance ->
// instance.group(
// Codec.STRING.fieldOf("s").forGetter(ComplexObject::s),
// Codec.INT.fieldOf("i").forGetter(ComplexObject::i)
// ).apply(instance, ComplexObject::new)
// )
return COMPLEX_OBJECT_CODEC.get();
}
}
// 假设存在一个 IForgeRegistry<Codec<? extends ExampleObject>> DISPATCH
public static final Codec<ExampleObject> = DISPATCH.getCodec() // 获取 Codec<Codec<? extends ExampleObject>>
.dispatch(
ExampleObject::type, // 从特定对象获取编解码器
Function.identity() // 从注册表获取编解码器
);
// 简单对象
{
"type": "string", // 用于 StringObject
"value": "value" // Codec 类型没有从 MapCodec 增强而来,需要字段
}
// 复杂对象
{
"type": "complex", // 用于 ComplexObject
// Codec 类型是从 MapCodec 增强而来的,可以内联
"s": "value",
"i": 0
}
待复核清单¶
- 原文 NBT 示例代码中存在两处疑似笔误:使用
JsonOps.INSTANCE处理Tag/exampleNbt(应为NbtOps.INSTANCE)。为忠实原文未改动代码,仅注释译文,建议主会话确认是否修正。 - 分发(Dispatch)一节代码中
public static final Codec<ExampleObject> = ...缺少变量名,为原文笔误,按原文保留。 - 编解码器体系(
Codec/DynamicOps/DataResult/RecordCodecBuilder)为 Mojang 库,与 1.20.1 一致(Forge 47.x 使用com.mojang.serialization),无需版本改动。 - 表格「Codec | Java 类型」表头中的 Java 类型按原文保留英文。