流编解码器
流编解码器是一种序列化工具,用于描述如何将对象存入流以及从流中读取,例如缓冲区。原版主要通过网络系统使用流编解码器同步数据。
说明 流编解码器与编解码器大体相似,因此本页采用相同的组织方式,便于对照。
使用流编解码器
流编解码器分别使用 StreamCodec#encode 和 StreamCodec#decode 将对象编码到流,以及从流中解码。encode 接收流和待编码对象;decode 接收流并返回解码后的对象。流通常是 ByteBuf、FriendlyByteBuf 或 RegistryFriendlyByteBuf。
// Let exampleStreamCodec represent a StreamCodec<ExampleJavaObject>
// Let exampleObject be a ExampleJavaObject
// Let buffer be a RegistryFriendlyByteBuf
// Encode Java object into the buffer stream
exampleStreamCodec.encode(buffer, exampleObject);
// Read Java object from buffer stream
ExampleJavaObject obj = exampleStreamCodec.decode(buffer);
提示 除非手动处理缓冲区对象,否则通常不需要直接调用
encode和decode。
现有流编解码器
ByteBufCodecs
ByteBufCodecs 包含某些基本类型和对象的编解码器静态实例。
| 流编解码器 | Java 类型 |
|---|---|
BOOL |
Boolean |
BYTE |
Byte |
SHORT |
Short |
INT |
Integer |
LONG |
Long |
FLOAT |
Float |
DOUBLE |
Double |
BYTE_ARRAY |
byte[]* |
LONG_ARRAY |
long[] |
STRING_UTF8 |
String** |
TAG |
Tag |
COMPOUND_TAG |
CompoundTag |
VECTOR3F |
Vector3fc |
QUATERNIONF |
Quaternionfc |
GAME_PROFILE |
GameProfile |
* 可以通过 ByteBufCodecs#byteArray 限制 byte[] 的元素数量。
** 可以通过 ByteBufCodecs#stringUtf8 限制 String 的字符数量。
此外,还有一些静态实例采用不同方式编码和解码基本类型及对象。
无符号短整数
UNSIGNED_SHORT 是 SHORT 的替代形式,用于将值视为无符号数。Java 中数字有符号,因此无符号短整数会以 Integer 发送和接收,并屏蔽高两个字节。
变长数字
VAR_INT 和 VAR_LONG 尽量用最少的空间编码数值:每次编码七位,以最高位标记该数字是否还有后续数据。范围在 0 到 2^28-1 的整数,以及 0 到 2^56-1 的长整数,发送时使用的字节数分别不超过普通整数或长整数。如果数值通常落在这些范围内,且大多接近较小的一端,应使用这些变长流编解码器。
提示
VAR_INT和VAR_LONG分别是INT和LONG的替代形式。
可信标签
TRUSTED_TAG 和 TRUSTED_COMPOUND_TAG 分别是 TAG 与 COMPOUND_TAG 的变体。后两者解码标签时有 2 MiB 的堆内存限制,可信标签则没有此限制。可信标签流编解码器最好只用于发往客户端的数据包,例如原版的方块实体数据包和实体数据序列化器。
如果需要其他大小限制,可以通过 ByteBufCodecs#tagCodec 或 #compoundTagCodec,提供具有指定大小的 NbtAccounter。还可以通过 #optionalTagCodec 获取由 Optional 包装的 Tag。
宽松 JSON
ByteBufCodecs#lenientJson 处理任意 JsonElement,允许 nan、infinite 等浮点描述形式,以及多个顶层对象。它接收允许的 JSON 最大大小。
原版与 NeoForge
Minecraft 和 NeoForge 为频繁编解码的对象定义了许多流编解码器,例如 Identifier 的 Identifier#STREAM_CODEC,以及 ChunkPos 的 NeoForgeStreamCodecs#CHUNK_POS。
大多数流编解码器位于对象类本身,或 StreamCodec、ByteBufCodecs、NeoForgeStreamCodecs 中。
创建流编解码器
可以创建流编解码器,将任意对象写入流或从流中读取。本节以缓冲区作为流进行介绍,因为这是主要用途。
流编解码器具有两个泛型参数:B 表示缓冲区,V 表示对象值。B 通常为 ByteBuf、FriendlyByteBuf 或 RegistryFriendlyByteBuf,三者依次继承。FriendlyByteBuf 增加了 Minecraft 专用读写方法,RegistryFriendlyByteBuf 则提供注册表及其对象的访问能力。
构造流编解码器时,B 应选择满足需求的最通用缓冲区类型。例如,Identifier 以字符串发送,普通 ByteBuf 就支持字符串,所以类型应为 StreamCodec<ByteBuf, Identifier>。FriendlyByteBuf 包含写入 ChunkPos 的方法,因此对应类型为 StreamCodec<FriendlyByteBuf, ChunkPos>。Item 需要访问注册表,所以类型应为 StreamCodec<RegistryFriendlyByteBuf, Item>。
多数接收流编解码器的方法,对缓冲区类型要求为 ? super B。因此,如果缓冲区类型为 RegistryFriendlyByteBuf,上述三种示例都可使用。
成员编码器
StreamMemberEncoder 是 StreamEncoder 的替代形式,参数顺序为待编码对象在前、缓冲区在后。对象本身有实例方法可写入缓冲区时,通常使用它。可以调用 StreamCodec#ofMember,由 StreamMemberEncoder 创建 StreamCodec。
// Some object to create a stream codec for
public class ExampleObject {
// The normal constructor
public ExampleObject(String arg1, int arg2, boolean arg3) { /* ... */ }
// The stream decoder reference
public ExampleObject(ByteBuf buffer) { /* ... */ }
// The stream encoder reference
public void encode(ByteBuf buffer) { /* ... */ }
}
// What the stream codec would look like
public static StreamCodec<ByteBuf, ExampleObject> STREAM_CODEC =
StreamCodec.ofMember(ExampleObject::encode, ExampleObject::new);
组合
流编解码器可以通过 StreamCodec#composite 读写对象。每个组合流编解码器定义一系列流编解码器与 getter,按提供顺序读写。composite 的重载最多支持十二个对象参数。
composite 中每两个参数构成一组:读写字段的流编解码器,以及从对象中取得待编码字段的 getter。最后一个参数是在解码时创建对象新实例的函数。
// Objects to create a stream codec for
public record SimpleExample(String arg1, int arg2, boolean arg3) {}
public record RegistryExample(double arg1, Holder<Item> arg2) {}
// The stream codecs
public static final StreamCodec<ByteBuf, SimpleExample> SIMPLE_STREAM_CODEC =
StreamCodec.composite(
// Stream codec and getter pair
ByteBufCodecs.STRING_UTF8, SimpleExample::arg1,
ByteBufCodecs.VAR_INT, SimpleExample::arg2,
ByteBufCodecs.BOOL, SimpleExample::arg3,
SimpleExample::new
);
// Since this has a holder, a RegistryFriendlyByteBuf is used
public static final StreamCodec<RegistryFriendlyByteBuf, RegistryExample> REGISTRY_STREAM_CODEC =
StreamCodec.composite(
// Note that ByteBuf stream codecs can be used here
ByteBufCodecs.DOUBLE, RegistryExample::arg1,
ByteBufCodecs.holderRegistry(Registries.ITEM), RegistryExample::arg2,
RegistryExample::new
);
转换器
可以通过映射方法,将流编解码器转换为等价或部分等价的表示。两种映射方法作用于值,另一种作用于缓冲区。
map 使用两个函数转换值:一个将当前类型转换为新类型,另一个将新类型转换回当前类型。这与编解码器转换器类似。
public static final StreamCodec<ByteBuf, Identifier> STREAM_CODEC =
ByteBufCodecs.STRING_UTF8.map(
// String -> Identifier
Identifier::new,
// Identifier -> String
Identifier::toString
);
apply 使用 StreamCodec.CodecOperation 转换值。该操作接收当前类型的流编解码器,返回新类型的流编解码器,通常用于封装 map 或接收辅助方法。
public static final StreamCodec<ByteBuf, List<Identifier>> STREAM_CODEC =
Identifier.STREAM_CODEC.apply(ByteBufCodecs.list());
mapStream 使用函数转换缓冲区,该函数接收新缓冲区类型并返回当前缓冲区类型。此方法很少需要使用,因为多数涉及流编解码器的方法无需改变缓冲区类型。
public static final StreamCodec<RegistryFriendlyByteBuf, Integer> STREAM_CODEC =
ByteBufCodecs.VAR_INT.mapStream(buffer -> (ByteBuf) buffer);
单位编解码器
若流编解码器只提供代码中指定的值,不编码任何内容,可以使用 StreamCodec#unit。无需通过网络同步信息时,这很有用。
警告 单位流编解码器要求编码对象与指定的单位值相等,否则会报错。因此,对象必须有能对单位对象返回 true 的
equals实现,或者编码时始终使用提供给流编解码器的那个实例。
public static final StreamCodec<ByteBuf, Item> UNIT_STREAM_CODEC =
StreamCodec.unit(Items.AIR);
延迟初始化
流编解码器有时依赖构造时尚不存在的数据。此时可以使用 NeoForgeStreamCodecs#lazy,让编解码器在第一次读写时构建。该方法接收提供流编解码器的 supplier。
public static final StreamCodec<ByteBuf, Item> LAZY_STREAM_CODEC =
NeoForgeStreamCodecs.lazy(
() -> StreamCodec.unit(Items.AIR)
);
集合
可通过 collection,从对象流编解码器生成集合流编解码器。它接收构造空集合的 IntFunction、对象流编解码器,以及可选的最大容量。
public static final StreamCodec<ByteBuf, Set<BlockPos>> COLLECTION_STREAM_CODEC =
ByteBufCodecs.collection(
HashSet::new, // Constructs a set with the specified capacity
BlockPos.STREAM_CODEC,
256 // The set can only have up to 256 elements
);
collection 的另一种重载可以结合 StreamCodec#apply 使用。
public static final StreamCodec<ByteBuf, Set<BlockPos>> COLLECTION_STREAM_CODEC =
BlockPos.STREAM_CODEC.apply(
ByteBufCodecs.collection(HashSet::new)
);
基于列表的集合也可以通过 StreamCodec#apply,调用带可选最大容量的 ByteBufCodecs#list 来指定。
public static final StreamCodec<ByteBuf, List<BlockPos>> LIST_STREAM_CODEC =
BlockPos.STREAM_CODEC.apply(
// The list can only have up to 256 elements
ByteBufCodecs.list(256)
);
映射
通过 ByteBufCodecs#map 可以使用键和值的两个流编解码器,生成映射的流编解码器。该函数还接收构造空映射的 IntFunction,以及可选的最大容量。
public static final StreamCodec<ByteBuf, Map<String, BlockPos>> MAP_STREAM_CODEC =
ByteBufCodecs.map(
HashMap::new, // Constructs a map with the specified capacity
ByteBufCodecs.STRING_UTF8,
BlockPos.STREAM_CODEC,
256 // The map can only have up to 256 elements
);
Either
ByteBufCodecs#either 可以由两个流编解码器生成新的流编解码器,以两种不同方式读写对象数据。该方法先读写一个布尔值,表示接下来使用第一个还是第二个流编解码器。
public static final StreamCodec<ByteBuf, Either<Integer, String>> EITHER_STREAM_CODEC =
ByteBufCodecs.either(
ByteBufCodecs.VAR_INT,
ByteBufCodecs.STRING_UTF8
);
ID 映射器
通过网络发送两端都存在的对象信息时,多数情况只需发送代表 ID 的整数。这能减少需同步的数据量;枚举和注册表都采用这种方式。
ByteBufCodecs#idMapper 提供便捷的对象 ID 发送方式。它可以接收一对分别在对象与整数之间转换的函数,也可以接收 IdMap。
// For some enum
public enum ExampleIdObject {
;
// Gets Id -> Enum
public static final IntFunction<ExampleIdObject> BY_ID =
ByIdMap.continuous(
ExampleIdObject::getId,
ExampleIdObject.values(),
ByIdMap.OutOfBoundsStrategy.ZERO
);
ExampleIdObject(int id) { /* ... */ }
}
// The stream codec would look like
public static final StreamCodec<ByteBuf, ExampleIdObject> ID_STREAM_CODEC =
ByteBufCodecs.idMapper(ExampleIdObject.BY_ID, ExampleIdObject::getId);
Optional
向 ByteBufCodecs#optional 提供流编解码器,可以生成发送 Optional 包装值的流编解码器。它先读写一个布尔值,表示是否继续读写对象。
public static final StreamCodec<RegistryFriendlyByteBuf, Optional<DataComponentType<?>>> OPTIONAL_STREAM_CODEC =
DataComponentType.STREAM_CODEC.apply(ByteBufCodecs::optional);
注册表对象
可以使用 registry、holderRegistry 或 holder 在网络上传输注册表对象。它们都接收表示对象所属注册表的 ResourceKey。
警告 自定义注册表必须通过调用
RegistryBuilder#sync并传入true来启用同步,否则编码器会抛出异常。
registry 和 holderRegistry 分别返回注册表对象,以及由 holder 包装的注册表对象。这两种方法发送表示注册表对象的 ID。
// Registry object
public static final StreamCodec<RegistryFriendlyByteBuf, Item> VALUE_STREAM_CODEC =
ByteBufCodecs.registry(Registries.ITEM);
// Holder of registry object
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<Item>> HOLDER_STREAM_CODEC =
ByteBufCodecs.holderRegistry(Registries.ITEM);
holder 返回由 holder 包装的注册表对象。它发送对象 ID;如果提供的 Holder 是直接引用,则发送对象本身。为此,holder 还接收注册表对象的流编解码器。
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<SoundEvent>> STREAM_CODEC =
ByteBufCodecs.holder(
Registries.SOUND_EVENT, SoundEvent.DIRECT_STREAM_CODEC
);
提示 只有 holder 不是直接引用时,
holder才会因未同步的自定义注册表而抛出异常。
Holder 集合
标签或由 holder 包装的注册表对象集合,可以通过 holderSet 发送。它接收表示对象所属注册表的 ResourceKey。
public static final StreamCodec<RegistryFriendlyByteBuf, HolderSet<Item>> HOLDER_SET_STREAM_CODEC =
ByteBufCodecs.holderSet(Registries.ITEM);
递归
对象有时会以字段形式引用同类型对象。例如,有隐藏状态效果时,MobEffectInstance 会接收可选的 MobEffectInstance。此时可以使用 StreamCodec#recursive,在用于创建流编解码器的函数中提供该流编解码器自身。
// Define our recursive object
public record RecursiveObject(Optional<RecursiveObject> inner) { /* ... */ }
public static final StreamCodec<ByteBuf, RecursiveObject> RECURSIVE_CODEC = StreamCodec.recursive(
recursedStreamCodec -> StreamCodec.composite(
recursedStreamCodec.apply(ByteBufCodecs::optional),
RecursiveObject::inner,
RecursiveObject::new
)
);
分派
流编解码器可以通过 StreamCodec#dispatch 使用子流编解码器,根据指定类型解码具体对象。这通常用于表示类型的注册表对象,例如 ParticleOptions 的 ParticleType,或 Stat 的 StatType。
分派流编解码器会先尝试读写类型对象,再通过传入方法中的函数读写当前对象。第一个 Function 接收当前对象,取得写入值所需的类型;第二个 Function 接收类型对象,取得读取该值所需的当前对象 StreamCodec。
// Define our object(s)
public abstract class ExampleObject {
// Define the method used to specify the object type for encoding
public abstract StreamCodec<? super RegistryFriendlyByteBuf, ? extends ExampleObject> streamCodec();
}
// Assume there is a ResourceKey<StreamCodec<? super RegistryFriendlyByteBuf, ? extends ExampleObject>> DISPATCH
public static final StreamCodec<RegistryFriendlyByteBuf, ExampleObject> DISPATCH_STREAM_CODEC =
ByteBufCodecs.registry(DISPATCH).dispatch(
// Get the stream codec from the specific object
ExampleObject::streamCodec,
// Get the stream codec from the registry object
Function.identity()
);
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。