包 ID
Play 状态 · 逐版本重排 · 返回总条目
拉弓包本身没有包 ID。它在线上是九个原版包各自带着自己的 ID 连续发出;这些 ID 在 Play 状态里每个版本都会重排,所以只对具体版本有意义。下表给出 1.21.4 的取值与协议号对照。
协议号
握手阶段客户端报的版本号,用来确认两份表是不是同一版:
| 版本 | 协议号 | 备注 |
|---|---|---|
| 1.17 | 755 | Ping / Pong 接管事务确认的第一版 |
| 1.17.1 | 756 | |
| 1.18.2 | 758 | 1.18 与 1.18.1 为 757 |
| 1.19.4 | 762 | 1.19 至 1.19.3 依次为 759 至 761 |
| 1.20.4 | 765 | 1.20.2 为 764,1.20 与 1.20.1 为 763 |
| 1.21.4 | 769 | 1.20.6 为 766,1.21 与 1.21.1 为 767 |
1.21.4 的取值
九个包在 1.21.4 的 Play 状态里的 ID。客户端一侧按「服务端 → 客户端」列,服务端一侧按「客户端 → 服务端」列:
| 简称 | 方向 | 类名 | ID(十六进制) | ID(十进制) |
|---|---|---|---|---|
S32 | 服务端 → 客户端 | ClientboundPingPacket | 0x37 | 55 |
S08 | 服务端 → 客户端 | ClientboundPlayerPositionPacket | 0x42 | 66 |
C0F | 客户端 → 服务端 | ServerboundPongPacket | 0x2B | 43 |
C07 | 客户端 → 服务端 | ServerboundPlayerActionPacket | 0x27 | 39 |
C08 | 客户端 → 服务端 | ServerboundUseItemPacket | 0x3D | 61 |
C06 | 客户端 → 服务端 | ServerboundMovePlayerPacket.PosRot | 0x1D | 29 |
C04 | 客户端 → 服务端 | ServerboundMovePlayerPacket.Pos | 0x1C | 28 |
C03 | 客户端 → 服务端 | ServerboundMovePlayerPacket.StatusOnly | 0x1F | 31 |
C09 | 客户端 → 服务端 | ServerboundSetCarriedItemPacket | 0x33 | 51 |
同一个移动包族还有一个只带朝向的变体 ServerboundMovePlayerPacket.Rot,在 1.21.4 里是 0x1E(30)。拉弓包不用它,但切包时会碰到。
为什么每版都变
Play 状态的包 ID 是「注册顺序」而不是固定常量:新增一个包,排在它后面的全部加一。1.17 到 1.21.4 之间几乎每个小版本都动过这张表,所以网上抄来的 ID 换个版本就对不上。
唯一稳定的是字段结构:S32 永远只有一个 int,C0F 也只有一个 int,C08 带手与朝向。判断一个包是不是它,看结构与方向,不要看 ID。
怎么查自己那一版
- 取当版本的协议表:PrismarineJS 的
minecraft-data里data/pc/<版本>/proto.yml的play.toServer.types与play.toClient.types,包名在packet:下的顺序就是 ID。 - 或者反着来:抓一个包,按字节流切出第一帧,读第一个 VarInt,那就是 ID;再对照该版本的表查名字。
- 改版本后重跑一遍,别缓存。
一帧长什么样
以 1.21.4 的一次使用物品为例,结构是「长度 VarInt + 包 ID VarInt + 载荷」:
[0x06] 长度:6 字节
[0x3D] 包 ID:ServerboundUseItemPacket
[0x00] Hand:主手
[....] Yaw / Pitch 与序列号,按该版本字段表读
换成 1.20.4,长度与 ID 都会变,字段顺序不变。切包方法与压缩阈值见抓包。