拉弓包(Pull Bow)

复合包 · 俗称 ClientboundPullBowPacket / ServerboundPullBowPacket · 最后更新:

拉弓包(Pull Bow)是 Minecraft Java 1.17+ 里的复合包:C08、半个 C07、移动包族与 S32/C0F 序号往返在同一 tick 内连续发出,服务端按到达顺序逐个处理。生效时把之前没有射出去的那支箭补射出去,随后可以获得一段检测豁免。

Pull Bow
中文名拉弓包
英文名Pull Bow
类型复合包(由多个原版包在同一 tick 内按顺序组成)
俗称类名ClientboundPullBowPacket
ServerboundPullBowPacket
社区叫法;官方映射中没有这两个类
别名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 复合载荷说(主流)、握手说以及认为拉弓包是两者结合的融合说。前两者在“拉弓包是否独立存在”这一点上互相矛盾,融合说则把两者拆成传输层与语义层分别处理。

Play

拉弓包只存在于 Play 状态。它没有独立分配的包 ID:线上看到的是组成它的几个原版包各自带着自己的 ID 连续发出,两个方向共用同一套字段布局,仅绑定方向不同。它的字段数量明显多于同类交互包,原因就在下面这张构成表。

以下字段表按握手说 / 融合说整理。C08 复合载荷说不承认存在独立的 Clientbound 一侧,并认为 Serverbound 一侧只是 C08 与 C07 先后处理的结果,Sequence 等握手用字段在该说法中没有意义。

构成

拉弓包的每一项内容都能在某个原版包里找到出处。它不是新造了字段,而是几个包在同一个 tick 里连续发出,本页把这一串的字段放在一张表里列出——这也是它字段看起来比同类包多的唯一原因。

序组成包高版本类名方向贡献的内容作用
1S32ClientboundPingPacket服务端 → 客户端Sequence序号往返的起点。这是 Sequence 唯一的真实来源
2C0FServerboundPongPacket客户端 → 服务端Sequence(回带)把 S32 的 id 原样送回。必须异步,否则会被主线程队列挤到下一个 tick
3C08ServerboundUseItemPacket客户端 → 服务端—重发一次使用物品,把服务端侧的 getTicksUsingItem() 归零
4C07ServerboundPlayerActionPacket客户端 → 服务端—释放使用物品(RELEASE_USE_ITEM)。所谓“半个”指的就是它:只有动作字段有效,位置与朝向是占位值,状态沿用前面的包
5C04 / C03ServerboundMovePlayerPacket客户端 → 服务端X、Y、Z、Yaw、Pitch、On Ground位移与朝向。复合包里这几项就是这一族包字段的并置
6S08ClientboundPlayerPositionPacket服务端 → 客户端位置基准服务端认为位置对不上时下发;拉弓包要求它延后应用
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 IDStateBound ToField NameField TypeNotes
独立 ID
无

由组成包
各自的 ID 承载
Play Client XDouble服务端记录的玩家绝对坐标;来源 C04/C03 族。
YDouble
ZDouble
YawFloat水平视角,单位为度。
PitchFloat垂直视角,单位为度。
On GroundBoolean玩家是否在地面,直接取自 C03 的 onGround 位。
Charge TicksVarInt服务端侧该玩家 getTicksUsingItem() 的值,不是包字段。下发时恒 ≥ 20。
SequenceVarInt来自 S32 的 id;客户端必须用 C0F 原样带回。

Serverbound

Pull Bow

收到首个 Clientbound Pull Bow 后的 1 tick 内,客户端有概率可以主动发出。可以只发半个(见半包)。主手不是弓、Charge Ticks 小于 20 或 Sequence 不匹配的包会被服务端丢弃。

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)。

前置条件

下列两条是必需的,缺一不可:

另有两条属于握手流程,C08 复合载荷说不需要:

触发是概率性的:同一 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 内被重置过,紧接着的释放与位移都按新状态结算,这段时间里的判定不会按“连续两次满蓄力拉弓”累积。

注意:GrimAC 貌似已经有针对这个发包序列的检测,实际使用时需要注意。

握手说

握手说同样承认拉弓包不是一个注册的类,但认为它是一个真实存在的独立流程:有服务端下发的首包、有 1 tick 的窗口,需要客户端在同一个 tick 内与服务端完成一次固定顺序的“握手”才会出现。该说法是流传最早的一版描述。

顺序数据包方向数量说明
0(无)本地状态— 主手持弓,等待 getTicksUsingItem() ≥ 20。
1S32服务端 → 客户端一个 序号往返的起点。它带一个 id,这个 id 就是拉弓包的 Sequence。
2C0F客户端 → 服务端一个 把 S32 的 id 原样送回。必须异步发送——走主线程队列会被挤到下一个 tick,往返就跨了 tick。
3C08客户端 → 服务端一个 重发一次使用物品,把服务端侧的 getTicksUsingItem() 归零。
4C07客户端 → 服务端半个 紧跟着发出半个 C07(RELEASE_USE_ITEM)。
5C06客户端 → 服务端一个 可选。仅在某些特殊情况下需要,用于与服务端同步位置。
6S08服务端 → 客户端一个 位置校正。收到后不立即应用,推迟 15 – 20 tick 再处理(见C08 说“维持”一节)。
7ClientboundPullBowPacket服务端 → 客户端一个 序列被接受后,服务端下发首个拉弓包(俗称),同时打开 1 tick 的发送窗口。
8ServerboundPullBowPacket客户端 → 服务端半个或一个 窗口内有概率可以主动发出。字段需填写当前的 pos、rot、ground。

速查:蓄力 ≥ 20 tick → S32(收)→ 异步 C0F(发)→ C08(发)→ 半个 C07(发)→ [C06(发,可选)] → S08(收,推迟应用)→ ClientboundPullBowPacket(收)→ ServerboundPullBowPacket(发,概率)

约束

半包与 C06 失败

握手说中最常见的失败情形是:已经补发了 C06,服务端却没有下发拉弓包。按该说法的解释,这通常不是 C06 的问题,而是第 4 步的半个 C07 没有和 S32、C0F、C08 落在同一个 tick 里,服务端认为握手不完整,于是不下发首包。该说法把这类失败归因于服务端实现差异:不同服务端对同一 tick 内连续包的处理窗口宽窄不一,同样的发送节奏在某些环境上成立、在另一些上不成立。

握手说里有两处“半个”,不要混淆:

“半个”在协议里有现成的含义:一个包只带一部分参数,另一部分与上一个包一样,由接收方沿用。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 的先后处理,等价于一轮级联
方向仅 C2SS2C 与 C2SS2C 与 C2S
关键数据包C08、C07、C04、C03、S08S32、C0F、C08、C07、C06全部
“半个”指什么只带动作字段、其余沿用前一个包的 C07握手中的半个 C07,以及窗口内回发的半个拉弓包同握手说
能否解释不射箭能(服务端侧 getTicksUsingItem() 被重发的 C08 归零)不能沿用 C08 说
如何处理 S08推迟 15 – 20 tick 再应用作为位置校正同 tick 收下即时异步回应 S32,推迟应用位置
概率性的来源C08 与半个 C07 能否赶上同一个 tick1 tick 窗口内能否发出两者叠加

历史

最早的描述只有握手说里的前三步(S32 → 异步 C0F → 半个 C07)以及随后服务端下发的首包,当时拉弓包被当作纯粹的 S2C 包,这也是 PullBowS2CPacket 这一写法流传最广的原因。后来客户端在拿到首包后主动发出的那一侧被补进来,拉弓包由此改归双向;C08 复合载荷说出现得更晚,把重点从流程挪到了 C08 与 C07 的先后处理上。

本页沿用社区常见的 1.8 时代简称。它们在高版本(Mojang 官方映射)中对应的数据包如下。高版本的数据包 ID 随版本变动,1.21.4 的取值见包 ID 对照。

简称高版本名称高版本类名绑定到原版用途
S32Ping (play)ClientboundPingPacket客户端服务端发一个 id,客户端用 C0F 原样送回;1.17 起承担原先的事务确认
S08Synchronize Player PositionClientboundPlayerPositionPacket客户端服务端设置玩家位置与视角
C0FPong (play)ServerboundPongPacket服务端回应服务端的 Ping。1.8 时代同一编号是事务确认;1.17 起改为 Ping / Pong
C07Player ActionServerboundPlayerActionPacket服务端挖掘、丢弃物品、释放使用物品(如松开弓)
C06Set Player Position and RotationServerboundMovePlayerPacket.PosRot服务端客户端上报位置与视角
C08Use Item / Use Item OnServerboundUseItemPacket
ServerboundUseItemOnPacket
服务端使用手中物品(如开始拉弓)、对方块使用物品
C03Set Player Movement FlagsServerboundMovePlayerPacket.StatusOnly服务端仅上报 onGround 等状态位;社区也用它泛指整族移动包
C04Set Player PositionServerboundMovePlayerPacket.Pos服务端客户端上报位置(1.8 中带视角的版本为 C06)
C09Set Held ItemServerboundSetCarriedItemPacket服务端切换快捷栏选中的槽位

命名

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 貌似已经有针对这个发包序列的检测,实际使用需要注意。

其余十条问答见常见问题。