拉弓包(Pull Bow)

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

Pull Bow
中文名拉弓包
英文名Pull Bow
类型复合包(由多个原版包在同一 tick 内合并而成)
俗称类名ClientboundPullBowPacket
ServerboundPullBowPacket
社区叫法;官方映射中没有这两个类
别名PullBowS2CPacket / PullBowC2SPacket
状态Play
方向双向(S2C 与 C2S);C08 说认为仅 C2S
数据包 ID0x7B(十进制 123)
1.21.x 复合视图下的编号,逐版本重排,不是协议分配的固定值
组成C08、C07、C03/C04、S32/C0F、S08(见构成)
适用版本Java 1.16+
前置条件主手持弓,getTicksUsingItem() ≥ 20
触发窗口1 tick(50 ms,握手说 / 融合说)
字段pos X/Y/Z、rot、ground 等
最早记录早期反作弊讨论(未具名)

拉弓包(英文名 Pull Bow)是《我的世界》(Minecraft)Java 版 1.16+ 中的一个复合包。它不是一个单独注册、单独编号的数据包类型——官方映射里找不到名为 Pull Bow 的类;它是同一个 tick 内连续发出的若干个原版包,被服务端当作一次动作处理的那一串,社区把这一串合起来称为“拉弓包”(见构成)。

因为不是一个类型,它也就没有正式的类名。玩家社区为它编了一对俗称类名:发往客户端的一侧叫 ClientboundPullBowPacket(Yarn 写法 PullBowS2CPacket),客户端发出的一侧叫 ServerboundPullBowPacket(Yarn 写法 PullBowC2SPacket)。这两个名字只存在于社区讨论里,官方映射中对应的只是它的组成包。按 C08 复合载荷说,只有 C2S 一侧成立;握手说与融合说则承认两侧都有内容(见理论模型)。

拉弓包不会在常规游戏流程中出现。玩家必须主手持弓,并等待 getTicksUsingItem() ≥ 20(即持续拉弓满 20 tick);在服务端侧完成一次序号往返之后,客户端便有概率在 1 tick 内把这一串包一起发出。它的字段之所以比同类交互包多,是因为它同时并置了几个包的字段,而不是新设计了字段(见 Play 一节的字段表)。在 Play 状态的复合视图里它显示为 0x7B(十进制 123);这个数字逐版本重排,不是协议分配的固定值。

关于拉弓包的工作原理,社区目前有三种说法:C08 复合载荷说(主流)、握手说(二流说法)以及认为拉弓包是两者结合的融合说。前两者在“拉弓包是否独立存在”这一点上互相矛盾,融合说则试图把两者拆成不同层面来调和;三者均未得到完整证实。

无论采用哪一种理论模型,拉弓包都因其特殊性质,在发出后可以使发送方在众多反作弊中获得针对该反作弊的部分检测豁免,包括但不限于 GrimAC、ACA、AAC、Polar、Ersa、Intave。

分节深读:构成 · Clientbound 一侧 · Serverbound 一侧 · 握手序列 · 三种理论模型 · 常见问题

Play

拉弓包只存在于 Play 状态。在复合视图里它的编号是 0x7B(十进制 123),逐版本重排。两个方向使用相同的字段布局,仅绑定方向不同。它的字段数量明显多于同类交互包,原因就在下面这张构成表。

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

构成

拉弓包的每一项内容都能在某个原版包里找到出处。它不是新造了字段,而是把几个包的内容并排放进了同一次发送——这也是它字段比同类包多的唯一原因。

序组成包高版本类名方向贡献的内容作用
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
protocol:
0x7B

复合视图
逐版本重排
Play Client XDouble服务端记录的玩家绝对坐标;来源 C04/C03 族。
YDouble
ZDouble
YawFloat水平视角,单位为度。
PitchFloat垂直视角,单位为度。
On GroundBoolean玩家是否在地面,直接取自 C03 的 onGround 位。
Charge TicksVarInt服务端侧该玩家 getTicksUsingItem() 的值,不是包字段。下发时恒 ≥ 20。
HalfBoolean半包标志。Clientbound 方向恒为 false,服务端不会下发半个包。
SequenceVarInt来自 S32 的 id;客户端必须用 C0F 原样带回。

Serverbound

Pull Bow

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

Packet IDStateBound ToField NameField TypeNotes
protocol:
0x7B

复合视图
逐版本重排
Play Server XDouble发包时玩家的绝对坐标;来源 C04/C03 族。服务端据此校验发包位置。
YDouble
ZDouble
YawFloat水平视角,单位为度。Half 为 true 时省略。
PitchFloat垂直视角,单位为度。Half 为 true 时省略。
On GroundBoolean发包时是否在地面。Half 为 true 时省略。
Charge TicksVarInt发包时客户端本地 getTicksUsingItem() 的值,不是包字段。
HalfBoolean为 true 时表示这是半个拉弓包,被省略的字段由服务端用自身状态补全。
SequenceVarInt必须等于打开窗口的那个 Clientbound 一侧回带过的 Sequence(即那次 S32 的 id)。

前置条件

下列两条是三种理论模型都承认的,缺一不可:

另有两条只在握手说与融合说中成立,C08 复合载荷说不需要:

无论哪种模型,触发都是概率性的。设触发机会按 tick 独立出现、每次成功的概率为 p,则从刚好满 t = 20 起、在随后的 t − 19 个 tick 内至少成功一次的概率为:

P(t) = 1 − (1 − p)t − 19 (t ≥ 20)

取 p ≈ 3.4%,也就是令 (1 − p) = 2−1/20,式子就化简成常见的那一种写法:

P(t) = 1 − 2−(t − 19) / 20 (t ≥ 20)

按这个写法,刚满 20 tick 时概率约 3.4%,蓄力 39 tick 时约 50%,此后趋近于 1。这解释了为什么“刚拉满就松手”几乎从不成功。需要说明的是:独立性假设本身没有依据——如果各次机会并不独立,或者过了某个 tick 之后概率陡增,这个式子就不再成立。各模型对概率来源的解释不同,见对比表。

理论模型

以下三种说法按社区接受程度排列。

C08 复合载荷说(主流)

这是目前最主流的说法。该模型认为拉弓包不是一个单独设计的数据包,而是 C08(开始拉弓)与 C07(松手)在同一个 tick 内被合并发送后,服务端把它们当成一个包来处理的结果,因此称为复合载荷。

合并

正常射箭时,C08 与 C07 之间至少隔着 20 tick 的蓄力时间,服务端分别处理。在客户端本地 getTicksUsingItem() ≥ 20 之后的某个 tick,如果客户端重发了一次 C08,又紧跟着发出半个 C07,两者有机会落进同一次发送(能否赶上就是概率的来源,蓄力越久、客户端越有余量对齐这两个包)。服务端收到后不再把它们拆成“使用物品”和“释放物品”两个动作,而是按 ID 0x7B 作为一个拉弓包处理。拉弓包的字段比普通交互包多,就是因为它同时带着两个包的内容。这一步是整条说法的核心假设:同一 tick 内的连续包会不会被合并处理,只有服务端知道,客户端抓包看不出差别。

效果

合并之后,这次松手不会生成箭矢。该说法给出的解释与原版逻辑一致:重发的 C08 会让服务端重新 startUsingItem(),服务端侧的 getTicksUsingItem() 归零,紧跟着的松手在 BowItem.releaseUsing() 中算出的力度为 0(低于 0.1),因此不射箭。客户端本地的计数仍 ≥ 20,这份蓄力被改为作用在玩家自己身上,方向与视角一致。速度按仰角 θ(即 −pitch,抬头为正)分成水平和垂直两部分:

v水平 = 0.707 · cos θ   v垂直 = 0.707 · sin θ  (单位:blocks/tick)

抬头 45° 时两者相等,各为 0.5 blocks/tick(约 10 格/秒)。位移由客户端在同一 tick 内连续发出的几个 C04 上报(社区称为级联 C04)。单个拉弓包的效果大致相当于一次稍高的跳跃:约 6 tick 到达顶点,高约 1.7 格,约 14 tick 后落回原高度。

维持

想让效果持续下去,该模型认为还需要同时满足两点:

按该说法,在这段时间里服务端只看得到一串“在地面上移动”的上报,基于位移预测和重力推演的检测(常被举例的是 Matrix 与 Grim)因此判断不出玩家已经离地。这条推论依赖一个前提:那串 C03 上报里的 onGround 位确实被覆写成了 true,而覆写本身在客户端侧是可做的。

握手说(二流说法)

握手说认为拉弓包是一个真实存在的独立数据包,需要客户端在同一个 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 内连续包的处理窗口宽窄不一,同样的发送节奏在某些环境上成立、在另一些上不成立。

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

握手说被归为二流说法,主要是因为它把拉弓包当成一个独立注册的包类型,于是没法解释为什么官方映射里找不到对应的类;也解释不了拉弓包为何不射箭。前者它只能归给“未导出”,后者则避而不谈。

融合说

也有说法称,拉弓包是上述两者的结合:握手说描述的是拉弓包的传输层(包怎么来、怎么发),C08 复合载荷说描述的是它的语义层(包到了之后发生什么)。按融合说,握手序列第 8 步发出的 C2S 拉弓包,其 pos 是 C08 说中这一轮级联 C04 的终点坐标,ground 就是“onGround 覆写”,因此一个拉弓包等价于一整轮级联。

两种说法表面上最大的冲突在序号与位置:握手说要求 S32 的往返在同一个 tick 内完成,C08 说却要求把 S08 推迟 15 – 20 tick 再应用。融合说的解释是两者针对的是不同的东西——推迟的是应用拉回位置,而 C0F 回应的是 S32、不经过主线程的处理队列、立即发出,这正是握手说要求 C0F 必须异步发送的原因。

至于“拉弓包是否独立存在”,融合说认为:它在复合视图里占一个编号(0x7B、双向、带 Sequence),但内容是 C08 与 C07 合并后的载荷,所以这个编号也沿用 C08 说的解释——它是合并视图的显示结果,不是协议分配的独立类型。融合说能同时解释字段多、双向、概率性这三个特征,但至今没有人给出完整的抓包记录,整体仍然十分神秘。

对比项C08 复合载荷说握手说融合说
地位主流二流说法少数派
拉弓包是否独立存在否,是 C08 与 C07 合并的结果是线路上独立,内容是 C08 + C07 的合并载荷,等价于一轮级联
方向仅 C2SS2C 与 C2SS2C 与 C2S
关键数据包C08、C07、C04、C03、S08S32、C0F、C08、C07、C06全部
“半个”指什么只到了一半的 C07握手中的半个 C07,以及窗口内回发的半个拉弓包同握手说
能否解释 0x7B能(合并视图的显示编号)不能,视为分配值沿用 C08 说
能否解释不射箭能(服务端侧 getTicksUsingItem() 被重发的 C08 归零)不能沿用 C08 说
如何处理 S08推迟 15 – 20 tick 再应用作为位置校正同 tick 收下即时异步回应 S32,推迟应用位置
概率性的来源C08 与半个 C07 能否赶上同一次发送1 tick 窗口内能否发出两者叠加

历史

拉弓包的发包行为最早见于早期的反作弊讨论,没有留下具名记录。最早的描述只有握手说中的前三步(S32 → 异步 C0F → 半个 C07)以及随后服务端下发的首包,当时普遍认为它是一个纯粹的 S2C 包,这也是 PullBowS2CPacket 这一写法流传最广的原因。后来才确认客户端在拿到首包后同样可以主动发出,拉弓包由此被重新归类为双向;C08 复合载荷说出现得更晚,但很快成为主流。

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

简称高版本名称高版本类名绑定到原版用途
S32Ping (play)ClientboundPingPacket客户端服务端发一个 id,客户端用 C0F 原样送回;1.17 起承担原先的事务确认
S08Synchronize Player PositionClientboundPlayerPositionPacket客户端服务端设置玩家位置与视角
C0FPong (play)ServerboundPongPacket服务端回应服务端的 Ping,1.17 起取代了原先的事务确认;1.16 中 C0F 仍是 ServerboundContainerAckPacket
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 前缀习惯造出来的:前者表示发往客户端的一侧,后者表示客户端发出的一侧。按 Yarn 的命名风格,两者分别写作 PullBowS2CPacket(Server-to-Client)与 PullBowC2SPacket(Client-to-Server)。不区分方向时统称 PullBowPacket 或“拉弓包”。

这四个名字都不是官方映射里的类。拉弓包是复合包,官方映射里只有它的组成包:ServerboundUseItemPacket、ServerboundPlayerActionPacket、ServerboundMovePlayerPacket、ClientboundPingPacket / ServerboundPongPacket、ClientboundPlayerPositionPacket。所以在源码里 grep PullBow 什么也找不到——这是正常的,它从来不是一个单独的类,而是几个类被合并处理时社区给的那一串起的名字。

蓄力计数同理:本文使用 Mojang 映射中的 LivingEntity.getTicksUsingItem()(Yarn 写作 getItemUseTime()),即“已使用当前物品的 tick 数”。旧资料中常见的 BowChargeTicks 并不是原版字段,只是社区对这个值的俗称。

常见问题

拉弓包(Pull Bow)是什么?
Minecraft Java 版 1.16+ 中的一个复合包:由 S32/C0F、C08、半个 C07 与移动包族在同一 tick 内合并而成,不是单独注册的包类型。
拉弓包是 S2C 包还是 C2S 包?
两侧都有内容:S2C 一侧是序号往返(S32/C0F)与位置校正(S08),C2S 一侧是 C08、半个 C07 与移动包族。C08 复合载荷说认为只有 C2S 一侧成立。
拉弓包的数据包 ID 是多少?
没有独立分配的 ID。在 Play 状态的复合视图里显示为 0x7B(十进制 123),逐版本重排;官方映射里也不存在对应的类。
触发的前置条件是什么?
主手必须持弓,并且 getTicksUsingItem() ≥ 20。任一条件不满足时,整个序列不会开始。握手说和融合说还要求先收到服务端下发的首包。
为什么蓄力满了还是发不出去?
触发是概率性的。按社区的经验公式,刚满 20 tick 时概率只有约 3.4%,继续蓄力可以提高概率。
补发了 C06,服务端却没有下发拉弓包?
按握手说的解释,多半是半个 C07 没有和 S32、C0F、C08 落在同一个 tick 内。该说法把这类失败归因于服务端对连续包的处理窗口宽窄不一。
发包顺序可以调换吗?
不可以。握手说要求先收 S32 并用 C0F 回带 id,再发 C08 与半个 C07;顺序错误或超出 1 tick 都不会触发。
哪种理论模型是对的?
没有定论。C08 复合载荷说是主流,握手说是二流说法,融合说认为两者描述的是同一件事的不同层面。
是谁最早发现的?
发包行为最早见于早期的反作弊讨论,没有留下具名记录。目前流传的描述都晚于这些讨论本身。