- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
16 KiB
network.reconnect_warp
Scope
核对游戏中 GC_WARP 换图、断线重连、保留角色槽位/direct-enter、旧 socket/实体/特效清理和重试策略。换图和断线重连虽然都要重新建立 TCP,但必须分别对照 40250 的调用链,不能只因为最终进入同一张地图就判定等价。
Reference call chain
UserInterface/PythonNetworkStreamPhaseGame.cpp::GamePhaseUserInterface/PythonNetworkStreamPhaseGame.cpp::RecvWarpPacketUserInterface/PythonNetworkStream.cpp::ConnectGameServerUserInterface/PythonNetworkStream.cpp::SetLoadingPhaseUserInterface/PythonNetworkStreamPhaseLoading.cpp::SetLoadingPhaseUserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhaseUserInterface/NetworkActorManager.cppEterLib/NetStream.cpp::ConnectEterLib/NetStream.cpp::Clear
Current call chain
extension/src/net/classic/classic_session.cpp::connect_direct_enterextension/src/net/classic/classic_session.cpp::connect_warpextension/src/net/classic/classic_session.cpp::pumpextension/src/net/m2_client.cpp::pump_classicextension/src/net/m2_client.cpp::warp_to_game_serverextension/src/net/m2_client.cpp::reconnectextension/src/net/entity_store.cpp::reset_for_map_changeproject/game_scene.gd::_on_world_resetproject/net_play.gd::clear_for_map_changeproject/net_world.gd::clear_for_map_changeproject/app_flow.gd::_on_disconnected,_on_char_list,_enter_characterproject/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 取出:第一个重连被消费后,第二个仍保留,不会因 ownerreturn丢失。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 的实际调用顺序重新核对了以下不可合并的差异:
RecvWarpPacket在 40250 中读完完整TPacketGCWarp后,直接在原CNetworkStream上Connect(lAddr,wPort);当前 classic 专用分支先在M2Client::pump_classic外部发出world_reset,再调用同一ClassicSession的connect_warp。传输清理方向一致,但清理 owner/时序不是 1:1,仍需真实旧 socket 尾包和新 socket 首包测试。- 当时 generic reconnect 与
GC_WARP不能视为同一实现:reconnect()→warp_to_game_server()会disconnect()、销毁ClassicSession;该差异已由 2026-09-21 修复轮收敛,当前主 classic 分支保留原 session。 - 40250 的
SetLoadingPhase()明确执行CPythonPlayer::Clear()、删除全部 Flying/Effect 并__DirectEnterMode_Initialize();当前_on_world_reset()将清理分散到 EntityStore、NetPlay、NetWorld、GroundItems、UI 和 scene 状态,随后connect_warp()又把 DirectEnter 设回 true。覆盖面存在,但顺序和单一属主未对齐。 - 40250 的非阻塞
Connect在OnProcess()以m_connectLimitTime超时后清理并触发失败;该 deadline 已由 2026-09-21 修复轮加入ClassicStream,仍需实际阻塞连接测试。 - 当时
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。