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

146 lines
16 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.reconnect_warp
## Scope
核对游戏中 `GC_WARP` 换图、断线重连、保留角色槽位/direct-enter、旧 socket/实体/特效清理和重试策略。换图和断线重连虽然都要重新建立 TCP,但必须分别对照 40250 的调用链,不能只因为最终进入同一张地图就判定等价。
## Reference call chain
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::GamePhase`
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvWarpPacket`
- `UserInterface/PythonNetworkStream.cpp::ConnectGameServer`
- `UserInterface/PythonNetworkStream.cpp::SetLoadingPhase`
- `UserInterface/PythonNetworkStreamPhaseLoading.cpp::SetLoadingPhase`
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhase`
- `UserInterface/NetworkActorManager.cpp`
- `EterLib/NetStream.cpp::Connect`
- `EterLib/NetStream.cpp::Clear`
## Current call chain
- `extension/src/net/classic/classic_session.cpp::connect_direct_enter`
- `extension/src/net/classic/classic_session.cpp::connect_warp`
- `extension/src/net/classic/classic_session.cpp::pump`
- `extension/src/net/m2_client.cpp::pump_classic`
- `extension/src/net/m2_client.cpp::warp_to_game_server`
- `extension/src/net/m2_client.cpp::reconnect`
- `extension/src/net/entity_store.cpp::reset_for_map_change`
- `project/game_scene.gd::_on_world_reset`
- `project/net_play.gd::clear_for_map_change`
- `project/net_world.gd::clear_for_map_change`
- `project/app_flow.gd::_on_disconnected`, `_on_char_list`, `_enter_character`
- `project/ui/reconnect_ui.gd::_retry_now`
## Reference branch matrix
| 场景 | 40250 实现 | 当前端实现 | 结论 |
|---|---|---|---|
| 普通选人进入 | `ConnectGameServer(slot)` 在同一个 `CPythonNetworkStream` 保存 `m_dwSelectedCharacterIndex` 并设置 DirectEnter,再连接角色地址 | `ClassicSession::connect_direct_enter` 保存 `m_selected_slot/m_direct_enter_slot`,发送登录 key 后在 SELECT 进入 Loading 并发送 select | MAPPED:已覆盖主要状态和包序,但仍需真实服务端断点证据 |
| `GC_WARP` | `RecvWarpPacket` 消费完整 warp;设置当前 selected slot 的 DirectEnter;在同一个 `CNetworkStream` 上 `Connect(lAddr,wPort)`;`Connect` 先清旧 socket/缓冲/加密/sequence | `pump_classic` 每次只取一个 warp cue,先 `EntityStore::reset_for_map_change` 和 `world_reset`,再在同一 `ClassicSession` 调 `connect_warp`;`ClassicStream::disconnect/connect` 清 socket/缓冲/加密/sequence,后续 warp 保留在队列 | PARTIAL:单次消费和保留状态方向已对齐,但清理 owner/真实包序仍不同 |
| warp 地址 | 40250 对 `lAddr,wPort` 直接 `Connect`,即使回到当前 channel 也按重连处理 | classic warp 同样格式化 `addr/port` 后连接;Godot/旧 backend 仍有 `same_server` 分支 | PARTIAL:classic 主链接近,双 backend 的 same-server 语义仍未统一 |
| Loading 进入 | `SetLoadingPhase` 设置 Loading process/leave、`CPythonPlayer::Clear`、删除全部飞行物和全部效果、清空 DirectEnter mode | classic warp 路径在收到 warp 时提前清 EntityStore/Godot actor/fly/effect/UI,再由 `SetLoading` 等价状态继续;`connect_warp` 将 DirectEnter 重新设置为 true | PARTIAL:当前在旧连接仍可能处理剩余 queue 前先清理;DirectEnter 清除/重设顺序与参考不同 |
| Game phase 离开 | `__LeaveGamePhase` 清 PVP key、NetworkActorManager、combo、character/item manager;随后 Loading 再清 player/fly/effect | `_on_world_reset`、`net_play.clear_for_map_change`、`net_world.clear_for_map_change`、`EntityStore.reset_for_map_change` 分散清理 | PARTIAL:覆盖面较全,但没有一份可证明顺序与重复调用幂等的单一 owner |
| warp 后角色槽位 | 同一 `CPythonNetworkStream` 保留 `m_dwSelectedCharacterIndex` 与 DirectEnter;新 Select/Loading 流程不回普通选人 | classic `connect_warp` 和有 selected slot/login key 的 `M2Client::warp_to_game_server` 均保留同一 `ClassicSession`;无票据/无槽位时仍有 fallback | PARTIAL:主链已对齐,fallback 和真实服务端包序仍待证 |
| 远端断线 | `CNetworkStream::Clear` 清 transport;Python phase/应用层按已有网络状态处理,不自动模拟角色列表 UI 回放 | `ClassicSession` 进入 Failed 并发 `disconnected`;GameScene 保留;ReconnectUI 每 5 秒最多重试 5 次 | PARTIAL:当前增加了 UI 自动重试和角色列表回放,未证明与参考的 phase/owner 行为一致 |
| generic reconnect | 40250 没有 `M2Client::reconnect` 这条新 session + Godot slot 回放调用链;已有网络对象和选中状态是参考依据 | 有 login key 且已有 selected slot 时,`M2Client::reconnect` 调用同一 `ClassicSession::connect_warp`;仅无票据/无槽位时才走新 session fallback | PARTIAL:主 classic 重连方向一致,fallback 和真实服务端包序仍待证 |
| 重连失败 | 由 `Connect`/phase process 的结果进入已有 network phase/error 路径;只有 `PHASE_GAME` 后才属于远端游戏断线 | classic `Failed` 按 `was_online_lost` 转 `disconnected`,AppFlow 保留 GameScene;`PHASE_GAME` 前连接失败走 `login_failed` | PARTIAL:事件分类已修正,但 timeout、重复回调和最终停止条件未与参考建立对应 |
| 重试并发 | 参考没有独立 5 秒 UI timer;连接对象本身负责一次 Connect 和 limitSec | `M2Client::reconnect` 对 Auth/GameConnect/GameLogin 做幂等 in-flight guard;ReconnectUI 的 5 秒 timer 仍是平台适配 | PARTIAL:guard 已有 native 实现,尚缺真实重复点击/旧 socket 竞态证据 |
| 超时 | Win32 `Connect` 记录 `m_connectLimitTime`,`OnProcess` 检查连接时限并清理 | `ClassicStream::connect` 使用 nonblocking connect,并默认记录 3000ms monotonic deadline;`process` 超时清理并进入失败状态 | MAPPED:连接 deadline 算法已对齐,仍需实际阻塞连接测试 |
## Cleanup order matrix
| 顺序点 | 40250 | 当前端 | 结论 |
|---|---|---|---|
| 1 | `RecvWarpPacket` 先读完 warp,再调用 `Connect`;`Connect::Clear` 丢弃旧 socket 中 warp 后的尾包 | parser 产生 warp cue 后,`ClassicStream` 在当前帧结束时清空剩余接收缓冲,`M2Client` 再 drain cue、reset world 并 connect | MAPPED:classic 旧连接尾包不会跨越 GC_WARP 边界 |
| 2 | `Connect::Clear` 清 socket、cipher、recv/send buffer、sequence | `ClassicStream::disconnect` 清 socket、buffer、sequence、cipher、phase | MAPPED |
| 3 | 进入 Loading 后 `CPythonPlayer::Clear`、Flying、Effect、DirectEnter initialize | `_on_world_reset` 清 NetPlay/NetWorld/Ground/UI/audio,EntityStore 清实体/状态/效果/fly/队伍局部状态 | PARTIAL:数据集合基本覆盖,owner 和时序不同 |
| 4 | Game leave 清 NetworkActor/character/item manager | GameScene/NetPlay/NetWorld 节点可能继续存活,靠 reset 方法清状态;节点本身不重建 | PARTIAL |
| 5 | 新 MainCharacter/Loading 数据重新建立 actor/main VID | 当前收到新包后由 EntityStore signal/catch-up/scene model 重建 | PARTIAL:需真实 warp burst 和重复 warp 测试 |
## Equivalence matrix
| 项目 | 状态 | 证据 |
|---|---|---|
| Preconditions | PARTIAL | classic `connect_warp` 检查 selected slot/login key;generic `reconnect` 增加连接中幂等 guard,但 fallback 仍需真实失败矩阵 |
| Branch structure | PARTIAL | GC_WARP 和有槽位的 generic reconnect 均走保留 session 分支;无槽位/无 ticket 仍走销毁/新建 session |
| Algorithms/formulas | MAPPED | warp 地址、端口、坐标单位和 IPv4 字节序已有明确映射 |
| State transition order | PARTIAL | 40250 是同一 stream 的 Connect→Loading;当前包含 external world_reset、Godot 场景清理和新 session 补偿路径 |
| Constants/units | MAPPED | `lAddr/wPort`、角色槽位和 server coordinate 转换均已定位 |
| Timing/event sources | PARTIAL | 参考由 GC_WARP/phase/Connect limit 驱动;当前另加 `ReconnectUI` 5 秒 timer 和 Loading enter delay |
| Resource/data sources | PARTIAL | GC_WARP 和 classic fast reconnect 使用当前 parser session;fallback 仍使用 cfg/login key 并重新取 char list |
| Protocol side effects | PARTIAL | 主 classic 重连保持 login key/direct-enter;fallback 仍可能先回角色列表再选人,包序不同 |
| Interruption/failure/cleanup | PARTIAL | 旧 transport 和多数世界状态可清理,但 timeout、重复 reconnect、重复 warp、旧 queue 和 UI retry 终止未闭环 |
## Required parity tests
- 真实 `GC_WARP`:记录旧 socket close、新 socket endpoint、login key、selected slot、DirectEnter 和 `PHASE_SELECT→CG_PLAYER_SELECT→PHASE_LOADING` 包序。
- 同服地址与跨服地址分别测试;确认两者都遵循 40250 的 `Connect(lAddr,wPort)`,且不会遗漏 world reset。
- warp burst 前后验证 EntityStore、NetWorld、NetPlay、GroundItems、Flying/Effect、PVP/duel、shop/exchange/safebox、quickslot/skill/points 的清理和重建顺序。
- 远端断线后验证:旧连接只产生一次 `disconnected`,重连期间不重复创建 session,不短暂切到 Login/Select,成功后回到原 slot。
- 对 `reconnect()` 加 in-flight、连接失败、连接超时、5 次上限、手动立即重试、再次断线和停止自动重试矩阵。
- 连续两个 `GC_WARP`、warp 后立刻断线、旧连接尾包与新连接首包交错等竞态测试。
本轮新增的包级边界回归:`GC_WARP + GC_CHARACTER_ADD` 同一读缓冲中到达时,旧尾包
不会在 host 调用 `connect_warp()` 前写入 EntityStore;warp cue 仍按 FIFO 保留给外层
重连 owner。classic session 与 legacy `GameClient` 都在当前 warp 帧后丢弃旧缓冲;
该测试仍不替代真实旧/新 socket 交错、重复 warp 和 datagram 注销验证。
同时新增真实 loopback 断开回归:TCP 已建立但尚未收到 `PHASE_GAME` 时 peer close,
不会设置 `was_online_lost`,从而不会把角色选择/DirectEnter 失败误导向局内重连。
## Evidence run
- `build/extension/net_classic_session_test` 已覆盖 direct-enter 的 slot endpoint、GC_WARP 的目标 endpoint、selected slot/login key 保留和 warp cue。
- `build/extension/net_entity_test` 额外覆盖多个 warp cue 的 FIFO 取出:第一个重连被消费后,第二个仍保留,不会因 owner `return` 丢失。
- `project/app_flow_lifecycle_test.gd` 已覆盖游戏态断线遮罩、角色列表回放和原 slot 选择的 UI 级行为。
- `project/gamescene_test.gd`、`project/p9_test.gd` 通过,证明世界重置/地图换图的局部装配和 warp 辅助状态没有断言失败。
- `project/p10_test.gd` 当前已通过;此前失败的 Loading 遮罩和无 SceneTree timer 问题已在前一轮修复。
- 静态逐行核对确认:classic generic reconnect 现在保留同一 session 的 selected slot/login key/DirectEnter;`ClassicStream::connect` 已有 3000ms deadline;`M2Client::reconnect` 对连接中状态做幂等保护。
- `project/disconnect_world_reset_test.gd` 已验证显式 `M2Client.disconnect_from_server()` 在 transport teardown 前发出一次 `world_reset`,重复调用仍按每个离线边界通知一次,避免保留的 GameScene 依赖 session 析构清理旧 presentation state。
- 现有测试仍没有证明真实 M2Client 重复点击、deadline 触发、旧/新 socket 交错和同服/跨服包序,因此合同保持 `PARTIAL`。
## Deep audit round 2026-09-20(历史快照,部分结论由后续修复轮覆盖)
本轮按 40250 的实际调用顺序重新核对了以下不可合并的差异:
1. `RecvWarpPacket` 在 40250 中读完完整 `TPacketGCWarp` 后,直接在原 `CNetworkStream` 上 `Connect(lAddr,wPort)`;当前 classic 专用分支先在 `M2Client::pump_classic` 外部发出 `world_reset`,再调用同一 `ClassicSession` 的 `connect_warp`。传输清理方向一致,但清理 owner/时序不是 1:1,仍需真实旧 socket 尾包和新 socket 首包测试。
2. 当时 generic reconnect 与 `GC_WARP` 不能视为同一实现:`reconnect()` → `warp_to_game_server()` 会 `disconnect()`、销毁 `ClassicSession`;该差异已由 2026-09-21 修复轮收敛,当前主 classic 分支保留原 session。
3. 40250 的 `SetLoadingPhase()` 明确执行 `CPythonPlayer::Clear()`、删除全部 Flying/Effect 并 `__DirectEnterMode_Initialize()`;当前 `_on_world_reset()` 将清理分散到 EntityStore、NetPlay、NetWorld、GroundItems、UI 和 scene 状态,随后 `connect_warp()` 又把 DirectEnter 设回 true。覆盖面存在,但顺序和单一属主未对齐。
4. 40250 的非阻塞 `Connect` 在 `OnProcess()` 以 `m_connectLimitTime` 超时后清理并触发失败;该 deadline 已由 2026-09-21 修复轮加入 `ClassicStream`,仍需实际阻塞连接测试。
5. 当时 `p10_test` 失败的 Loading/SceneTree 问题已修复;当前 `p10_test`、`app_flow_lifecycle_test`、`gamescene_test`、`p9_test` 均通过。
## Active UserInterface datagram/stream reconnect review
- `PythonNetworkDatagram*` 的状态 sender 表和 `PythonNetworkStream.h` 的 phase/请求 API 共同决定 warp、observer、角色和 UI 请求在何时仍可发送;`PythonNetworkStreamPhaseOffline.cpp` 明确切离 offline 时不能保留旧 phase 的 packet 状态。
- 本轮静态核对确认当前端虽然有 `connect_warp`/world reset,但仍需补 datagram sender 注销、offline 旧包丢弃、warp 前后 selected/direct-enter 状态和新旧 socket 尾包隔离的逐分支证据;合同保持 `PARTIAL`。
## Implementation fix round 2026-09-21
按 40250 `CNetworkStream::Connect(limitSec=3)` 和同一 stream `Clear()->Connect()` 语义修复 classic 重连:`ClassicStream` 为非阻塞 TCP 连接记录 3000ms 单调时钟 deadline,超时清理 socket/buffer/cipher/sequence 并进入失败;`M2Client::reconnect` 在已有 login key 和 selected slot 时不再销毁 `ClassicSession`,而是清理 world 后调用同一 session 的 `connect_warp`,保留角色槽位、login key 和 DirectEnter。连接中的重复重试改为幂等返回,不创建第二个 session。
回归:`net_classic_session_test`、`net_classic_stream_test`、完整 `ctest`(23/23)、`p10_test`、`app_flow_lifecycle_test`、`gamescene_test`、`p9_test` 和 `git diff --check` 均通过。真实阻塞连接 deadline、重复点击/旧 socket 竞态、同服/跨服 burst 包序及 datagram 注销仍保持 `PARTIAL`。
## Audit/fix round 2026-09-20 — warp queue ownership
继续核对 `PythonNetworkStreamPhaseGame.cpp::RecvWarpPacket` 的“消费一个完整 warp 后立即替换同一连接”语义与当前 `M2Client::pump_classic/pump_game`:当前实现原先对 `drain_warps()` 返回的整个 vector 遍历,但在第一个跨服 warp 后立即 `return`,导致同一帧已进入队列的后续 warp 被一起清空;这与参考端后续 phase tick 保留后续包不一致。
修复内容:
- `EntityStore::take_next_warp()` 改为 FIFO 一次取一个 cue,未处理 cue 保留在队列中。
- classic 和 legacy game 两条 M2Client warp 分支都改为每次 pump 只消费一个 warp;跨服重连 `return` 后,下一次 pump 仍能看到后续 cue。
- 新增 `net_entity_test` 的双 warp 顺序/队列耗尽断言。
回归:`net_entity_test`、`net_classic_session_test`、`net_loopback_test`、`p9_test.gd`、`gamescene_test.gd`、`p10_test.gd`、`app_flow_lifecycle_test.gd` 和 `git diff --check` 均通过;完整 `ctest` 在前一轮已为 23/23。
仍为 `PARTIAL`:真实旧/新 socket 交错、同服/跨服 live 包序、连接 deadline 实际超时、重复 reconnect/warp、datagram 注销和清理 owner 的时序仍需继续取证。
## Implementation fix round 2026-09-22T08:20Z
复核 40250 `SetOffLinePhase`/`__LeaveGamePhase` 后发现,当前显式 `M2Client.disconnect_from_server()` 只销毁 auth/game/classic session;如果 `AppFlow` 保留 `GameScene` 进入断线重连或用户主动离线,场景侧不会收到 `world_reset`,旧 Actor、飞行、目标、UI 和 BGM 状态只能等待节点销毁。按参考端“先清 phase owner,再清 transport”的顺序,当前实现先对活动 `EntityStore` 调用 `reset_for_map_change()`,发出 `world_reset`,再断开各 transport 并回到 Idle。
修复前新增 `disconnect_world_reset_test.gd` 稳定复现两项失败;修复后通过,且 `app_flow_lifecycle_test.gd`、`netbridge_test.gd` 通过。该修复只闭合显式 disconnect 的清理通知,不代表真实 GC_WARP/重连包序、统一 generation、旧新 socket 竞态和通用 EffectManager 已等价,合同仍保持 `PARTIAL`。
## Status
`PARTIAL`。