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

16 KiB
Raw Blame History

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;在同一个 CNetworkStreamConnect(lAddr,wPort)Connect 先清旧 socket/缓冲/加密/sequence pump_classic 每次只取一个 warp cue,先 EntityStore::reset_for_map_changeworld_reset,再在同一 ClassicSessionconnect_warpClassicStream::disconnect/connect 清 socket/缓冲/加密/sequence,后续 warp 保留在队列 PARTIAL:单次消费和保留状态方向已对齐,但清理 owner/真实包序仍不同
warp 地址 40250 对 lAddr,wPort 直接 Connect,即使回到当前 channel 也按重连处理 classic warp 同样格式化 addr/port 后连接;Godot/旧 backend 仍有 same_server 分支 PARTIALclassic 主链接近,双 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_resetnet_play.clear_for_map_changenet_world.clear_for_map_changeEntityStore.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 清 transportPython phase/应用层按已有网络状态处理,不自动模拟角色列表 UI 回放 ClassicSession 进入 Failed 并发 disconnectedGameScene 保留;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 Failedwas_online_lostdisconnectedAppFlow 保留 GameScenePHASE_GAME 前连接失败走 login_failed PARTIAL:事件分类已修正,但 timeout、重复回调和最终停止条件未与参考建立对应
重试并发 参考没有独立 5 秒 UI timer;连接对象本身负责一次 Connect 和 limitSec M2Client::reconnect 对 Auth/GameConnect/GameLogin 做幂等 in-flight guardReconnectUI 的 5 秒 timer 仍是平台适配 PARTIALguard 已有 native 实现,尚缺真实重复点击/旧 socket 竞态证据
超时 Win32 Connect 记录 m_connectLimitTimeOnProcess 检查连接时限并清理 ClassicStream::connect 使用 nonblocking connect,并默认记录 3000ms monotonic deadlineprocess 超时清理并进入失败状态 MAPPED:连接 deadline 算法已对齐,仍需实际阻塞连接测试

Cleanup order matrix

顺序点 40250 当前端 结论
1 RecvWarpPacket 先读完 warp,再调用 ConnectConnect::Clear 丢弃旧 socket 中 warp 后的尾包 parser 产生 warp cue 后,ClassicStream 在当前帧结束时清空剩余接收缓冲,M2Client 再 drain cue、reset world 并 connect MAPPEDclassic 旧连接尾包不会跨越 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/audioEntityStore 清实体/状态/效果/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 keygeneric 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 sessionfallback 仍使用 cfg/login key 并重新取 char list
Protocol side effects PARTIAL 主 classic 重连保持 login key/direct-enterfallback 仍可能先回角色列表再选人,包序不同
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() 前写入 EntityStorewarp 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.gdproject/p9_test.gd 通过,证明世界重置/地图换图的局部装配和 warp 辅助状态没有断言失败。
  • project/p10_test.gd 当前已通过;此前失败的 Loading 遮罩和无 SceneTree timer 问题已在前一轮修复。
  • 静态逐行核对确认:classic generic reconnect 现在保留同一 session 的 selected slot/login key/DirectEnterClassicStream::connect 已有 3000ms deadlineM2Client::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 后,直接在原 CNetworkStreamConnect(lAddr,wPort);当前 classic 专用分支先在 M2Client::pump_classic 外部发出 world_reset,再调用同一 ClassicSessionconnect_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 的非阻塞 ConnectOnProcess()m_connectLimitTime 超时后清理并触发失败;该 deadline 已由 2026-09-21 修复轮加入 ClassicStream,仍需实际阻塞连接测试。
  5. 当时 p10_test 失败的 Loading/SceneTree 问题已修复;当前 p10_testapp_flow_lifecycle_testgamescene_testp9_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_testnet_classic_stream_test、完整 ctest23/23)、p10_testapp_flow_lifecycle_testgamescene_testp9_testgit 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_testnet_classic_session_testnet_loopback_testp9_test.gdgamescene_test.gdp10_test.gdapp_flow_lifecycle_test.gdgit 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.gdnetbridge_test.gd 通过。该修复只闭合显式 disconnect 的清理通知,不代表真实 GC_WARP/重连包序、统一 generation、旧新 socket 竞态和通用 EffectManager 已等价,合同仍保持 PARTIAL

Status

PARTIAL