拉弓包(Pull Bow)
复合包 · 俗称 ClientboundPullBowPacket / ServerboundPullBowPacket · 最后更新:
拉弓包(Pull Bow)是 Minecraft Java 1.17+ 里的复合包:C08、半个 C07、移动包族与 S32/C0F 序号往返在同一 tick 内连续发出,服务端按到达顺序逐个处理。生效时把之前没有射出去的那支箭补射出去,随后可以获得一段检测豁免。
| 中文名 | 拉弓包 |
|---|---|
| 英文名 | Pull Bow |
| 类型 | 复合包(由多个原版包在同一 tick 内按顺序组成) |
| 俗称类名 | ClientboundPullBowPacketServerboundPullBowPacket社区叫法;官方映射中没有这两个类 |
| 别名 | PullBowS2CPacket / PullBowC2SPacket |
| 状态 | Play |
| 方向 | 双向(S2C 与 C2S);C08 说认为仅 C2S |
| 独立 ID | 无 线上看到的是几个原版包各自带着自己的 ID 连续发出 |
| 组成 | C08、C07、C03/C04、S32/C0F、S08(见构成) |
| 适用版本 | Java 1.17+ 序号往返依赖 1.17 起才有的 Ping / Pong |
| 前置条件 | 主手持弓,getTicksUsingItem() ≥ 20 |
| 触发窗口 | 1 tick(50 ms,握手说 / 融合说) |
| 字段 | pos X/Y/Z、rot、ground 等 |
| 效果 | 把之前没有射出去的那支箭补射出去;随后获得一段检测豁免 GrimAC 已有针对该发包的检测 |
拉弓包(英文名 Pull Bow)是《我的世界》(Minecraft)Java 版 1.17+ 中的一个复合包。它不是一个单独注册、单独编号的数据包类型——官方映射里找不到名为 Pull Bow 的类;它是同一个 tick 内连续发出的若干个原版包,被服务端按到达顺序逐个处理的那一串,社区把这一串合起来称为“拉弓包”(见构成)。
因为不是一个类型,它也就没有正式的类名。玩家社区为它编了一对俗称类名:发往客户端的一侧叫 ClientboundPullBowPacket(也写作 PullBowS2CPacket),客户端发出的一侧叫 ServerboundPullBowPacket(也写作 PullBowC2SPacket)。这两个名字只存在于社区讨论里,官方映射中对应的只是它的组成包。按 C08 复合载荷说,只有 C2S 一侧成立;握手说与融合说则承认两侧都有内容(见理论模型)。
拉弓包不会在常规游戏流程中出现。玩家必须主手持弓,并等待 getTicksUsingItem() ≥ 20(即持续拉弓满 20 tick);在服务端侧完成一次序号往返之后,客户端便有概率在 1 tick 内把这一串包一起发出。它的字段之所以比同类交互包多,是因为它同时并置了几个包的字段,而不是新设计了字段(见 Play 一节的字段表)。它没有独立分配的 ID——线上看到的是这几个包各自带着自己的 ID 连续发出。
关于拉弓包的工作原理,社区目前有三种说法:C08 复合载荷说(主流)、握手说以及认为拉弓包是两者结合的融合说。前两者在“拉弓包是否独立存在”这一点上互相矛盾,融合说则把两者拆成传输层与语义层分别处理。
- 构成七个原版包逐项溯源
- Clientbound 一侧八个字段与 Sequence
- Serverbound 一侧半包规则与三项校验
- 握手序列九步顺序与失败排查
- 三种说法九项逐条对比
- 抓包按 tick 对齐看这一串
- 包 ID 对照1.21.4 实测与协议号
- 半个包C07 与 C04 两种写法
- 反作弊豁免从哪来
- 术语表十二条定义
- 常见问题十四条问答
Play
拉弓包只存在于 Play 状态。它没有独立分配的包 ID:线上看到的是组成它的几个原版包各自带着自己的 ID 连续发出,两个方向共用同一套字段布局,仅绑定方向不同。它的字段数量明显多于同类交互包,原因就在下面这张构成表。
以下字段表按握手说 / 融合说整理。C08 复合载荷说不承认存在独立的 Clientbound 一侧,并认为 Serverbound 一侧只是 C08 与 C07 先后处理的结果,Sequence 等握手用字段在该说法中没有意义。
构成
拉弓包的每一项内容都能在某个原版包里找到出处。它不是新造了字段,而是几个包在同一个 tick 里连续发出,本页把这一串的字段放在一张表里列出——这也是它字段看起来比同类包多的唯一原因。
| 序 | 组成包 | 高版本类名 | 方向 | 贡献的内容 | 作用 |
|---|---|---|---|---|---|
| 1 | S32 | ClientboundPingPacket | 服务端 → 客户端 | Sequence | 序号往返的起点。这是 Sequence 唯一的真实来源 |
| 2 | C0F | ServerboundPongPacket | 客户端 → 服务端 | Sequence(回带) | 把 S32 的 id 原样送回。必须异步,否则会被主线程队列挤到下一个 tick |
| 3 | C08 | ServerboundUseItemPacket | 客户端 → 服务端 | — | 重发一次使用物品,把服务端侧的 getTicksUsingItem() 归零 |
| 4 | C07 | ServerboundPlayerActionPacket | 客户端 → 服务端 | — | 释放使用物品(RELEASE_USE_ITEM)。所谓“半个”指的就是它:只有动作字段有效,位置与朝向是占位值,状态沿用前面的包 |
| 5 | C04 / C03 | ServerboundMovePlayerPacket | 客户端 → 服务端 | X、Y、Z、Yaw、Pitch、On Ground | 位移与朝向。复合包里这几项就是这一族包字段的并置 |
| 6 | S08 | ClientboundPlayerPositionPacket | 服务端 → 客户端 | 位置基准 | 服务端认为位置对不上时下发;拉弓包要求它延后应用 |
| 7 | 本地状态 | LivingEntity.getTicksUsingItem() | — | Charge Ticks | 不是包字段,来自客户端本地计时 |
对应关系:Sequence ← S32/C0F,pos/rot/ground ← C04/C03 族,Charge Ticks ← 本地计时,S08 提供位置基准,而 C08 与半个 C07 负责解释“为什么这次松手不射箭”。没有一项是新造的字段。
Clientbound
Pull Bow
前置条件满足且触发序列被服务端接受后,由服务端发送给客户端。客户端收到的首个 Pull Bow 同时负责打开 1 tick 的发送窗口,客户端此后才可能发出 Serverbound 一侧的 Pull Bow。
| Packet ID | State | Bound To | Field Name | Field Type | Notes |
|---|---|---|---|---|---|
| 独立 ID 无 由组成包 各自的 ID 承载 |
Play | Client | X | Double | 服务端记录的玩家绝对坐标;来源 C04/C03 族。 |
| Y | Double | ||||
| Z | Double | ||||
| Yaw | Float | 水平视角,单位为度。 | |||
| Pitch | Float | 垂直视角,单位为度。 | |||
| On Ground | Boolean | 玩家是否在地面,直接取自 C03 的 onGround 位。 | |||
| Charge Ticks | VarInt | 服务端侧该玩家 getTicksUsingItem() 的值,不是包字段。下发时恒 ≥ 20。 | |||
| Sequence | VarInt | 来自 S32 的 id;客户端必须用 C0F 原样带回。 |
Serverbound
Pull Bow
收到首个 Clientbound Pull Bow 后的 1 tick 内,客户端有概率可以主动发出。可以只发半个(见半包)。主手不是弓、Charge Ticks 小于 20 或 Sequence 不匹配的包会被服务端丢弃。
| Packet ID | State | Bound To | Field Name | Field Type | Notes |
|---|---|---|---|---|---|
| 独立 ID 无 由组成包 各自的 ID 承载 |
Play | Server | X | Double | 发包时玩家的绝对坐标;来源 C04/C03 族。服务端据此校验发包位置。 |
| Y | Double | ||||
| Z | Double | ||||
| Yaw | Float | 水平视角,单位为度。发半个时省略,服务端沿用上一个移动包的值。 | |||
| Pitch | Float | 垂直视角,单位为度。发半个时省略,服务端沿用上一个移动包的值。 | |||
| On Ground | Boolean | 发包时是否在地面。每个移动包都带这一位,半个也不例外。 | |||
| Charge Ticks | VarInt | 发包时客户端本地 getTicksUsingItem() 的值,不是包字段。 | |||
| Sequence | VarInt | 必须等于打开窗口的那个 Clientbound 一侧回带过的 Sequence(即那次 S32 的 id)。 |
前置条件
下列两条是必需的,缺一不可:
- 主手持弓:弓必须在主手。副手持弓不计入。
- 蓄力:客户端本地的
getTicksUsingItem() ≥ 20,即持续拉弓满 20 tick(1 秒)。这是原版弓的满蓄力时间:BowItem.getPowerForTime(20)恰好为 1.0。蓄力不足时序列不会开始。
另有两条属于握手流程,C08 复合载荷说不需要:
- 首包在先:客户端不能凭空发出拉弓包,必须先拿到服务端下发的首个拉弓包。
- 窗口:首包到达后只有 1 tick(50 ms)的发送窗口。
触发是概率性的:同一 tick 内的对齐能否成功,取决于客户端在那一 tick 里还剩多少余量。各模型对概率来源的解释不同,见对比表。
理论模型
以下三种说法按社区接受程度排列。
C08 复合载荷说(主流)
这是目前最主流的说法。该模型认为拉弓包不是一个单独设计的数据包,而是 C08(开始拉弓)与 C07(松手)在同一个 tick 内先后到达、服务端按顺序处理它们产生的结果。两个包各自独立执行,但效果只有放在一起才成立,因此称为复合载荷。
顺序
正常射箭时,C08 与 C07 之间至少隔着 20 tick 的蓄力时间,服务端分别处理。在客户端本地 getTicksUsingItem() ≥ 20 之后的某个 tick,如果客户端重发了一次 C08,又紧跟着发出半个 C07,两者有机会落进同一个 tick(能否赶上就是概率的来源,蓄力越久、客户端越有余量对齐这两个包)。服务端不会合并它们:每个包都带长度前缀、各自解码,进入主线程队列后按到达顺序逐个执行——先执行 C08 重新开始使用物品,再执行 C07 释放。拉弓包的效果完全来自这个先后关系:C07 处理时,服务端看到的已经是刚被 C08 重置过的状态。
效果
生效时,之前没有射出去的那支箭会真正射出去。这一轮里服务端侧的蓄力计时刚被重发的 C08 归零,半个 C07 在 BowItem.releaseUsing() 中算出的力度为 0(低于 0.1),所以松手那一刻没有生成箭矢;窗口内回发之后,这支箭在本轮补射出去,落点按满蓄力计算——BowItem.getPowerForTime(20) 恰为 1.0。
随后可以获得反作弊的一部分检测豁免。服务端手上的使用物品状态在这个 tick 内被重置过,紧接着的释放与位移都按新状态结算,这段时间里的判定不会按“连续两次满蓄力拉弓”累积。
握手说
握手说同样承认拉弓包不是一个注册的类,但认为它是一个真实存在的独立流程:有服务端下发的首包、有 1 tick 的窗口,需要客户端在同一个 tick 内与服务端完成一次固定顺序的“握手”才会出现。该说法是流传最早的一版描述。
| 顺序 | 数据包 | 方向 | 数量 | 说明 |
|---|---|---|---|---|
| 0 | (无) | 本地状态 | — | 主手持弓,等待 getTicksUsingItem() ≥ 20。 |
| 1 | S32 | 服务端 → 客户端 | 一个 | 序号往返的起点。它带一个 id,这个 id 就是拉弓包的 Sequence。 |
| 2 | C0F | 客户端 → 服务端 | 一个 | 把 S32 的 id 原样送回。必须异步发送——走主线程队列会被挤到下一个 tick,往返就跨了 tick。 |
| 3 | C08 | 客户端 → 服务端 | 一个 | 重发一次使用物品,把服务端侧的 getTicksUsingItem() 归零。 |
| 4 | C07 | 客户端 → 服务端 | 半个 | 紧跟着发出半个 C07(RELEASE_USE_ITEM)。 |
| 5 | C06 | 客户端 → 服务端 | 一个 | 可选。仅在某些特殊情况下需要,用于与服务端同步位置。 |
| 6 | S08 | 服务端 → 客户端 | 一个 | 位置校正。收到后不立即应用,推迟 15 – 20 tick 再处理(见C08 说“维持”一节)。 |
| 7 | ClientboundPullBowPacket | 服务端 → 客户端 | 一个 | 序列被接受后,服务端下发首个拉弓包(俗称),同时打开 1 tick 的发送窗口。 |
| 8 | ServerboundPullBowPacket | 客户端 → 服务端 | 半个或一个 | 窗口内有概率可以主动发出。字段需填写当前的 pos、rot、ground。 |
速查:蓄力 ≥ 20 tick → S32(收)→ 异步 C0F(发)→ C08(发)→ 半个 C07(发)→ [C06(发,可选)] → S08(收,推迟应用)→ ClientboundPullBowPacket(收)→ ServerboundPullBowPacket(发,概率)
约束
- 时间:第 1 至第 5 步必须落在同一个 tick 内;第 8 步必须落在第 7 步之后的 1 tick 内。
- 顺序:先收
S32并用C0F回带 id,再发C08与半个C07,不可调换。 - 异步:
C0F必须异步发送。这是整个序列里唯一不能等主线程的一步。 - 数量:
C0F为一个,C07为半个。 - 位置:第 6 步的
S08收到后推迟 15 – 20 tick 再应用,与“第 1 至第 5 步同 tick”的要求并不冲突——推迟的是应用,不是接收。 - 位置同步:特殊情况下需要一个
C06同步位置,常规流程不需要。
半包与 C06 失败
握手说中最常见的失败情形是:已经补发了 C06,服务端却没有下发拉弓包。按该说法的解释,这通常不是 C06 的问题,而是第 4 步的半个 C07 没有和 S32、C0F、C08 落在同一个 tick 里,服务端认为握手不完整,于是不下发首包。该说法把这类失败归因于服务端实现差异:不同服务端对同一 tick 内连续包的处理窗口宽窄不一,同样的发送节奏在某些环境上成立、在另一些上不成立。
握手说里有两处“半个”,不要混淆:
- 半个
C07(第 4 步):握手的一部分,发生在首包之前。 - 半个拉弓包(第 8 步):首包之后窗口内的回发,移动部分用只带坐标的
C04(ServerboundMovePlayerPacket.Pos)而不是带朝向的C06:省略rot,只带pos与ground,服务端沿用上一个移动包的朝向。
“半个”在协议里有现成的含义:一个包只带一部分参数,另一部分与上一个包一样,由接收方沿用。C07 释放使用物品时只有动作字段有意义,坐标与朝向固定填占位值;C04 不带朝向,朝向沿用上一个包。两处“半个”都是这个意思。
握手说本身不解释这一刻为什么没有生成箭矢,这一层由 C08 说补上。
融合说
也有说法称,拉弓包是上述两者的结合:握手说描述的是拉弓包的传输层(包怎么来、怎么发),C08 复合载荷说描述的是它的语义层(包到了之后发生什么)。按融合说,握手序列第 8 步发出的 C2S 拉弓包,其 pos 是 C08 说中这一轮级联 C04 的终点坐标,ground 就是“onGround 覆写”,因此一个拉弓包等价于一整轮级联。
两种说法表面上最大的冲突在序号与位置:握手说要求 S32 的往返在同一个 tick 内完成,C08 说却要求把 S08 推迟 15 – 20 tick 再应用。融合说的解释是两者针对的是不同的东西——推迟的是应用拉回位置,而 C0F 回应的是 S32、不经过主线程的处理队列、立即发出,这正是握手说要求 C0F 必须异步发送的原因。
至于“拉弓包是否独立存在”,融合说认为:它在线路上的呈现是双向的、带 Sequence 的一串连续包,但内容就是 C08 与 C07 的先后处理,所以这一串也沿用 C08 说的解释——它是几个包在同一个 tick 里的连续发送,不是协议分配的独立类型。融合说能同时解释字段多、双向、概率性这三个特征。
| 对比项 | C08 复合载荷说 | 握手说 | 融合说 |
|---|---|---|---|
| 接受度 | 最广 | 次之 | 较少 |
| 拉弓包是否独立存在 | 否,是 C08 与 C07 先后处理的结果 | 是独立流程(但不是独立的类) | 线路上是一个独立流程,内容是 C08 → C07 的先后处理,等价于一轮级联 |
| 方向 | 仅 C2S | S2C 与 C2S | S2C 与 C2S |
| 关键数据包 | C08、C07、C04、C03、S08 | S32、C0F、C08、C07、C06 | 全部 |
| “半个”指什么 | 只带动作字段、其余沿用前一个包的 C07 | 握手中的半个 C07,以及窗口内回发的半个拉弓包 | 同握手说 |
| 能否解释不射箭 | 能(服务端侧 getTicksUsingItem() 被重发的 C08 归零) | 不能 | 沿用 C08 说 |
如何处理 S08 | 推迟 15 – 20 tick 再应用 | 作为位置校正同 tick 收下 | 即时异步回应 S32,推迟应用位置 |
| 概率性的来源 | C08 与半个 C07 能否赶上同一个 tick | 1 tick 窗口内能否发出 | 两者叠加 |
历史
最早的描述只有握手说里的前三步(S32 → 异步 C0F → 半个 C07)以及随后服务端下发的首包,当时拉弓包被当作纯粹的 S2C 包,这也是 PullBowS2CPacket 这一写法流传最广的原因。后来客户端在拿到首包后主动发出的那一侧被补进来,拉弓包由此改归双向;C08 复合载荷说出现得更晚,把重点从流程挪到了 C08 与 C07 的先后处理上。
相关数据包
本页沿用社区常见的 1.8 时代简称。它们在高版本(Mojang 官方映射)中对应的数据包如下。高版本的数据包 ID 随版本变动,1.21.4 的取值见包 ID 对照。
| 简称 | 高版本名称 | 高版本类名 | 绑定到 | 原版用途 |
|---|---|---|---|---|
S32 | Ping (play) | ClientboundPingPacket | 客户端 | 服务端发一个 id,客户端用 C0F 原样送回;1.17 起承担原先的事务确认 |
S08 | Synchronize Player Position | ClientboundPlayerPositionPacket | 客户端 | 服务端设置玩家位置与视角 |
C0F | Pong (play) | ServerboundPongPacket | 服务端 | 回应服务端的 Ping。1.8 时代同一编号是事务确认;1.17 起改为 Ping / Pong |
C07 | Player Action | ServerboundPlayerActionPacket | 服务端 | 挖掘、丢弃物品、释放使用物品(如松开弓) |
C06 | Set Player Position and Rotation | ServerboundMovePlayerPacket.PosRot | 服务端 | 客户端上报位置与视角 |
C08 | Use Item / Use Item On | ServerboundUseItemPacketServerboundUseItemOnPacket | 服务端 | 使用手中物品(如开始拉弓)、对方块使用物品 |
C03 | Set Player Movement Flags | ServerboundMovePlayerPacket.StatusOnly | 服务端 | 仅上报 onGround 等状态位;社区也用它泛指整族移动包 |
C04 | Set Player Position | ServerboundMovePlayerPacket.Pos | 服务端 | 客户端上报位置(1.8 中带视角的版本为 C06) |
C09 | Set Held Item | ServerboundSetCarriedItemPacket | 服务端 | 切换快捷栏选中的槽位 |
命名
ClientboundPullBowPacket 与 ServerboundPullBowPacket 是社区俗称,沿用 Mojang 的 Clientbound / Serverbound 前缀习惯造出来的:前者表示发往客户端的一侧,后者表示客户端发出的一侧。社区也按 S2C / C2S 的习惯把它们写作 PullBowS2CPacket(Server-to-Client)与 PullBowC2SPacket(Client-to-Server)。不区分方向时统称 PullBowPacket 或“拉弓包”。
这四个名字都不是官方映射里的类。拉弓包是复合包,官方映射里只有它的组成包:ServerboundUseItemPacket、ServerboundPlayerActionPacket、ServerboundMovePlayerPacket、ClientboundPingPacket / ServerboundPongPacket、ClientboundPlayerPositionPacket。所以在源码里 grep PullBow 什么也找不到——这是正常的,它从来不是一个单独的类,而是几个类在同一 tick 内被依次处理时,社区给那一串起的名字。
蓄力计数同理:本文使用 Mojang 映射中的 LivingEntity.getTicksUsingItem()(Yarn 写作 getItemUseTime()),即“已使用当前物品的 tick 数”。旧资料中常见的 BowChargeTicks 并不是原版字段,只是社区对这个值的俗称。
常见问题
- 拉弓包(Pull Bow)是什么?
- Minecraft Java 版 1.17+ 中的一个复合包:由
S32/C0F、C08、半个C07与移动包族在同一 tick 内依次发出、由服务端按顺序处理,不是单独注册的包类型。 - 拉弓包是 S2C 包还是 C2S 包?
- 两侧都有内容:S2C 一侧是序号往返(
S32/C0F)与位置校正(S08),C2S 一侧是C08、半个C07与移动包族。C08 复合载荷说认为只有 C2S 一侧成立。 - 触发的前置条件是什么?
- 主手必须持弓,并且
getTicksUsingItem() ≥ 20。任一条件不满足时,整个序列不会开始。握手说和融合说还要求先收到服务端下发的首包。 - 生效之后会发生什么?
- 之前没有射出去的那支箭会真正射出去,落点按满蓄力计算;随后可以获得反作弊的一部分检测豁免。GrimAC 貌似已经有针对这个发包序列的检测,实际使用需要注意。
其余十条问答见常见问题。