# 阶段 3(0.1.15)M0 版本适配诊断与 M4 主动发送事务审计记录 日期:2026 年 10 月 12 日 客户端版本:国际服 `2026.09.15.0000.0000` 目标二进制:`ffxiv_dx11.exe`(SHA256 `5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`) ## 1. M0 统一版本适配清单与门控(`Core/AdaptationManifest.cs`) 针对全部 24 项保留功能及 M4 主动发送通道建立统一适配清单,按模块记录版本锁、SDK 偏移要求、原生签名及 RVA 证据: | 诊断分类 (`AdaptationIssue`) | 判定条件 | 门控行为 (`AdaptationManifest.ApplyGate`) | | --- | --- | --- | | `None` | 实现存在、版本/偏移与签名全部匹配且未发生运行异常 | 清除不可用门控,允许启用 | | `MissingImplementation` | 模块尚未注册实现 | 保持不可用,拒绝启用 | | `VersionMismatch` | `VersionLocked == true` 的模块(`2, 7, 8, 14, 16, 36` 及 M4 主动发送)在非 `2026.09.15.0000.0000` 客户端运行 | 自动关闭并标记不可用;非版本锁定模块(如 `1, 3, 9, 10, 11, 37`)与离线导入/导出不受影响 | | `OffsetMismatch` | `RequiresSdkOffsets == true`(模块 `8`:依赖 `Character.StatusManager` `0x23B0` 与 `GameObject.EntityId` `0x78`)偏移不匹配 | 仅门控对应模块,不影响其他模块 | | `SignatureMissing` | 任一必需签名在当前客户端匹配数为 `0` | 自动关闭并标记不可用,显示缺失签名名称与已核对 RVA | | `MultipleMatches` | `RequireUnique == true` 的签名在 `.text` 段出现 `>1` 处匹配 | 拒绝盲挂,标记不可用并显示匹配数量(模块 `15` `0x8D6F30` 显式声明 `RequireUnique = false` 使用首匹配包装函数) | | `RuntimeFault` | 运行期捕获异常并标记为 `ModuleState.Failed` | 保留具体异常详情并显示在 M0 诊断面板 | 本机 `ffxiv_dx11.exe` 签名扫描核验:21 条唯一签名(`RequireUnique = true`)在 `.text` 段均精确匹配 `1` 处;模块 `15` 包装签名匹配首处 `0x8D6F30`。 ## 2. M4 原生主动发送链路反汇编核实 ### 2.1 为什么不沿用旧 `IC/NetManager.cs` 的 `0x4810E0` 包装函数 反汇编 `0x4810E0`(旧签名 `48 8B 49 08 48 85 C9 74 08 45 33 C9`): - `0x4810E0`: `mov rcx, [rcx+8]`(从 `NetworkModuleProxy*` `+0x08` 读取 `ReceiverCallback*`) - `0x4810E4`: `test rcx, rcx; jz 0x4810F1` - `0x4810E9`: `xor r9d, r9d; xor r8d, r8d; call 0x1CFD140` - `0x4810F1`: `mov al, 1; add rsp, 28h; ret` **缺陷**:`0x4810E0` 在调用 `0x1CFD140` 后无条件执行 `mov al, 1`,即使 `ReceiverCallback*` 为空或底层 `ZoneUp` 返回 `0`(断线/未就绪),也会恒定返回 `1`,把发送失败伪装成成功。 ### 2.2 已核实的原生发送链(保留真实返回值) 1. `Framework.Instance()->NetworkModuleProxy`(偏移 `+0x1678`,对应原生 getter `0xD28F0`)。 2. `ReceiverCallback* = *(nint*)((nint)networkModuleProxy + 0x08)`。 3. `ReceiverSendPacket`(RVA `0x1CFD140`,唯一签名 `48 83 EC 38 48 8B 89 70 0A 00 00 ...`,1 处匹配): - 读取 `ZoneClient* = *(nint*)(receiver + 0xA70)`;若为空直接 `xor al, al; ret` 返回 `0`。 - 非空时以 `priority = 0` 调用 `ZoneClient.SendPacket`(RVA `0x1D03040`,唯一签名 `48 83 EC 28 48 8B 89 98 00 00 00 ...`,1 处匹配)。 4. `ZoneClient.SendPacket`(`0x1D03040`): - 检查 `*(nint*)(zoneClient + 0x98) != 0`,调用 `ZoneUp`(`0x1D718C0`),并将 `ZoneUp` 的真实 `byte` 返回码原样返回给调用方。 5. `ZoneUp`(`0x1D718C0`)在 `0x1D7198C` 读取发送缓冲区契约: - `+0x00`: `uint Opcode` - `+0x08`: `ulong TotalSize`(`payload.Length + 0x10`,即含 16 字节 IPC 头) - `+0x20`: `byte[payload.Length]` 业务载荷 - 原生调用方(如 `0xB8A7AA` `UP_EventStart`、`0xB352F0` `UP_ClientTrigger`)均在栈上分配 `0xF60` 字节清零缓冲区传入;`NetworkManager.SendNative` 同样采用至少 `0xF60` 字节的清零固定缓冲区调用 `0x1CFD140`。 ### 2.3 为什么不用合成 `UP_PingReq` 做主动发包测试 反汇编 `0x1D02776`(`UP_PingReq` 构造)与 `0x1D022E1`(`DOWN_PingRsp` 消费)证实:客户端在发送 `UP_PingReq` 与接收 `DOWN_PingRsp` 时会直接读写 `NetworkModule` 内部延迟测量状态(`[r14+0BCCh]`、`[r14+0C3Ch]`)。插件在外部合成 `UP_PingReq` 会干扰客户端原生 Ping 状态机。因此 M4 仅提供经过结构审计的业务包编码器(`UP_ActionSend`、`UP_ClientTrigger`、`UP_EventStart`、`UP_EventFinish*` 8 档、`UP_EventAction*` 8 档)。 ## 3. M4 主动发送调度与请求响应事务(`Core/ActivePacketSender.cs`) - **来源标注(`ActiveSendScope`)**:`ActivePacketDispatcher.Pump` 调用原生发送时开启线程本地 `ActiveSendScope`,`NetworkPipeline.Read` 自动将该次 `ZoneUp` 捕获标记为 `PacketOrigin.PluginActiveSend`,与游戏自身触发的 `PacketOrigin.NativeZone` 明确区分。 - **主线程调度与重入防护**:仅在 `Framework.Update` 主线程调用 `PumpActiveSends`;`NetworkPipeline.InCallback` 与 `inReceiveCallback` 禁止在任何网络回调中递归调用 `Pump`。 - **有界队列、限速与会话绑定**:默认最大排队 `32` 条、最小发送间隔 `100 ms`、排队超时 `2000 ms`、响应超时 `5000 ms`。每次入队与发送均核对 `SendSession`(角色在场、非切区、非占卜回放、`Territory`、`LocalAddress`、`LocalEntityId` 一致);切区或会话变更自动取消未完成请求。 - **显式事务状态机**:区分 `Queued`、`SentNative`、`RejectedNative`、`Confirmed`、`TimedOut`、`Canceled`。`Cancel` 仅能取消仍处于 `Queued` 的请求,绝不把取消排队伪装成撤回已发送原生包。 - **请求响应关联**:`UP_ActionSend` 自动等待同 `RequestId` 的 `DOWN_Effect`(`0x313`),`UP_EventStart` / `UP_EventFinish*` 支持等待同 `EventId` 的 `DOWN_EventStart` / `DOWN_EventFinish`,并在收到响应时计算真实往返耗时 `RoundTripMs`。