握手序列
九步 · 时间约束 · 失败排查 · 返回总条目
握手说认为拉弓包需要一个固定顺序的序列才会出现,且其中大部分步骤必须落在同一个 tick 内。本页是这个序列的完整展开。九个步骤里,第 1 至第 5 步同 tick,第 8 步打开窗口,第 9 步落在窗口内。
| 顺序 | 数据包 | 方向 | 数量 | 说明 |
|---|---|---|---|---|
| 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 再处理。 |
| 7 | ClientboundPullBowPacket | 服务端 → 客户端 | 一个 | 序列被接受后,服务端下发首个拉弓包(俗称),同时打开 1 tick 的发送窗口。 |
| 8 | ServerboundPullBowPacket | 客户端 → 服务端 | 半个或一个 | 窗口内有概率可以主动发出。字段需填写当前的 pos、rot、ground。 |
速查:蓄力 ≥ 20 tick → S32(收)→ 异步 C0F(发)→ C08(发)→ 半个 C07(发)→ [C06(发,可选)] → S08(收,推迟应用)→ 首包(收)→ 回发(发,概率)
约束
- 时间:第 1 至第 5 步必须落在同一个 tick 内;第 8 步必须落在第 7 步之后的 1 tick 内。
- 顺序:先收
S32并用C0F回带 id,再发C08与半个C07,不可调换。 - 异步:
C0F必须异步发送。这是整个序列里唯一不能等主线程的一步。 - 数量:
C0F为一个,C07为半个。 - 位置:第 6 步的
S08收到后推迟 15 – 20 tick 再应用,与“第 1 至第 5 步同 tick”的要求并不冲突——推迟的是应用,不是接收。 - 位置同步:特殊情况下需要一个
C06同步位置,常规流程不需要。
两处“半个”不要混淆
- 半个
C07(第 4 步):握手的一部分,发生在首包之前。 - 半个拉弓包(第 8 步):首包之后窗口内的回发,移动部分用只带坐标的
C04(ServerboundMovePlayerPacket.Pos)而不是带朝向的C06:省略rot,只带pos与ground,服务端沿用上一个移动包的朝向。
失败排查
| 症状 | 该说法给出的原因 | 先查哪一步 |
|---|---|---|
补了 C06 却没有首包 |
半个 C07 没和 S32、C0F、C08 落在同一个 tick |
第 4 步的时序,而不是 C06 本身 |
| 首包来了,回发被丢 | Charge Ticks 低于 20,或 Sequence 与窗口对不上 | 第 2 步的 id 是否真的回带成功 |
| 箭照常射出去了 | 重发的 C08 没落在释放之前,服务端计时没归零 |
第 3 步与第 4 步的先后 |
| 一轮成功、第三轮起全失败 | 位置差累积到服务端判定超限,S08 被应用 |
第 6 步推迟的时长是否短于滞空时间 |
该说法把这类失败归因于服务端实现差异:不同服务端对同一 tick 内连续包的处理窗口宽窄不一,同样的发送节奏在某些环境上成立、在另一些上不成立。这也是为什么没有一份放之四海皆准的参数表。
本页是三种理论模型里握手说的展开。若采用 C08 复合载荷说,第 1、2、6、7 步都不需要——那份说法里只有 C2S 一侧。