- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
146 lines
16 KiB
Markdown
146 lines
16 KiB
Markdown
# 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`。
|