流编解码器(Stream Codecs)
流编解码器(Stream Codecs)
流编解码器是一种序列化工具,用于描述如何将对象存储到流中或从流中读取对象,例如缓冲区。流编解码器主要由原版的网络系统用于同步数据。
注意:由于流编解码器与编解码器(Codecs)类似,因此本文档的格式与编解码器文档相同,以展示它们的相似性。
使用流编解码器
流编解码器通过 StreamCodec#encode 和 StreamCodec#decode 分别将对象编码到流中或从流中解码对象。encode 方法接受流和要编码的对象。decode 方法接受流并返回解码后的对象。通常,流是 ByteBuf、FriendlyByteBuf 或 RegistryFriendlyByteBuf 之一。
// 假设 exampleStreamCodec 表示一个 StreamCodec<ExampleJavaObject>
// 假设 exampleObject 是一个 ExampleJavaObject
// 假设 buffer 是一个 RegistryFriendlyByteBuf
// 将 Java 对象编码到缓冲区流中
exampleStreamCodec.encode(buffer, exampleObject);
// 从缓冲区流中读取 Java 对象
ExampleJavaObject obj = exampleStreamCodec.decode(buffer);注意:除非你手动处理缓冲区对象,否则通常不会直接调用 encode 和 decode。
现有的流编解码器
ByteBufCodecs
ByteBufCodecs 包含某些基本类型和对象的静态流编解码器实例。
| 流编解码器 | Java 类型 |
|---|---|
BOOL | Boolean |
BYTE | Byte |
SHORT | Short |
INT | Integer |
FLOAT | Float |
DOUBLE | Double |
BYTE_ARRAY | byte[]* |
STRING_UTF8 | String** |
TAG | Tag |
COMPOUND_TAG | CompoundTag |
VECTOR3F | Vector3f |
QUATERNIONF | Quaternionf |
GAME_PROFILE | GameProfile |
-
byte[] 可以通过ByteBufCodecs#byteArray 限制为特定数量的值。 -
String 可以通过ByteBufCodecs#stringUtf8 限制为特定数量的字符。
此外,还有一些静态实例使用不同的方法编码和解码基本类型和对象。
无符号短整型(Unsigned Shorts)
UNSIGNED_SHORT 是 SHORT 的替代方案,旨在被视为无符号数。由于 Java 中的数字是有符号的,因此无符号短整型作为整数发送和接收,高两位字节被屏蔽。
可变大小数字(Variable-Sized Number)
VAR_INT 和 VAR_LONG 是流编解码器,其中值被编码为尽可能小。这是通过每次编码七位并使用高位作为标记来实现的,以指示是否有更多数据。对于整数,0 到 228-1 之间的数字将发送比整数字节数更短或相等的字节数;对于长整型,0 到 256-1 之间的数字将发送比长整型字节数更短或相等的字节数。如果你的数字通常在此范围内且通常位于较低端,则应使用这些可变流编解码器。
注意:VAR_INT 是 INT 的替代方案。
可信标签(Trusted Tags)
TRUSTED_TAG 和 TRUSTED_COMPOUND_TAG 分别是 TAG 和 COMPOUND_TAG 的变体,它们具有无限的堆来解码标签,而 TAG 和 COMPOUND_TAG 的限制为 2MiB。可信标签流编解码器应理想地仅用于客户端绑定的数据包,例如原版用于方块实体数据包和实体数据序列化器。
如果需要使用不同的限制,则可以使用 NbtAccounter 并提供给定的大小,使用 ByteBufCodecs#tagCodec 或 #compoundTagCodec。
创建流编解码器
流编解码器可以创建用于读取或写入任何对象到流中。本文档将重点介绍将流作为缓冲区的流编解码器,因为这是其主要用途。
流编解码器有两个泛型:B 表示缓冲区,V 表示对象值。B 通常是以下三种类型之一:ByteBuf、FriendlyByteBuf、RegistryFriendlyByteBuf,每种类型都扩展了前一种类型。FriendlyByteBuf 添加了 Minecraft 特定的读写方法,而 RegistryFriendlyByteBuf 提供了对注册表及其对象的访问。
在构造流编解码器时,B 应是最不特定的缓冲区类型。例如,ResourceLocation 作为字符串发送。由于字符串由常规 ByteBuf 支持,因此其类型应为 StreamCodec<ByteBuf, ResourceLocation>。FriendlyByteBuf 包含写入 ChunkPos 的方法,因此其类型应为 StreamCodec<FriendlyByteBuf, ChunkPos>。Item 需要访问注册表,因此其类型应为 StreamCodec<RegistryFriendlyByteBuf, Item>。
大多数接受流编解码器的方法都查找 ? super B 作为缓冲区类型,这意味着如果缓冲区类型是 RegistryFriendlyByteBuf,则上述所有示例都可以使用。
成员编码器(Member Encoders)
StreamMemberEncoder 是 StreamEncoder 的替代方案,其中编码对象首先出现,缓冲区其次。这通常用于编码对象包含将对象写入缓冲区的实例方法时。可以通过调用 StreamCodec#ofMember 使用 StreamMemberEncoder 创建流编解码器。
// 要创建流编解码器的对象
public class ExampleObject {
// 普通构造函数
public ExampleObject(String arg1, int arg2, boolean arg3) { /* ... */ }
// 流解码器引用
public ExampleObject(ByteBuf buffer) { /* ... */ }
// 流编码器引用
public void encode(ByteBuf buffer) { /* ... */ }
}
// 流编解码器的样子
public static StreamCodec<ByteBuf, ExampleObject> =
StreamCodec.ofMember(ExampleObject::encode, ExampleObject::new);组合(Composites)
流编解码器可以通过 StreamCodec#composite 读取和写入对象。每个组合流编解码器定义了一系列流编解码器和 getter,它们按提供的顺序读取/写入。composite 的重载最多支持六个参数。
每两个参数表示用于读取/写入字段的流编解码器和从对象中获取字段的 getter。最后一个参数是在解码时创建新对象的函数。
// 要创建流编解码器的对象
public record SimpleExample(String arg1, int arg2, boolean arg3) {}
public record RegistryExample(double arg1, Holder<Item> arg2) {}
// 流编解码器
public static final StreamCodec<ByteBuf, SimpleExample> SIMPLE_STREAM_CODEC =
StreamCodec.composite(
// 流编解码器和 getter 对
ByteBufCodecs.STRING_UTF8, SimpleExample::arg1,
ByteBufCodecs.VAR_INT, SimpleExample::arg2,
ByteBufCodecs.BOOL, SimpleExample::arg3,
SimpleExample::new
);
// 由于此对象包含 holder,因此使用 RegistryFriendlyByteBuf
public static final StreamCodec<RegistryFriendlyByteBuf, RegistryExample> REGISTRY_STREAM_CODEC =
StreamCodec.composite(
// 注意,ByteBuf 流编解码器可以在此处使用
ByteBufCodecs.DOUBLE, RegistryExample::arg1,
ByteBufCodecs.holderRegistry(Registries.ITEM), RegistryExample::arg2,
RegistryExample::new
);转换器(Transformers)
流编解码器可以使用映射方法转换为等效或部分等效的表示形式。两个映射方法应用于值,而一个映射方法应用于缓冲区。
map 方法使用两个函数转换值:一个将当前类型转换为新类型,另一个将新类型转换回当前类型。这与编解码器转换器类似。
public static final StreamCodec<ByteBuf, ResourceLocation> STREAM_CODEC =
ByteBufCodecs.STRING_UTF8.map(
// String -> ResourceLocation
ResourceLocation::new,
// ResourceLocation -> String
ResourceLocation::toString
);apply 方法使用 StreamCodec.CodecOperation 转换值。StreamCodec.CodecOperation 接受当前类型的流编解码器并返回新类型的流编解码器。这些通常包装 map 或接受辅助方法。
public static final StreamCodec<ByteBuf, List<ResourceLocation>> STREAM_CODEC =
ResourceLocation.STREAM_CODEC.apply(ByteBufCodecs.list());mapStream 方法使用一个函数转换缓冲区,该函数接受新缓冲区类型并返回当前缓冲区类型。此方法应很少使用,因为大多数带有流编解码器的方法不需要更改缓冲区的类型。
public static final StreamCodec<RegistryFriendlyByteBuf, Integer> STREAM_CODEC =
ByteBufCodecs.VAR_INT.mapStream(buffer -> (ByteBuf) buffer);单位(Unit)
一个流编解码器可以提供代码中的值并编码为空,可以使用 StreamCodec#unit 表示。如果不应通过网络同步任何信息,则这很有用。
警告:单位流编解码器期望任何编码对象必须与指定的单位匹配;否则将抛出错误。因此,所有对象必须具有某些 equals 实现,该实现为单位对象返回 true,或者提供给流编解码器的实例在编码时始终提供。
public static final StreamCodec<ByteBuf, Item> UNIT_STREAM_CODEC =
StreamCodec.unit(Items.AIR);延迟初始化(Lazy Initialized)
有时,流编解码器可能依赖于构造时不存在的数据。在这种情况下,可以使用 NeoForgeStreamCodecs#lazy 让流编解码器在首次读取/写入时构造自身。该方法接受提供的流编解码器。
public static final StreamCodec<ByteBuf, Item> LAZY_STREAM_CODEC =
NeoForgeStreamCodecs.lazy(
() -> StreamCodec.unit(Items.AIR)
);集合(Collections)
可以从对象流编解码器生成集合的流编解码器,使用 collection。collection 接受一个 IntFunction,用于构造空集合、对象的流编解码器以及可选的最大大小。
public static final StreamCodec<ByteBuf, Set<BlockPos>> COLLECTION_STREAM_CODEC =
ByteBufCodecs.collection(
HashSet::new, // 构造具有指定容量的集合
BlockPos.STREAM_CODEC,
256 // 集合最多只能有 256 个元素
);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(
// 列表最多只能有 256 个元素
ByteBufCodecs.list(256)
);映射(Map)
可以使用两个流编解码器通过 ByteBufCodecs#map 生成键和值对象的映射的流编解码器。该函数还接受一个 IntFunction,用于构造空映射,以及可选的最大大小。
public static final StreamCodec<ByteBuf, Map<String, BlockPos>> MAP_STREAM_CODEC =
ByteBufCodecs.map(
HashMap::new, // 构造具有指定容量的映射
ByteBufCodecs.STRING_UTF8,
BlockPos.STREAM_CODEC,
256 // 映射最多只能有 256 个元素
);任一(Either)
可以使用两个流编解码器通过 ByteBufCodecs#either 生成两种不同方法读取/写入某些对象数据的流编解码器。此方法首先读取/写入一个布尔值,指示是读取/写入第一个还是第二个流编解码器。
public static final StreamCodec<ByteBuf, Either<Integer, String>> EITHER_STREAM_CODEC =
ByteBufCodecs.either(
ByteBufCodecs.VAR_INT,
ByteBufCodecs.STRING_UTF8
);ID 映射器(Id Mapper)
在大多数情况下,当通过网络发送信息时,如果对象存在于双方,则发送表示 ID 的整数。表示对象的 ID 减少了需要通过网络同步的信息量。枚举和注册表都使用此方法。
ByteBufCodecs#idMapper 提供了一种方便的方式来发送对象的 ID。它接受两个函数,将对象转换为 int,反之亦然,或者接受一个 IdMap。
// 对于某个枚举
public enum ExampleIdObject {
;
// 获取 ID -> 枚举
public static final IntFunction<ExampleIdObject> BY_ID =
ByIdMap.continuous(
ExampleIdObject::getId,
ExampleIdObject.values(),
ByIdMap.OutOfBoundsStrategy.ZERO
);
ExampleIdObject(int id) { /* ... */ }
}
// 流编解码器的样子
public static final StreamCodec<ByteBuf, ExampleIdObject> ID_STREAM_CODEC =
ByteBufCodecs.idMapper(ExampleIdObject.BY_ID, ExampleIdObject::getId);注意:NeoForge 提供了一个替代方案,用于不缓存枚举值的 ID 映射器,通过 IExtensibleEnum#createStreamCodecForExtensibleEnum。然而,这很少需要在可扩展枚举之外使用。
可选(Optional)
可以通过提供流编解码器给 ByteBufCodecs#optional 生成发送 Optional 包装值的流编解码器。此方法首先读取/写入一个布尔值,指示是否读取/写入对象。
public static final StreamCodec<RegistryFriendlyByteBuf, Optional<DataComponentType<?>>> OPTIONAL_STREAM_CODEC =
DataComponentType.STREAM_CODEC.apply(ByteBufCodecs::optional);注册表对象(Registry Objects)
注册表对象可以通过以下三种方法之一发送到网络:registry、holderRegistry 或 holder。每种方法都接受一个 ResourceKey,表示注册表对象所在的注册表。
警告:自定义注册表必须通过调用 RegistryBuilder#sync 并将值设置为 true 来同步。否则,编码器将抛出异常。
registry 和 holderRegistry 分别返回注册表对象或持有者包装的注册表对象。这些方法发送表示注册表对象的 ID。
// 注册表对象
public static final StreamCodec<RegistryFriendlyByteBuf, Item> VALUE_STREAM_CODEC =
BytebufCodecs.registry(Registries.ITEM);
// 持有者包装的注册表对象
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<Item>> HOLDER_STREAM_CODEC =
BytebufCodecs.holderRegistry(Registries.ITEM);holder 返回持有者包装的注册表对象。此方法发送表示注册表对象的 ID,或者如果提供的持有者是直接引用,则发送注册表对象本身。为此,holder 还接受注册表对象的流编解码器。
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<SoundEvent>> STREAM_CODEC =
ByteBufCodecs.holder(
Registries.SOUND_EVENT, SoundEvent.DIRECT_STREAM_CODEC
);注意:如果持有者不是直接的,则 holder 只会为非同步的自定义注册表抛出异常。
持有者集合(Holder Sets)
可以使用 holderSet 发送标签或持有者包装的注册表对象的集合。它接受一个 ResourceKey,表示注册表对象所在的注册表。
public static final StreamCodec<RegistryFriendlyByteBuf, HolderSet<Item>> HOLDER_SET_STREAM_CODEC =
BytebufCodecs.holderSet(Registries.ITEM);递归(Recursive)
有时,对象可能引用相同类型的对象作为字段。例如,MobEffectInstance 如果存在隐藏效果,则接受一个可选的 MobEffectInstance。在这种情况下,可以使用 StreamCodec#recursive 将流编解码器作为创建流编解码器的函数的一部分提供。
// 定义我们的递归对象
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
)
);分发(Dispatch)
流编解码器可以具有子流编解码器,这些子流编解码器可以根据某些指定类型解码特定对象,通过 StreamCodec#dispatch。这通常与表示类型的注册表对象一起使用,例如 ParticleType 用于 ParticleOptions 或 StatType 用于 Stats。
分发流编解码器首先尝试读取/写入类型对象。然后,使用方法中提供的函数之一读取/写入当前对象。第一个函数接受当前对象并获取要写入值的类型。第二个函数接受类型对象并获取当前对象的流编解码器以读取值。
// 定义我们的对象
public abstract class ExampleObject {
// 定义用于指定对象类型以进行编码的方法
public abstract StreamCodec<? super RegistryFriendlyByteBuf, ? extends ExampleObject> streamCodec();
}
// 假设有一个 ResourceKey<StreamCodec< super RegistryFriendlyByteBuf, ? extends ExampleObject>> DISPATCH
public static final StreamCodec<RegistryFriendlyByteBuf, ExampleObject> DISPATCH_STREAM_CODEC =
ByteBufCodecs.registry(DISPATCH).dispatch(
// 从特定对象获取流编解码器
ExampleObject::streamCodec,
// 从注册表对象获取流编解码器
Function.identity()
);