Codec 编解码器
Codec 是 Mojang 的 DataFixerUpper 提供的序列化工具,用于描述对象如何在不同格式之间转换,例如 JSON 的 JsonElement 和 NBT 的 Tag。
原创说明图,依据 Forge 1.21.x 官方文档 绘制。
使用 Codec
Codec 主要用于将 Java 对象编码(序列化)为某种数据格式,并将格式化的数据对象解码(反序列化)回对应的 Java 类型。通常分别使用 Codec#encodeStart 和 Codec#parse。
DynamicOps
为了确定编码和解码的中间文件格式,#encodeStart 和 #parse 都需要一个 DynamicOps 实例来定义该格式中的数据。
DataFixerUpper 包含 JsonOps,可对保存在 Gson 的 JsonElement 实例中的 JSON 数据编解码。它支持两种序列化方式:JsonOps#INSTANCE 定义标准 JSON,JsonOps#COMPRESSED 可将数据压缩成单个字符串。
// exampleCodec 是 Codec<ExampleJavaObject>,exampleObject 是 ExampleJavaObject
// exampleJson 是 JsonElement
exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject); // 编码为普通 JsonElement
exampleCodec.encodeStart(JsonOps.COMPRESSED, exampleObject); // 编码为压缩 JsonElement
exampleCodec.parse(JsonOps.INSTANCE, exampleJson); // 假设 JsonElement 按普通方式解析
Minecraft 还提供 NbtOps,用于对保存在 Tag 实例中的 NBT 数据编解码,可以使用 NbtOps#INSTANCE 引用。
// exampleCodec 是 Codec<ExampleJavaObject>,exampleObject 是 ExampleJavaObject
// exampleNbt 是 Tag
exampleCodec.encodeStart(NbtOps.INSTANCE, exampleObject); // 将 Java 对象编码为 Tag
exampleCodec.parse(NbtOps.INSTANCE, exampleNbt); // 将 Tag 解码为 Java 对象
原文此处使用
JsonOps.INSTANCE,与示例的 NBT 类型不一致;译文改用NbtOps.INSTANCE。
格式转换
DynamicOps 也可单独用于转换两种编码格式:使用 #convertTo 并传入目标 DynamicOps 格式和待转换的已编码对象。
// 将 Tag 转换为 JsonElement;exampleTag 是 Tag
JsonElement convertedJson = NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, exampleTag);
DataResult
使用 Codec 编码或解码会返回 DataResult,其中包含转换后的实例,或者转换失败时的错误数据。成功时,#result 提供的 Optional 包含转换结果;失败时,#error 提供的 Optional 包含 PartialResult,其中有错误信息,并根据 Codec 的实现可能包含部分转换的对象。
DataResult 还有许多方法可将结果或错误转换为所需格式。例如 #resultOrPartial 成功时返回包含结果的 Optional,失败时返回部分转换的对象;该方法接受字符串消费者,用于决定如何报告错误信息。
// exampleCodec 是 Codec<ExampleJavaObject>;exampleJson 是 JsonElement
DataResult<ExampleJavaObject> result = exampleCodec.parse(JsonOps.INSTANCE, exampleJson);
result
.resultOrPartial(errorMessage -> /* 处理错误信息 */)
.ifPresent(decodedObject -> /* 处理解码后的对象 */);
现有 Codec
基本类型
Codec 类为若干基本类型提供静态 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
Minecraft 和 Forge 为经常编解码的对象定义了许多 Codec,例如用于 ResourceLocation 的 ResourceLocation#CODEC、用于 DateTimeFormatter#ISO_INSTANT 格式的 Instant 的 ExtraCodecs#INSTANT_ISO8601,以及用于 CompoundTag 的 CompoundTag#CODEC。
警告:
CompoundTag不能使用JsonOps从 JSON 解码数字列表。JsonOps转换时会将数字设置为能容纳它的最窄类型,而ListTag强制其数据使用同一类型,因此不同类型的数字(例如 64 是byte,384 是short)转换时会报错。
原版和 Forge 注册表也为其包含的对象类型提供 Codec(例如 Registry#BLOCK 或 ForgeRegistries#BLOCKS 的 Codec<Block>)。Registry#byNameCodec 和 IForgeRegistry#getCodec 将注册对象编码为注册名,压缩时编码为整数标识符。原版注册表还有 Registry#holderByNameCodec,它编码为注册名,并解码为包装在 Holder 中的注册对象。
创建 Codec
可以为任意对象创建 Codec 以进行编解码。为便于理解,下面展示对应的编码 JSON。
Record
Codec 可通过 record 定义对象,每个 record Codec 定义一个具有显式命名字段的对象。创建方式很多,最简单的是 RecordCodecBuilder#create。它接受一个函数,该函数定义 Instance 并返回对象的应用(App)。可以类比为创建类的 instance,再使用构造器将类应用(apply)到所构造的对象。
public class SomeObject {
public SomeObject(String s, int i, boolean b) { /* ... */ }
public String s() { /* ... */ }
public int i() { /* ... */ }
public boolean b() { /* ... */ }
}
字段
Instance 可通过 #group 定义最多 16 个字段。每个字段必须是一个应用,定义对象所属的实例和对象类型。最简单的方法是从 Codec 指定要解码的字段名称,以及编码字段时使用的 getter。
必填字段可通过 #fieldOf 创建;以 Optional 包装或有默认值的字段可通过 #optionalFieldOf 创建。两者都需要表示编码对象中字段名的字符串。然后使用 #forGetter 设置编码时的 getter,该函数接收对象并返回字段数据。
接下来通过 #apply 指定实例如何为应用构造对象。为方便起见,分组字段应按构造函数中的顺序排列,这样函数可以直接使用构造函数方法引用。
public static final Codec<SomeObject> RECORD_CODEC = RecordCodecBuilder.create(instance -> // 给定一个实例
instance.group( // 定义实例中的字段
Codec.STRING.fieldOf("s").forGetter(SomeObject::s), // 字符串
Codec.INT.optionalFieldOf("i", 0).forGetter(SomeObject::i), // 整数,字段缺省时为 0
Codec.BOOL.fieldOf("b").forGetter(SomeObject::b) // 布尔值
).apply(instance, SomeObject::new) // 定义如何创建对象
);
// 编码的 SomeObject
{ "s": "value", "i": 5, "b": false }
// 另一个 SomeObject;省略 i,默认为 0
{ "s": "value2", "b": true }
转换器
Codec 可通过映射方法转为等价或部分等价的表示。每个映射方法接受两个函数:一个将当前类型转为新类型,另一个将新类型转回当前类型。完全等价时使用 #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 |
// 并非所有字符串都能变成整数,但所有整数都能变成字符串
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
);
// "5" 返回 5;"value" 报错,因为它不是整数
范围 Codec
范围 Codec 是 #flatXMap 的一种实现:值不在给定最小值和最大值的闭区间内时返回错误 DataResult,但仍将该值作为部分结果提供。整数、浮点数和双精度数分别使用 #intRange、#floatRange 和 #doubleRange。
public static final Codec<Integer> RANGE_CODEC = Codec.intRange(0, 4);
// 4 有效;5 超出 [0, 4],报错
默认值
编码或解码失败时,可以通过 Codec#orElse 或 Codec#orElseGet 提供默认值。
public static final Codec<Integer> DEFAULT_CODEC = Codec.INT.orElse(0); // 也可通过 #orElseGet 提供值
// "value" 不是整数,默认值为 0
Unit
Codec#unit 可表示一个提供代码内值、编码时不输出任何内容的 Codec。这适合数据对象中含有无法编码的条目时使用。
public static final Codec<IForgeRegistry<Block>> UNIT_CODEC = Codec.unit(
() -> ForgeRegistries.BLOCKS // 也可以使用原始值
);
// 无编码内容,将返回方块注册表 Codec
List
通过 Codec#listOf 从对象 Codec 生成对象列表的 Codec:
// 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)
]
译注:此处的注释仅用于说明,实际 JSON 不支持注释。
列表 Codec 解码出的列表不可变;需要可变列表时,应对列表 Codec 应用转换器。
Map
通过 Codec#unboundedMap 从两个 Codec 生成键值映射 Codec。无界映射允许使用基于字符串或可转换为字符串的值作为键。
// 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)
}
译注:此处的注释仅用于说明,实际 JSON 不支持注释。
无界映射 Codec 解码出的映射不可变;需要可变映射时,应应用转换器。
警告:无界映射只支持能够编码/解码为字符串的键。可以使用键值对的列表 Codec 绕过此限制。
Pair
通过 Codec#pair 从两个 Codec 生成对象对的 Codec。Pair Codec 先解码左侧对象,再从编码对象的剩余部分解码右侧对象。因此 Codec 必须在解码后保留编码对象的一些信息(例如 record),或者扩展为 MapCodec 并通过 #codec 转换为普通 Codec。通常可以通过将 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 键以读取右侧对象
}
译注:此处的注释仅用于说明,实际 JSON 不支持注释。
提示:非字符串键的映射 Codec 可通过键值对列表配合转换器编解码。
Either
通过 Codec#either 从两个 Codec 生成可用两种方式编解码对象数据的 Codec。它先尝试第一个 Codec,失败后尝试第二个;如果也失败,DataResult 只包含第二个 Codec 的错误。
public static final Codec<Either<Integer, String>> EITHER_CODEC = Codec.either(Codec.INT, Codec.STRING);
// 解码 5 得到 Either$Left<Integer, String>;解码 "value" 得到 Either$Right<Integer, String>
提示:可结合转换器,从两种编码方式获取特定对象。
Dispatch
Codec 可通过 Codec#dispatch 使用子 Codec,根据指定类型解码特定对象。这通常用于包含 Codec 的注册表,例如规则测试或方块放置器。
Dispatch Codec 首先从字符串键(通常为 type)获取编码类型。类型解码后,调用 getter 获取用于解码实际对象的特定 Codec。如果解码使用的 DynamicOps 压缩映射,或者对象 Codec 本身没有扩展为 MapCodec(如 record 或带字段的基本类型),对象就需要存储在 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> EXAMPLE_CODEC = DISPATCH.getCodec()
.dispatch(ExampleObject::type, Function.identity());
原文的 Dispatch 示例缺少 Codec 变量名;此处补为
EXAMPLE_CODEC。
// 简单对象:Codec 没有从 MapCodec 扩展,需要 value 字段
{ "type": "string", "value": "value" }
// 复杂对象:Codec 从 MapCodec 扩展,可内联
{ "type": "complex", "s": "value", "i": 0 }
原文:Codecs。