Serverbound 一侧

半包规则 · 服务端校验 · 返回总条目

客户端在收到首个 Clientbound 一侧的东西之后 1 tick 内,有概率可以主动发出这一侧。它同样不是新注册的包类型,字段来自 C08、半个 C07 与 C04/C03 族。

Packet IDStateBound ToField NameField TypeNotes
独立 ID
无

由组成包
各自的 ID 承载
Play Server XDouble发包时玩家的绝对坐标;来源 C04/C03 族。服务端据此校验发包位置。
YDouble
ZDouble
YawFloat水平视角,单位为度。发半个时省略,服务端沿用上一个移动包的值。
PitchFloat垂直视角,单位为度。发半个时省略,服务端沿用上一个移动包的值。
On GroundBoolean发包时是否在地面。每个移动包都带这一位,半个也不例外。
Charge TicksVarInt发包时客户端本地 getTicksUsingItem() 的值,不是包字段。
SequenceVarInt必须等于打开窗口的那个 Clientbound 一侧回带过的 Sequence(即那次 S32 的 id)。

“半个”是什么意思

“半个”不是把一个包截掉一半发出去——协议里每个包都带长度前缀、按帧解码,少一个字节服务端都会读错。它指的是只带一半参数,另一半和上一个包一样,由服务端沿用。原版移动包本来就是这么设计的:ServerboundMovePlayerPacket 有四个变体,各自有独立的包 ID,变体本身就决定带哪些字段。

写法移动部分用的包线上字段字节数没带的部分
一个 C06(PosRot) X, Y, Z, Yaw, Pitch, On Ground 8 × 3 + 4 × 2 + 1 = 33 无,全部由客户端给出
半个 C04(Pos) X, Y, Z, On Ground 8 × 3 + 1 = 25 Yaw、Pitch 沿用上一个移动包的值

Charge Ticks 与 Sequence 不在移动包里:前者是客户端本地计时,后者由 C0F 带回。两种写法里它们都一样。

服务端校验顺序

服务端收到这一侧之后依次检查三项,任何一项不通过就整包丢弃:

  1. 手持物:主手必须仍然是弓。副手持弓不算,中途切槽会使这一次作废。
  2. 蓄力:Charge Ticks ≥ 20。低于 20 的值直接拒绝,不做插值也不做容差。
  3. 配对:Sequence 必须等于窗口打开时那个值。过期或伪造的号一律丢弃。

三项都过了之后,这一轮才算成立——注意服务端并不合并这些包,它们仍按到达顺序逐个执行。这个顺序也解释了为什么大多数失败发生在第 3 项:手持物和蓄力在客户端侧很容易满足,配对号却依赖那一次 S32 / C0F 往返是否落在同一 tick。

为什么需要“半个”

完整布局要把坐标、朝向、状态全部写一遍,共 24 字节的坐标加 8 字节朝向。而在窗口只有 1 tick 的情况下,客户端手里的朝向通常和上一次移动包报上去的完全一样——重复发一遍不增加信息,只增加占用。

半个写法把这部分省掉,换用不带朝向的 C04,服务端沿用上一个包的朝向。代价是客户端必须确信服务端那份状态是新的,而这正是 Sequence 要保证的事:配对号对得上,说明窗口就是刚开的那个,服务端手里那份状态也刚更新过。

与这一侧对应的另一半见 Clientbound 一侧;窗口如何打开见握手序列。