Files
mtgodot-poc/audit/contracts/network.packet-dispatch.md
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

236 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# network.packet_dispatch_protocol
## Scope
核对 40250 的完整 GC 包注册表、静态/动态分帧、阶段 allowlist、包头 padding、未知包与已知未处理包的副作用、一次 pump 的消费数量,以及 ping/sequence 的发送顺序。这里的“已实现”要求完整调用链的分支、消费长度、状态变化和失败清理都等价,只有同名 `case` 或能找到解析函数不能算等价。
## Reference call chain
- `UserInterface/Packet.h`
- `UserInterface/PythonNetworkStream.cpp::CMainPacketHeaderMap`
- `UserInterface/PythonNetworkStream.cpp::CheckPacket`
- `UserInterface/PythonNetworkStream.cpp::RecvErrorPacket`
- `UserInterface/PythonNetworkStream.cpp::RecvPhasePacket`
- `UserInterface/PythonNetworkStream.cpp::RecvPingPacket`
- `UserInterface/PythonNetworkStream.cpp::OnProcess`
- `UserInterface/PythonNetworkStreamPhaseLogin.cpp::LoginPhase`
- `UserInterface/PythonNetworkStreamPhaseSelect.cpp::SelectPhase`
- `UserInterface/PythonNetworkStreamPhaseLoading.cpp::LoadingPhase`
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::GamePhase`
- `EterLib/NetPacketHeaderMap.cpp`
- `EterLib/NetStream.cpp`
`CMainPacketHeaderMap` 的有效表实际有 104 项静态、13 项动态,共 117 个有效 GC 注册项。源码中另有两行被注释掉的 `Set``SLOW_TIMER`、旧 `TARGET_CREATE`),不计入运行时注册表。动态项包括 `QUEST_INFO``DUEL_START``CHAT``SYNC_POSITION``SHOP``SCRIPT``MESSENGER``GUILD``DUNGEON``NPC_POSITION``LAND_LIST``HYBRIDCRYPT_KEYS``HYBRIDCRYPT_SDB``WHISPER` 在 40250 主表中是静态注册项,文本尾部由 `RecvWhisperPacket` 在固定头消费后再按 `wSize` 消费。
## Current call chain
- `extension/src/net/classic/classic_stream.cpp::process`
- `extension/src/net/classic/classic_stream.cpp::feed`
- `extension/src/net/classic/classic_stream.cpp::dispatch`
- `extension/src/net/classic/classic_stream.cpp::handle_control`
- `extension/src/net/classic/classic_session.cpp::on_packet`
- `extension/src/net/classic/classic_parser.cpp::on_gc`
- `extension/src/net/classic/wire_classic.h::packet_size_gc`
- `extension/src/net/classic/wire_classic.h::is_dynamic_gc`
- `extension/src/net/classic/wire_classic.h::is_sequence_cg`
- `extension/src/net/net_stream.cpp`
## Reference branch matrix
| 场景 | 40250 实现 | 当前端实现 | 结论 |
|---|---|---|---|
| 包头查表 | `CMainPacketHeaderMap::Get` 只接受主表中的 117 个 GC header;未注册 header 清空接收缓冲并 `PostQuitMessage(0)` | 当前主游戏流已按数值覆盖参考 117/117 个 header;仅独立 guild-mark `SYMBOL_DATA=133` 保留额外动态适配;未知普通流 header 回调错误并断流 | PARTIAL:注册数值和静态/动态类别已对齐,`sizeof`/别名布局和应用级退出副作用仍未完全证明 |
| 静态包 | 使用 `sizeof(TPacket...)` 的完整包长;不足保留缓冲等待 | `packet_size_gc` 当前有 104 个静态 case,按数值对应参考 104 个静态注册项;不足保留缓冲等待 | PARTIAL:当前表达式与参考 `sizeof` 的逐项布局证明仍未完成 |
| 动态包 | 先读取 `TDynamicSizePacketHeader`,以 wire `wSize` 判断完整包;随后由具体 handler 再消费字段尾部 | 以 `DynHead.size` 分帧;`QUEST_INFO` 按 flag 重算实际服务端尾部,`GUILD/SKILL_INFO` 保留 40250 服务端少写一字节的兼容长度;字段尾部不足时 parser 返回失败 | PARTIAL:特殊包的边界、过大值、声明值与实际消费值仍不是同一算法,但截断 handler 已与 `Recv()` 失败路径对齐 |
| `WHISPER` | 主表标为静态;先消费 `TPacketGCWhisper`,再按 `wSize - sizeof(header)` 消费文本 | `packet_size_gc` 使用固定 `GCWhisperHead`,静态分派先校验完整 `wSize`,回调 parser 时去掉 `[header][wSize]` | MAPPED:固定头、文本尾和后续包边界已对齐 |
| 前导零 | `CheckPacket` 只在检查下一个 header 时剥掉连续零;遇到第一个非零停止;全是零则等待更多数据 | `dispatch` 只剥掉当前前导零 run,零耗尽时等待下一次读取,遇到非零后再查表 | MAPPED:已覆盖 zero-only、zero-run+valid packet 回归 |
| 阶段 allowlist | Login、Select、Loading、Game 各自 switch;未命中走 `RecvDefaultPacket/RecvErrorPacket`Loading 的 default 才委托一次 `GamePhase` | `ClassicSession::on_packet` 已收口 Login/Select/DirectEnterLoading 使用 Loading 专属集并回退 Game 集;Game 使用显式 Game 集;错误阶段在当前帧后清空剩余接收缓冲 | PARTIAL:完整 117 项映射、handler 失败清理和所有分支仍未逐项证明 |
| Login allowlist | `PHASE/LOGIN_SUCCESS3/4/FAILURE/EMPIRE/MATRIX/PASSPOD/LOGIN_KEY/PING/HYBRID*` | 可解析的 GC header 由统一 parser 尝试处理;可选集成包明确 false,其他不按 Login 表拒绝 | PARTIAL |
| Select allowlist | `PHASE/EMPIRE/LOGIN_SUCCESS3/4/CREATE/DELETE/CHANGE_NAME/HANDSHAKE/HYBRID*/KEY_AGREEMENT*/POINT_CHANGE/PING``PLAYER_POINT_CHANGE` 只消费不更新角色属性 | Select 专用门禁已存在;`GC_PLAYER_POINT_CHANGE` 在 Select/DirectEnter 中只消费不写入 EntityStoreLoading/Game 才进入 parser | MAPPED |
| Loading allowlist | `PHASE/MAIN_CHARACTER*/CHARACTER_UPDATE/PLAYER_POINTS/POINT_CHANGE/ITEM_SET(20)/PING/QUICKSLOT_ADD/HYBRID*`default 进入一次 GamePhase | `loading_phase_packet` 先收 Loading 专属包;21 号 `ITEM_SET2` 和其他 GamePhase 包走一次 `game_phase_packet` fallbackHybridCrypt 由 ClassicStream 消费后返回 | MAPPED:阶段专属包、GamePhase fallback 和控制包 owner 已按数值/调用链对齐 |
| Game allowlist | 只在 GamePhase switch 中派发游戏包,包含实体、战斗、道具、任务、商店、公会、仓库、效果等分支 | `game_phase_packet` 显式复刻 GamePhase 非控制分支;未命中不再进入统一 parser | PARTIAL:每个分支的副作用和 `RecvDefaultPacket` 清理仍未闭合 |
| 一次 pump | `OnProcess` 每次只运行当前 phase process 一次;GamePhase 按 `dwRecvCount++ >= MAX_RECV_COUNT-1` 与 8192 字节阈值继续;PHASE/握手类控制包立即 return | `ClassicStream::dispatch` 在 Game 阶段缓冲低于 8192 字节时消费 3 个完整包后 return;phase/握手/初始密钥协商后立即 return,`KEY_AGREEMENT_COMPLETED` 会先解密已缓存字节并继续派发;Loading→Game fallback 仍不设 Game 限额 | PARTIAL:大于 8192 字节的阈值、精确 tick 驱动和全量 burst 事件矩阵仍未完全证明 |
| 已知但 handler 失败 | phase handler 失败后调用 `RecvErrorPacket`,记录并清空接收缓冲;不由该分支直接断 socket | `on_packet == false` 报错后消费当前帧、清空剩余接收缓冲并保留 socket/phase owner;未知 header、底层 framing 错误仍走断流 | MAPPED:已对齐清缓冲与 owner 保留,应用层对可选集成错误的提示仍是适配差异 |
| 未知 header | 清空接收缓冲并退出消息循环 | `on_error` 后断流到 Offline | PARTIAL:应用级退出副作用不同 |
| 断包/乱序 | `Peek` 失败则保留缓冲等待;具体 handler 自己决定二次 `Recv` 失败 | framing 缓冲保留,但动态 underflow、未知 header 和 handler false 直接断流 | PARTIAL |
| Ping/Pong | 消费一个 `GC_PING`;发送 `CG_PONG`;安全模式额外 `SendSequence()` | `handle_control` 消费 blank`send_fixed` 在 sequence 模式附加序号 | MAPPED:该单分支的发送顺序和 sequence 条件一致 |
## Concrete table audit
### 40250 注册表已核对的结构性事实
- `CMainPacketHeaderMap` 共有 117 个有效 `Set(HEADER_GC_...)`,其中静态 104、动态 13;两行注释掉的 `Set` 不计入,数量由 `audit_packet_registry.py` 从当前参考源自动解析。
- 其中 13 个使用 `DYNAMIC_SIZE_PACKET`,其余 104 个使用 `STATIC_SIZE_PACKET`
- `HEADER_GC_WHISPER` 是静态注册,不在动态表;文本是 handler 在固定头之后单独读取。
- `HEADER_GC_SLOW_TIMER``HEADER_GC_TARGET_CREATE` 在 40250 源码中是注释掉的 `Set`,不能仅因为 `Packet.h` 中存在结构体就视为有效注册项。
- `GamePhase` 中出现的 `OBSERVER_*`、攻击等分支不能反向证明它们已进入 `CMainPacketHeaderMap`;分帧有效性以 map 为准。
### 当前端逐项核对结果
- 当前 `packet_size_gc` 有 104 个 GC 尺寸 case;按数值已覆盖参考端 117 个注册项中的全部 104 个静态项,名称使用了 `LOGIN_SUCCESS_NEWSLOT``CHARACTER_CREATE_SUCCESS``CHARACTER_POINTS` 等协议别名;逐项 `sizeof/byte layout` 仍需机器化证明。
- 当前 `is_dynamic_gc` 有 14 个 GC 分支,除 40250 的 13 项动态集合外只额外包含已登记的 `SYMBOL_DATA` 兼容路径;这不能与 40250 主表直接视为完全等价。
- 当前 parser 有 112 个 GC case,数量大于主表并不代表覆盖正确,因为部分 case 是 optional integration 或其他版本兼容分支,且缺少 phase gate。
- 当前 `packet_size_gc` 有 104 个明确静态 case`is_dynamic_gc` 有 14 个 case;按数值与类别已覆盖参考 117/117 个注册项,唯一额外数值是独立 guild-mark 连接使用的 `HDR_GC_SYMBOL_DATA=133`,必须继续作为显式兼容差异保留。
- 当前 `QUEST_INFO``GUILD/SKILL_INFO` 的特殊尺寸修正是当前端为服务端兼容而加入的行为;需要以真实 40250 包序列证明它不会改变声明尺寸、剩余包边界和失败清理。
## Equivalence matrix
| 项目 | 状态 | 证据 |
|---|---|---|
| Preconditions | PARTIAL | 静态/动态长度检查存在;动态 handler 的固定字段/记录尾部不足已返回失败,但动态最小头和所有声明值边界尚未完全机器化 |
| Branch structure | PARTIAL | 静态/动态主分支存在;当前额外兼容分支和统一 parser 改变了 40250 的分派分支 |
| Algorithms/formulas | PARTIAL | `QUEST_INFO`/`GUILD` 重算长度,40250 `CheckPacket` 以声明 `wSize` 做完整性判断 |
| State transition order | PARTIAL | 当前一次 dispatch 可消费完整 burst40250 每个 OnProcess tick 只运行一次 phase process |
| Constants/units | PARTIAL | 当前尺寸单位均为 wire bytes104 个静态 case/13 个主表动态 case 的数值类别已对齐,但与 40250 `sizeof` 别名的逐项证明未完成 |
| Timing/event sources | PARTIAL | `feed` 立即 dispatch 且 socket process drain40250 由 phase tick 驱动单包消费 |
| Resource/data sources | MAPPED | 两侧主调用链、Packet/HeaderMap、wire table 均已定位 |
| Protocol side effects | PARTIAL | Ping/Pong/sequence 一致;未知包、handler false、错误 phase 的清理/退出不同 |
| Interruption/failure/cleanup | PARTIAL | 已知 handler false 现在清缓冲并保留 owner;动态字段截断会进入该路径;unknown/动态 framing underflow/底层 I/O 仍断流或终止当前连接,和 40250 的应用级退出副作用尚未完全统一 |
## Required parity tests
- 从 40250 `CMainPacketHeaderMap` 自动生成 117 项 `(header, sizeof, dynamic)` 清单,与当前 `packet_size_gc/is_dynamic_gc` 逐项比较;数值/类别覆盖已达到 117/117,下一步必须补齐别名的实际 `sizeof/byte layout` 对照,不能依赖人工推断。
- 对 13 个动态包分别覆盖:只有固定头、`wSize == header``wSize < header`、声明长度大于缓冲区、声明长度后紧跟下一个包、handler 实际消费尾部不足。
- 单独覆盖 `WHISPER` 的“固定头成功但文本尾部不完整”两段式行为,并确认文本后续包边界。
- 对 Login/Select/Loading/Game 的每个 allowlist 做允许与拒绝矩阵,确认错误阶段不会调用世界/角色/道具副作用。
- 同一缓冲区放入 1、2、N 个完整包,分别记录 40250 phase tick 与当前 dispatch 的事件顺序和消费数量。
- 覆盖 leading zero、packet body zero、未知 header、已知但 handler false、dynamic underflow、静态截断、连续包和错误后剩余缓冲。
## Evidence run
- `build/extension/net_classic_stream_test``build/extension/net_classic_wire_test``build/extension/net_classic_session_test`:PASS,已覆盖固定头、文本尾分片、后续静态包边界、前导零、错误阶段拒绝,以及已知 handler 失败后保留 phase owner 并继续处理下一 tick。
- `.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py`:报告有效参考注册表 `117/104/13`,当前 `packet_size_gc=104``is_dynamic_gc=14`,数值覆盖 `117/117`,额外数值仅为已登记的 guild-mark `HDR_GC_SYMBOL_DATA=133`;报告仍为 `PARTIAL`,因为该独立适配和 `sizeof/byte layout` 证明尚未闭合。
- `.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py --strict`:按非 GAIDEN 数值比较 Login/Select/Loading/Game 四个阶段,排除由 ClassicStream 消费的控制包和独立 observer stream 后,四阶段均无缺失或额外 payload case;该结果只证明阶段分支结构,不证明每个 handler 的副作用。
- `build/extension/net_classic_session_test``build/extension/net_classic_stream_test`PASS,新增 Loading 专属/ Game fallback、Game 阶段拒绝 Loading 主角色包、HYBRIDCRYPT 丢弃消费和 phase 控制包后的 pipelined packet 边界回归。
- `build/extension/net_classic_stream_test`:PASS,新增 Game 阶段四个小包只消费前三个、下一 tick 消费第四个的 `MAX_RECV_COUNT` 回归;旧版 85 字节 `HEADER_GC_MAIN_CHARACTER=15` 也已加入分帧/解析测试。
- 以上测试尚未覆盖 117 项逐包尺寸/布局生成矩阵、每个 GamePhase 分支的允许/拒绝矩阵和 8192 字节以上 burst,因此不能将本合同提升为 `MAPPED``TEST_VERIFIED`
## Audit/fix round 2026-09-22 — dynamic handler truncation
沿 40250 的动态包调用链复核 `CheckPacket -> Recv*Packet -> RecvErrorPacket` 后,修复了当前 parser 在“包头/声明长度已经被分帧,但具体字段尾部不足”时错误返回成功的问题:
- `SCRIPT``QUEST_INFO``MESSENGER``SHOP``GUILD``DUNGEON``NPC_POSITION``LAND_LIST``CHAT` 的固定字段、变长记录和 flag-driven 字段现在不足即返回失败,不再以空数据、部分记录或默认值继续产生世界副作用。
- `QUEST_INFO` 仍按 40250 服务端实际写入的 flag-driven 尾部计算帧长;`GUILD/SKILL_INFO` 仍保留服务端声明 22、实际写入 21 字节的已证实兼容分支。两者是协议适配差异,不伪装成主表算法完全相同。
- `ClassicStream` 现将这些 parser 失败统一交给已修复的 `RecvErrorPacket` 适配:消费当前帧、清空同批剩余数据、保留阶段 owner;下一帧仍可继续处理。
回归通过:`net_classic_stream_test``net_classic_session_test`。新增了 handler 失败后的清理流水线和截断动态 SCRIPT 回归;parser 同时收紧了消息、公会、商店、任务、地城、NPC 位置、土地列表和聊天的尾部检查。完整 13 项动态包声明长度矩阵、逐项 `sizeof/byte layout`、8192 字节以上 burst 和应用退出副作用仍保持 `PARTIAL`
## Audit/fix round 2026-09-22 — Select point-change side effect
复核 40250 `PythonNetworkStreamPhaseSelect.cpp::SelectPhase` 后确认,
`HEADER_GC_PLAYER_POINT_CHANGE` 在 SelectPhase 中只执行一次固定长度 `Recv` 并立即返回,
不会调用 `RecvPointChange()`,因此不会把选角阶段收到的旧点数写入 `CPythonPlayer`
当前端此前虽然已经有 Select/DirectEnter allowlist,但会把该包继续交给
phase-neutral `ClassicParser::on_gc`,错误地更新 `EntityStore`。现已在
`ClassicSession::on_packet` 的 Select/DirectEnter 分支加入等价的消费后返回;同一包在
Loading/Game 仍按 40250 的对应 handler 正常更新点数。新增选角阶段“消费但无世界副作用”
回归测试,native session 测试通过。
Loading/Game 完整 allowlist、逐项 `sizeof/byte layout`、动态声明长度矩阵和大 burst
仍保持 `PARTIAL`
## Audit/fix round 2026-09-22 — phase branch matrix and Loading fallback
新增 `audit_phase_dispatch.py`,按实际非 GAIDEN 的 header 数值自动比较 40250
`LoginPhase/SelectPhase/LoadingPhase/GamePhase` 与当前 session gate。报告明确排除
ClassicStream 已消费的 ping、phase、握手、密钥协商和 HybridCrypt 控制包,以及不在主
`CMainPacketHeaderMap` 中的 observer stream 包,避免把协议层 owner 差异误报成 payload
缺失。当前四阶段 payload case 均为数值一致。
该矩阵同时发现并修正了两处调用链差异:Loading 不再把 21 号 `ITEM_SET2` 当作
Loading 专属分支,而是与 40250 一样通过一次 GamePhase fallbackLogin/Select/Loading
的 HybridCrypt 不再进入 session allowlist,而由 ClassicStream 在分帧后消费并立即返回。
`net_classic_session_test``net_classic_stream_test` 和 phase 矩阵审计通过。
handler 副作用、逐项 `sizeof/byte layout`、动态声明长度矩阵和大 burst 仍保持
`PARTIAL`
## Audit/fix round 2026-09-21 — Loading/Game phase-local dispatch
沿 `PythonNetworkStreamPhaseLoading.cpp::LoadingPhase`
`PythonNetworkStreamPhaseGame.cpp::GamePhase` 的真实 switch/default 路径补齐当前端的阶段门禁:
- `Loading` 先接受主角色、点数、物品、快捷栏和 HybridCrypt 包;其他包只有命中 GamePhase 明确分支时才进入 parser,复刻 Loading 的一次 GamePhase fallback。
- `InGame` 只接受 GamePhase 明确列出的实体、战斗、道具、任务、商店、公会、仓库、效果等分支;登录成功、角色主初始化等迟到包不再改写世界状态。
- HybridCrypt 动态包按 40250 的“消费后返回”处理,不再误交给当前没有 vendor crypto handler 的通用 parser。
- `PHASE`、握手和初始密钥协商控制包消费后立即结束当前 dispatch,避免后续包在旧阶段 owner 下被提前处理;`KEY_AGREEMENT_COMPLETED` 是例外,必须先解密同一缓冲区中的后续密文并继续派发。
回归通过:`net_classic_session_test``net_classic_stream_test``net_classic_encstream_test`。已补上 GamePhase 小 burst 的 `MAX_RECV_COUNT` 边界、旧版 85 字节主角色包、KEY_AGREEMENT_COMPLETED 后已缓存密文继续派发,以及已知 handler 失败后的 `RecvErrorPacket` 清缓冲/owner 保留;8192 字节以上缓冲的继续消费、逐项布局、动态包声明长度和可选集成错误提示仍保持 `PARTIAL`
## Audit/fix round 2026-09-21 — numeric registry alignment
读取 40250 Windows 工程的实际条件分支后确认:`GAIDEN` 未定义,`_IMPROVED_PACKET_ENCRYPTION_``EterBase/ServiceDefs.h` 启用。审计脚本现在会先计算非 GAIDEN 的包头数值,再比较当前 wire 表的静态/动态类别。
- 当前主游戏流按数值覆盖参考端全部 `117/117` 个有效注册项,静态项为 `104`,主表动态项为 `13`
- 移除了不在 40250 `CMainPacketHeaderMap` 中的普通流包头:攻击 `12`、旧更新 `24`、观察者 `96/97/98` 和 Panama `151`;它们仍可作为 parser 历史兼容代码存在,但不会再被主流分帧接受。
- `HDR_GC_SYMBOL_DATA=133` 仅保留给独立 guild-mark 下载连接,并由审计报告明确标为额外适配,不再混入主游戏注册表的等价结论。
验证:`audit_packet_registry.py` 报告数值覆盖 `117/117`、类别无 mismatch;主流 `packet_size_gc=104``is_dynamic_gc` 含 13 个参考动态项加 1 个独立标志适配。逐项 `sizeof`/字节布局和动态包实际消费仍待继续闭合。
## Implementation fix round 2026-09-21
按 40250 `CMainPacketHeaderMap::Set(HEADER_GC_WHISPER, sizeof(TPacketGCWhisper), STATIC_SIZE_PACKET)``RecvWhisperPacket()` 的两段式读取修复当前 classic framing`GC_WHISPER` 不再归入通用动态 GC,固定头先按静态尺寸等待,随后按 `wSize` 等待文本尾;传给 phase-neutral parser 时去掉 `[header][wSize]`,保持 `[type][name][text]` 数据形状,并验证文本后紧跟静态包不会被吞掉。
回归:`net_classic_wire_test``net_classic_stream_test``net_classic_session_test``audit_packet_registry.py``ctest --test-dir build --output-on-failure`23/23)和 `git diff --check` 均通过。逐项 `sizeof`/byte layout、burst 大缓冲、handler 失败清理和 `SYMBOL_DATA` 独立连接边界仍未完全闭合,合同保持 `PARTIAL`
### Active EterLib transport-core review
- `EterLib/NetStream.cpp`/`.h` 实现 TCP connect/clear/disconnect、收发 buffer、Peek/Recv/Send/SendFlush、packet sequence 和可选 TEA security`NetPacketHeaderMap` 将 header 映射为 packet type`NetAddress` 负责地址/端口序列化。
- `NetDatagram`/`NetDatagramReceiver`/`NetDatagramSender` 提供 UDP socket、bind、recv/peek、缓冲区和清理;`NetDevice` 管理网络设备初始化/销毁。当前端 `classic_session`/parser 已覆盖主要 classic stream,但没有证明 EterLib 的 buffer partial read、sequence、断线清理、UDP 辅助路径和 header map 规则均在所有调用方闭环。
- 本轮完成 EterLib 网络底层活跃文件静态核对;已有 native wire/session 测试只覆盖当前适配层,合同继续为 `PARTIAL`
## Active UserInterface packet bridge review
- `PythonNetworkDatagram.cpp/h``PythonNetworkDatagramModule.cpp` 维护 state→header 的 datagram sender 注册、删除、选择、处理/检查 packet、bind 和状态包;它不是普通 stream 的 parser 别名,sender 生命周期和 header map 必须同步。
- `PythonNetworkStream.h` 列出的接口覆盖角色/物品/商店/交易/任务/公会/飞行/聊天等全部 CG 请求以及相应 phase/GC 处理入口;`PythonNetworkStreamPhaseOffline.cpp::OffLinePhase` 是离线 phase 的清理/转移入口。
- 当前端 classic session 已有主要 packet registry 和适配测试,但没有证明所有 UI bridge 请求的状态门控、sender 注册、partial read、offline 清理、重复/未知 header 和副作用顺序都与 40250 一致;本轮纳入 active source 证据,合同保持 `PARTIAL`
## Status
`PARTIAL`
## Audit round 2026-09-21 — registry/phase/sequence refresh
按当前工作树重新运行包注册表、阶段分派和发送序列表审计:
- `audit_packet_registry.py`:40250 有效主游戏注册表为 117 项,其中静态 104、动态 13;当前端主流按数值覆盖 117/117,静态/动态类别无 mismatch。当前额外的 `HDR_GC_SYMBOL_DATA=133` 仍只属于独立 guild-mark 连接适配,未计入主游戏流等价结论。
- `audit_phase_dispatch.py`Login、Select、Loading、Game 四阶段的 payload case 按数值均无缺失或额外分支;脚本排除了由 `ClassicStream` 消费的 transport/control 包和独立 observer stream,结果只证明阶段结构,不证明 handler 的世界副作用等价。
- `audit_sequence_table.py`:当前端与 40250 的 32768 字节 sequence table SHA-256 完全一致。
- `audit_source_coverage.py`:活动 40250 Visual Studio 源文件 524 个,显式归属 142 个、静态复核 364 个、未归属行为 0 个;这说明清单覆盖门通过,但不代表这些合同已完成等价验证。
本轮没有发现可以在不改变已登记适配语义的前提下安全修复的新代码差异。合同继续保持 `PARTIAL`,待办仍是逐项 `sizeof`/字节布局证明、13 个动态包的声明长度与截断矩阵、8192 字节以上 burst/单 tick 消费顺序、每个 handler 的副作用和错误阶段清理,以及 unknown header 的应用级退出副作用取证。
## Audit evidence reconciliation 2026-09-22T07:20Z
按当前工作树重新执行 packet/phase 结构审计:`audit_packet_registry.py` 报告参考有效注册表 `117` 项(静态 `104`、动态 `13`),当前主游戏流数值覆盖 `117/117`,类别无 mismatch;额外 `HDR_GC_SYMBOL_DATA=133` 仍只属于独立 guild-mark 连接适配。`audit_phase_dispatch.py` 的 Login/Select/Loading/Game payload case 均无缺失或额外数值。两项结果仅证明注册表和阶段分支结构,不能替代逐项 `sizeof/byte layout`、动态包边界、8192 字节 burst、handler 副作用与失败清理证明,因此合同继续 `PARTIAL`
## Audit round 2026-09-20 — registry and dynamic-boundary recheck
本轮重新执行 40250 `CMainPacketHeaderMap` 数值/类别对照和四阶段 dispatch 矩阵:
- `audit_packet_registry.py` 仍为参考主表 `117` 项、静态 `104`、动态 `13`;当前主游戏流按数值覆盖 `117/117`,类别无 mismatch。额外的 `HDR_GC_SYMBOL_DATA=133` 仅作为独立公会图标下载适配保留,不能计入主流 1:1 等价。
- `audit_phase_dispatch.py --strict` 的 Login/Select/Loading/Game payload case 均无缺失或额外数值;控制包、HybridCrypt 和 observer 专用包仍由各自 owner 消费。
- `net_classic_wire_test``net_classic_stream_test``net_classic_session_test` 均通过,固定包、WHISPER 两段式尾部、前导零、已知 handler 失败清缓冲和已确认的 GUILD/SKILL_INFO 短写兼容没有回归。
本轮没有把“注册表覆盖”和“phase case 数值一致”升级为完整等价。仍未闭合的是 13 个动态包的声明长度/实际字段尾部全矩阵、逐项 `sizeof/byte layout` 自动证明、8192 字节以上 burst 的单 tick 消费边界,以及主流与独立 `SYMBOL_DATA` 连接的运行时隔离证据;合同继续为 `PARTIAL`
## Audit/fix round 2026-09-20 — login/select phase dispatch gate
沿 40250 `LoginPhase``SelectPhase``LoadingPhase` 的 switch/default 分支核对后,当前 classic session 增加了 Login 与 Select 的显式 GC allowlist
- Connecting 阶段允许真实游戏连接先收到 `LOGIN_SUCCESS3/4`,随后仍由 `PHASE_SELECT` 接管;这保留了 DirectEnter 的实际连接顺序。
- LoggingIn 只接受 LoginPhase 的登录成功/失败、帝国、登录 key、可选认证集;CharSelect 只接受 SelectPhase 的角色 CRUD、帝国、登录成功和 point-change 集。
- DirectEnter 在 host stage 已显示 Loading、协议仍为 SelectPhase 时沿用 Select allowlist,普通 Loading 继续由 GamePhase fallback 处理游戏包。
- 错误阶段的游戏 spawn 包现在在到达 `ClassicParser` 前被拒绝,不能改写 EntityStore;增加 Login/Select 反例测试。
阶段 allowlist 未命中现在按 40250 `RecvErrorPacket` 语义在当前帧消费后清空剩余接收缓冲并保留 socket/phase owner;真正的 parser/handler 失败仍由适配层 `on_packet == false` 断开 socket。Loading/Game 全量 allowlist、117 项机器映射、burst tick 和特殊动态包边界仍需继续审计,合同保持 `PARTIAL`
## Audit/fix round 2026-09-20 — character CRUD phase gate
本轮沿 `LoginPhase`/`SelectPhase` 的 default/error 分支复核发现,当前端 phase-neutral parser 会让角色创建、删除和改名回包绕过阶段 owner。该差异已在 `ClassicSession::on_packet` 收口:角色 CRUD/改名只允许 `CharSelect`,或允许 DirectEnter 在协议仍处于 SelectPhase 的 `Loading + m_direct_enter` 窗口;其他阶段返回明确错误,不再写入角色槽或发 UI 事件。
验证通过:`net_classic_session_test``net_classic_stream_test``net_classic_wire_test``net_loopback_test``char_create_delete_test.gd``test_intro_select_parity.gd``p10_test.gd``git diff --check`
完整四阶段 allowlist、117-header 机器映射、burst tick、动态包边界和其他世界/道具副作用仍未闭合,合同保持 `PARTIAL`