流编解码器

流编解码器

流编解码器是一种序列化工具,用于描述如何将对象存入流以及从流中读取,例如缓冲区。原版主要通过网络系统使用流编解码器同步数据。

说明 流编解码器与编解码器大体相似,因此本页采用相同的组织方式,便于对照。

使用流编解码器

流编解码器分别使用 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。