Codec
Codec 是 Mojang DataFixerUpper 库中的序列化工具,用于描述对象如何在不同格式之间转换,例如 JSON 的 JsonElement 和 NBT 的 Tag。
使用 codec
Codec 主要用于将 Java 对象编码(序列化)为某种数据格式,以及将格式化数据对象解码(反序列化)回对应 Java 类型。通常分别使用 Codec#encodeStart 与 Codec#parse 完成。
DynamicOps
为了确定编码和解码所用的中间格式,#encodeStart 与 #parse 都需要 DynamicOps 实例,定义该格式中的数据操作。
DataFixerUpper 提供 JsonOps,处理保存在 Gson 的 JsonElement 中的 JSON 数据。它支持两种序列化方式:JsonOps#INSTANCE 对应标准 JSON,JsonOps#COMPRESSED 允许将数据压缩到单个字符串中。
// Let exampleCodec represent a Codec<ExampleJavaObject>
// Let exampleObject be a ExampleJavaObject
// Let exampleJson be a JsonElement
// Encode Java object to regular JsonElement
exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject);
// Encode Java object to compressed JsonElement
exampleCodec.encodeStart(JsonOps.COMPRESSED, exampleObject);
// Decode JsonElement into Java object
// Assume JsonElement was parsed normally
exampleCodec.parse(JsonOps.INSTANCE, exampleJson);
Minecraft 还提供 NbtOps,用于处理保存在 Tag 实例中的 NBT 数据,可通过 NbtOps#INSTANCE 引用。
// Let exampleCodec represent a Codec<ExampleJavaObject>
// Let exampleObject be a ExampleJavaObject
// Let exampleNbt be a Tag
// Encode Java object to Tag
exampleCodec.encodeStart(NbtOps.INSTANCE, exampleObject);
// Decode Tag into Java object
exampleCodec.parse(NbtOps.INSTANCE, exampleNbt);
为处理注册表条目,Minecraft 提供了 RegistryOps,其中包含用于获取可用注册表元素的查找提供器。通过 RegistryOps#create 创建时,传入用于存储数据的特定 DynamicOps,以及能访问所需注册表的查找提供器。NeoForge 在此基础上提供 ConditionalOps,让注册表 codec 查找支持条目加载条件。
// Let lookupProvider be a HolderLookup.Provider
// Let exampleCodec represent a Codec<ExampleJavaObject>
// Let exampleObject be a ExampleJavaObject
// Let exampleJson be a JsonElement
// Get the registry ops for JsonElement
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, lookupProvider);
// Encode Java object to JsonElement
exampleCodec.encodeStart(ops, exampleObject);
// Decode JsonElement into Java object
exampleCodec.parse(ops, exampleJson);
格式转换
DynamicOps 还可以单独用于转换两种已编码格式。调用 #convertTo,传入目标 DynamicOps 格式与待转换对象即可。
// Convert Tag to JsonElement
// Let exampleTag be a Tag
JsonElement convertedJson = NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, exampleTag);
DataResult
使用 codec 编解码会返回 DataResult。转换成功时,它保存转换后的实例;失败时保存错误信息。成功时,#result 返回的 Optional 包含转换后的对象;失败时,#error 返回的 Optional 包含 PartialResult,其中有错误消息,并且根据 codec 的情况可能包含部分转换后的对象。
DataResult 还提供多种方法,将结果或错误转换为所需形式。例如,#resultOrPartial 成功时返回包含结果的 Optional,失败时返回部分转换对象。它接受一个字符串 consumer,用来决定如何报告错误消息。
// Let exampleCodec represent a Codec<ExampleJavaObject>
// Let exampleJson be a JsonElement
// Decode JsonElement into Java object
DataResult<ExampleJavaObject> result = exampleCodec.parse(JsonOps.INSTANCE, exampleJson);
result
// Get result or partial on error, report error message
.resultOrPartial(errorMessage -> /* Do something with error message */)
// If result or partial is present, do something
.ifPresent(decodedObject -> /* Do something with decoded object */);
现有 codec
基础类型
Codec 类为若干基础类型提供静态 codec 实例。
| Codec | Java 类型 |
|---|---|
BOOL |
Boolean |
BYTE |
Byte |
SHORT |
Short |
INT |
Integer |
LONG |
Long |
FLOAT |
Float |
DOUBLE |
Double |
STRING |
String(注1) |
BYTE_BUFFER |
ByteBuffer |
INT_STREAM |
IntStream |
LONG_STREAM |
LongStream |
PASSTHROUGH |
Dynamic<?>(注2) |
EMPTY |
Unit(注3) |
- 可以通过
Codec#string或Codec#sizeLimitedString限制字符串字符数。 Dynamic保存以某个受支持DynamicOps格式编码的值,通常用于在不同编码格式之间转换对象。Unit用来表示null对象。
原版与 NeoForge
Minecraft 和 NeoForge 为经常编解码的对象定义了许多 codec。例如,ResourceLocation#CODEC 用于资源位置,ExtraCodecs#INSTANT_ISO8601 用于采用 DateTimeFormatter#ISO_INSTANT 格式的 Instant,CompoundTag#CODEC 用于复合标签。
注意
CompoundTag无法用JsonOps从 JSON 解码数字列表。转换时,JsonOps会为数字选择最窄类型,而ListTag强制所有数据使用特定类型。因此,不同类型的数字(例如 64 为 byte,384 为 short)会在转换时产生错误。
原版与 NeoForge 注册表还为所包含的对象类型提供 codec,例如 BuiltInRegistries#BLOCK 提供 Codec<Block>。Registry#byNameCodec 将注册对象编码为注册名称。原版注册表还提供 Registry#holderByNameCodec,编码结果也是注册名称,但解码时返回包装在 Holder 中的注册对象。
创建 codec
可以为任意对象创建 codec。为便于理解,以下示例会同时展示对应的编码 JSON。
记录
Codec 可以通过记录式构建定义对象,明确指定各字段名称。构建 record codec 的方式很多,最简单的是 RecordCodecBuilder#create。
RecordCodecBuilder#create 接受函数,函数定义一个 Instance 并返回对象的应用(App)。可以将它类比为创建类的实例,并通过构造函数将类应用到构建对象上。
// Some object to create a codec for
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 函数,接受对象并返回字段数据。
警告 如果元素在解析时出错,
#optionalFieldOf也会报告错误。若希望忽略此类错误,请使用#lenientOptionalFieldOf。
之后通过 #apply 应用组合结果,定义如何构造对象。为便于直接引用构造函数,建议按构造函数参数顺序排列字段。
public static final Codec<SomeObject> RECORD_CODEC = RecordCodecBuilder.create(instance -> // Given an instance
instance.group( // Define the fields within the instance
Codec.STRING.fieldOf("s").forGetter(SomeObject::s), // String
Codec.INT.optionalFieldOf("i", 0).forGetter(SomeObject::i), // Integer, defaults to 0 if field not present
Codec.BOOL.fieldOf("b").forGetter(SomeObject::b) // Boolean
).apply(instance, SomeObject::new) // Define how to create the object
);
// Encoded SomeObject
{
"s": "value",
"i": 5,
"b": false
}
// Another encoded SomeObject
{
"s": "value2",
// i is omitted, defaults to 0
"b": true
}
// Another encoded SomeObject
{
"s": "value2",
// Will throw an error as lenientOptionalFieldOf is not used
"i": "bad_value",
"b": true
}
转换器
通过映射方法,可以把 codec 转换为等价或部分等价的表示。映射方法接受两个函数:一个将当前类型转为新类型,另一个将新类型转回当前类型。完全等价时使用 #xmap。
// A class
public class ClassA {
public ClassB toB() { /* ... */ }
}
// Another equivalent class
public class ClassB {
public ClassA toA() { /* ... */ }
}
// Assume there is some codec 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 |
// Given an string codec to convert to a integer
// Not all strings can become integers (A is not fully equivalent to B)
// All integers can become strings (B is fully equivalent to A)
public static final Codec<Integer> INT_CODEC = Codec.STRING.comapFlatMap(
s -> { // Return data result containing error on failure
try {
return DataResult.success(Integer.valueOf(s));
} catch (NumberFormatException e) {
return DataResult.error(s + " is not an integer.");
}
},
Integer::toString // Regular function
);
// Will return 5
"5"
// Will error, not an integer
"value"
范围 codec
范围 codec 基于 #flatXMap 实现。值不在指定最小值与最大值之间(含端点)时,返回错误 DataResult,但仍将该值作为部分结果提供。整数、浮点数和双精度浮点数分别使用 #intRange、#floatRange 和 #doubleRange。
public static final Codec<Integer> RANGE_CODEC = Codec.intRange(0, 4);
// Will be valid, inside [0, 4]
4
// Will error, outside [0, 4]
5
字符串解析器
Codec#stringResolver 基于 flatXmap 实现,将字符串映射到某种对象。
public record StringResolverObject(String name) { /* ... */ }
// Assume there is some Map<String, StringResolverObject> OBJECT_MAP
public static final Codec<StringResolverObject> STRING_RESOLVER_CODEC = Codec.stringResolver(StringResolverObject::name, OBJECT_MAP::get);
// Will map this string to its associated object
"example_name"
默认值
编解码失败时,可通过 Codec#orElse 或 Codec#orElseGet 提供默认值。
public static final Codec<Integer> DEFAULT_CODEC = Codec.INT.orElse(
errorMessage -> /* Do something with the error message */,
0 // Can also be a supplied value via #orElseGet
);
// Not an integer, defaults to 0
"value"
Unit
Codec#unit 表示提供代码中的一个值、但不编码任何内容的 codec。当数据对象中使用无法编码的条目时很有用。
public static final Codec<IEventBus> UNIT_CODEC = Codec.unit(
() -> NeoForge.EVENT_BUS // Can also be a raw value
);
// Nothing here, will return the NeoForge event bus
惰性初始化
有时,codec 依赖的数据在构造时尚不存在。这时可用 Codec#lazyInitialized,通过传入 codec supplier,让 codec 在第一次编解码时构造自身。
public static final Codec<IEventBus> LAZY_CODEC = Codec.lazyInitialized(
() -> Codec.Unit(NeoForge.EVENT_BUS)
);
// Nothing here, will return the NeoForge event bus
// Encodes/decodes the same way as the normal codec
列表
通过对象 codec 的 Codec#listOf 可以生成对象列表的 codec。listOf 还可接受表示列表最小和最大长度的整数。sizeLimitedListOf 作用相似,但只指定最大长度。
// BlockPos#CODEC is a Codec<BlockPos>
public static final Codec<List<BlockPos>> LIST_CODEC = BlockPos.CODEC.listOf();
// Encoded List<BlockPos>
[
[1, 2, 3], // BlockPos(1, 2, 3)
[4, 5, 6], // BlockPos(4, 5, 6)
[7, 8, 9] // BlockPos(7, 8, 9)
]
列表 codec 解码出的对象存储在不可变列表中。如果需要可变列表,应对列表 codec 应用转换器。
映射
通过 Codec#unboundedMap,可以使用两个 codec 分别表示键和值,生成映射 codec。无界映射可以使用任何基于字符串或可转换为字符串的值作为键。
// BlockPos#CODEC is a Codec<BlockPos>
public static final Codec<Map<String, BlockPos>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, BlockPos.CODEC);
// Encoded 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)
}
无界映射 codec 解码出的对象存储在不可变映射中。如果需要可变映射,应对其应用转换器。
注意 无界映射只支持能够编解码为字符串的键。可使用键值对的列表 codec 绕过此限制。
Pair
通过 Codec#pair 可将两个 codec 组合为对象对的 codec。
Pair codec 先解码左侧对象,再用编码对象剩余的部分解码右侧对象。因此,codec 必须在解码后对编码对象保留一定结构信息(例如记录),或先扩展为 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()
);
// Encoded Pair<Integer, String>
{
"left": 5, // fieldOf looks up 'left' key for left object
"right": "value" // fieldOf looks up 'right' key for right object
}
建议 非字符串键的映射可以通过键值对列表结合转换器进行编解码。
Either
通过 Codec#either 可以组合两个 codec,为同一数据提供两种不同编解码方式。
Either codec 先尝试第一个 codec,失败后尝试第二个。如果第二个也失败,DataResult 只包含第二次失败的错误。
public static final Codec<Either<Integer, String>> EITHER_CODEC = Codec.either(
Codec.INT,
Codec.STRING
);
// Encoded Either.Left<Integer, String>
5
// Encoded Either.Right<Integer, String>
"value"
建议 可结合转换器,从两种不同编码方式得到特定对象。
Xor
Codec#xor 是 Either 的特例,只有恰好一种方式处理成功时才成功。如果两个 codec 都能处理,则返回错误。
public static final Codec<Either<Integer, String>> XOR_CODEC = Codec.xor(
Codec.INT.fieldOf("number").codec(),
Codec.STRING.fieldOf("text").codec()
);
// Encoded Either.Left<Integer, String>
{
"number": 4
}
// Encoded Either.Right<Integer, String>
{
"text": "value"
}
// Throws an error as both can be decoded
{
"number": 4,
"text": "value"
}
备选方式
Codec#withAlternative 是 Either 的特例:两个 codec 都试图解码同一种对象,但采用不同存储格式。首先尝试主 codec,失败后使用第二个。编码时始终使用主 codec。
public static final Codec<BlockPos> ALTERNATIVE_CODEC = Codec.withAlternative(
BlockPos.CODEC,
RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.fieldOf("x").forGetter(BlockPos::getX),
Codec.INT.fieldOf("y").forGetter(BlockPos::getY),
Codec.INT.fieldOf("z").forGetter(BlockPos::getZ)
), BlockPos::new)
);
// Normal method to decode BlockPos
[ 1, 2, 3 ]
// Alternative method to decode BlockPos
{
"x": 1,
"y": 2,
"z": 3
}
递归
有时,对象字段会引用相同类型的对象。例如,EntityPredicate 对载具、乘客和目标实体使用另一个 EntityPredicate。这时可使用 Codec#recursive,将 codec 本身作为参数提供给创建它的函数。
// Define our recursive object
public record RecursiveObject(Optional<RecursiveObject> inner) { /* ... */ }
public static final Codec<RecursiveObject> RECURSIVE_CODEC = Codec.recursive(
RecursiveObject.class.getSimpleName(), // This is for the toString method
recursedCodec -> RecordCodecBuilder.create(instance -> instance.group(
recursedCodec.optionalFieldOf("inner").forGetter(RecursiveObject::inner)
).apply(instance, RecursiveObject::new))
);
// An encoded recursive object
{
"inner": {
"inner": {}
}
}
分派
通过 Codec#dispatch,codec 可以根据指定类型选择子 codec 解码特定对象。这通常用于保存 codec 的注册表,例如规则测试或方块放置器。
分派 codec 首先从某个字符串键(通常是 type)获取编码类型,再解码该类型,并通过 getter 找到解码实际对象所用的 codec。如果解码所用 DynamicOps 会压缩映射,或者对象 codec 本身不是从 MapCodec 扩展而来(例如记录或作为字段的基础类型),对象就需要存放在 value 键中。否则,对象会与其他数据在同一层级解码。
// Define our object
public abstract class ExampleObject {
// Define the method used to specify the object type for encoding
public abstract MapCodec<? extends ExampleObject> type();
}
// Create simple object which stores a string
public class StringObject extends ExampleObject {
public StringObject(String s) { /* ... */ }
public String s() { /* ... */ }
public MapCodec<? extends ExampleObject> type() {
// A registered registry object
// "string":
// Codec.STRING.xmap(StringObject::new, StringObject::s).fieldOf("string")
return STRING_OBJECT_CODEC.get();
}
}
// Create complex object which stores a string and integer
public class ComplexObject extends ExampleObject {
public ComplexObject(String s, int i) { /* ... */ }
public String s() { /* ... */ }
public int i() { /* ... */ }
public MapCodec<? extends ExampleObject> type() {
// A registered registry object
// "complex":
// RecordCodecBuilder.mapCodec(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();
}
}
// Assume there is an Registry<MapCodec<? extends ExampleObject>> DISPATCH
public static final Codec<ExampleObject> = DISPATCH.byNameCodec() // Gets Codec<MapCodec<? extends ExampleObject>>
.dispatch(
ExampleObject::type, // Get the codec from the specific object
Function.identity() // Get the codec from the registry
);
// Simple object
{
"type": "string", // For StringObject
"value": "value" // Codec type is not augmented from MapCodec, needs field
}
// Complex object
{
"type": "complex", // For ComplexObject
// Codec type is augmented from MapCodec, can be inlined
"s": "value",
"i": 0
}
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。