# network.login.phase_flow ## Scope 对比 40250 从连接、握手、认证、登录、角色列表、直连选人、Loading 到 GAME 的阶段属主、包顺序、登录 key、失败回调、可选安全包、超时和重试清理,与当前 `ClassicSession` / `M2Client` / `AppFlow` 的实现。 ## Reference call chain ### 游戏连接与握手 - `UserInterface/PythonNetworkStream.cpp::Connect` - `UserInterface/PythonNetworkStreamPhaseHandShake.cpp::SetHandShakePhase/HandShakePhase` - 初始 `GC_HANDSHAKE -> CG_HANDSHAKE` - 记录 server time / client time - `CG_TIME_SYNC`、sequence 和可选 key agreement - `EterLib/NetStream.cpp` ### 登录与账号认证 - `UserInterface/AccountConnector.cpp::Connect/Process/OnConnectFailure` - `UserInterface/AccountConnector.cpp::__AuthState_RecvPhase` - `PHASE_AUTH -> CG_LOGIN3` / 相关 client key - `GC_AUTH_SUCCESS -> SetLoginKey -> CPythonNetworkStream::Connect` - auth close/failure 回调登录 UI - `UserInterface/PythonNetworkStreamPhaseLogin.cpp::SetLoginPhase/LoginPhase` - `CG_LOGIN` 或 `CG_LOGIN2(login_key)` - 清理内存中的密码 - `GC_LOGIN_SUCCESS3/4`、`GC_LOGIN_FAILURE`、`GC_EMPIRE`、矩阵/Passpod/Hybrid 分支 - `UserInterface/PythonNetworkStreamPhaseLogin.cpp::__RecvLoginSuccessPacket3/4` - 保存固定四槽角色数据,空槽保持零初始化 - 非 DirectEnter 路径只刷新 SelectPhase,不自动选择角色 - `UserInterface/PythonNetworkStream.cpp::RecvPhasePacket/ClosePhase` ### 选人与进入游戏 - `UserInterface/PythonNetworkStream.cpp::ConnectGameServer` - 保存 selected slot,设置 DirectEnter,连接角色地址 - `UserInterface/PythonNetworkStreamPhaseSelect.cpp::SetSelectPhase/SelectPhase` - 普通选人页或 DirectEnter Loading UI - DirectEnter 的 `introloading.LoadingWindow.Open -> net.SendSelectCharacterPacket(chrSlot)` 发送 `CG_PLAYER_SELECT` - `UserInterface/PythonNetworkStreamPhaseLoading.cpp::SetLoadingPhase/SendEnterGame` - `UserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhase` ## Current call chain - `project/app_flow.gd::_do_connect/_enter_character` - `extension/src/net/m2_client.cpp::connect_to_server/pump_auth/pump_game/pump_classic` - `extension/src/net/classic/classic_session.cpp` - `connect/retry_login/connect_with_login_key/connect_direct_enter` - `on_auth_phase/on_auth_packet/on_phase/on_packet` - `send_auth_login/send_login/send_login_by_key/send_select_char/enter_game` - `extension/src/net/classic/classic_stream.cpp::process/handle_control/dispatch` - `extension/src/net/classic/classic_parser.cpp::on_gc` - `extension/tools/net_classic_e2e.cpp`(真实包序探针,支持全空角色账号只验证 SelectPhase) - `project/ui/loading_screen.gd`, `project/ui/reconnect_ui.gd` 登录失败后的重试还经过: - `project/app_flow.gd::_do_connect -> M2Client::retry_login` - `M2Client::retry_login -> ClassicSession::retry_login -> ClassicSession::connect` - `ClassicSession::connect -> ClassicStream::connect(auth)`:只替换 AccountConnector 对应的认证 socket;首次登录或没有待重试 owner 时才回退到 `M2Client::connect_to_server` 的完整清理路径。 ## Branch matrix | 场景 | 40250 | 当前端 | 结论 | |---|---|---|---| | 初始连接/握手 | `CNetworkStream::Connect` 后由 HandShake phase 回显时间、建立 server clock,再进入 Login;`NetStream` 提供 3 秒 TCP connect limit | `ClassicStream` 非阻塞 connect、DH2/CTR、握手回显、time-sync 和 3 秒 TCP connect deadline 已实现,`ClassicSession` 保持 Connecting 到 LOGIN | MAPPED 主路径;phase 静默/失败矩阵仍未完整证明 | | 认证成功 | AccountConnector 收 `GC_AUTH_SUCCESS`,保存 login key,断开 auth 后连接目标 game server | `on_auth_packet` 保存 `m_auth_login_key`,下一 pump 关 auth、建 game stream,再用 `CG_LOGIN2` | PARTIAL:顺序接近,但 auth/旧 stream/错误回调不完全同 owner | | 认证失败/关闭 | 账号 UI 收具体状态或 OnConnectFailure,可按 Login 逻辑重新尝试;`OnRemoteDisconnect -> __OfflineState_Set` | `GC_LOGIN_FAILURE` 通过 `on_login_failure` 通知并保留 AUTH owner;拒绝的 `GC_AUTH_SUCCESS` 通知 `BESAMEKEY`,在当前帧后清理 auth stream;auth peer close 回 `ClassicSession::Offline`,M2Client 保留非终态 LoginPhase owner,不伪造 `login_failed`/`connect_failed` | PARTIAL:真实连接失败/重试入口和 UI 包序仍未完整证明 | | 普通登录 | `SetLoginPhase` 发送 `CG_LOGIN`/`CG_LOGIN2`,清密码,收 LOGIN_SUCCESS3/4 后刷新选人 | `send_login/send_login_by_key` 按每次 `PHASE_LOGIN` 重新执行;parser 支持 3/4 槽位,成功写入后清理 session password,host retry cache 同步清理 | PARTIAL:协议主路径和凭据发送边界已对齐,真实包序仍不同 | | 登录失败包 | `GC_LOGIN_FAILURE` 回调 `OnLoginFailure(status)`,保留 Login phase 处理能力;`intrologin.OnLoginFailure` 关闭 connecting dialog、保留原 LoginPhase 窗口并回到密码框 | parser 解析 status,`ClassicSession::on_packet` 触发 `on_login_failure` 但保持 `LoggingIn`,M2Client 发 `login_failed`;AppFlow 在 LOGIN 阶段保留原登录 `CanvasLayer`、更新原 Status 并请求密码框焦点;不再把应用层拒绝变成 transport failure | PARTIAL:跨阶段失败、完整 retry 包序和真实服务端 UI/重试仍未完整证明 | | PHASE_CLOSE | `ClosePhase -> SetLoginPhase`,由 Login/Offline owner 接管,不是普通 transport error;若原 owner 是 Game/Loading,先运行对应 phase-leave 清理 | 游戏 `PHASE_CLOSE` 在 `ClassicSession` 中先清理 Loading/Game 的 map-local world,并通过 `on_phase_leave`/`phase_changed(login)` 通知宿主;`M2Client::on_phase_close` 不再伪造 `disconnected` 或占用远程断线重试 owner;AppFlow 释放 GameScene 并切换为唯一 LoginPhase owner;auth close 保留 AUTH owner | PARTIAL:真实 PHASE_CLOSE 包序、跨阶段重试和旧 stream 乱序仍未证明 | | DirectEnter | 40250 保存 slot/login key,连接 slot 的 lAddr:wPort;进入 Select 后切 Loading,由 `LoadingWindow.Open` 发送该 slot 的 `CG_PLAYER_SELECT`,收到主角包后由 `GameWindow.LoadData` 发送 `CG_ENTERGAME` 进入地图 | `connect_direct_enter/connect_warp` 连接 slot endpoint;`PHASE_SELECT` 发送选中 slot 的 `CG_PLAYER_SELECT` 后切 Loading,`GC_MAIN_CHARACTER` 事件中发送 `CG_ENTERGAME`;连接失败的 ClosePhase owner 会清除 DirectEnter 模式 | PARTIAL:DirectEnter 和一次成功 GC_WARP 的本地/实时包序已验证,失败回退和完整服务端矩阵仍未证明 | | 普通选人 | Select UI 发 `CG_PLAYER_SELECT`,再等待 Loading/Game | `ClassicSession::select_char` 只允许 CharSelect stage,M2Client 同步 selected_pid | MAPPED 主路径;重复点击/阶段乱序测试不足 | | Loading/EnterGame | SetLoadingPhase 先清 player/fly/effect/direct-enter;`GC_MAIN_CHARACTER -> GameWindow.LoadData -> SendEnterGame` 按明确事件调用并带 sequence;未识别包经 `RecvErrorPacket` 清理缓冲但保留 phase/socket owner | `GC_MAIN_CHARACTER`/BGM 主角包解析完成并发送版本包后,`ClassicSession::enter_game()` 在同一 Loading 事件边界发送一次 `CG_ENTERGAME`;`m_entergame_sent` 防重复,未知包仍清理未读缓冲并保留 owner | PARTIAL:EnterGame 事件边界已对齐;Loading reset 的完整资源时序、真实 Windows UI 和其它服务器包序仍未完整证明 | | 可选账号/安全分支 | Matrix、Runup、Passpod、Hybrid、Improved Encryption 有独立收发和 UI/资源副作用 | `ClassicSession` 对静态可选包和 `ClassicStream` 对 HybridCrypt 动态包均完成分帧消费,并通过 `on_unsupported_security`/`optional_security_required` 明确报告部署能力缺口;当前仍无 Matrix/Passpod UI、EterPack HybridCrypt/Panama 注入或 HS/XTrap runtime | PARTIAL:不静默断开,但功能副作用仍未等价 | | 断线/超时 | 已建立 TCP 后 `OnRemoteDisconnect -> SetLoginPhase`;连接建立失败走 `OnConnectFailure`,DirectEnter 额外经过 `ClosePhase`;AccountConnector 认证端断线回 Offline | `ClassicStream` 在清理 socket 前分别通知 remote/connect failure;游戏 peer close 清理旧 world、回 `LoggingIn`,AppFlow 释放 GameScene 并成为唯一 LoginPhase owner;认证 peer close 回 `Offline`,M2Client 发登录失败并允许显式重试 | PARTIAL:真实游戏 peer-close 已验证;认证/握手/Loading 静默、PHASE_CLOSE live、真实完整包序和跨阶段 retry 仍未完整证明 | | 重复进入 | 参考由 phase、direct-enter 和 `m_isStartGame` 控制;重复 phase 会重新进入对应 phase handler | `PHASE_LOGIN/PHASE_AUTH` 每次重新执行对应发送路径;`CG_ENTERGAME` 仍由 `m_entergame_sent` 防同一 Loading 周期重复,AppFlow/重连 UI 的交叉保护仍未闭环 | PARTIAL | ## Equivalence matrix | 项目 | 状态 | 证据 | |---|---|---| | Preconditions | PARTIAL | 当前检查 server address、slot、login key 和 stage;direct-enter/旧连接/凭据生命周期未完全对齐 | | Branch structure | PARTIAL | 普通成功主路径已映射;close、可选安全包、超时、登录失败保留 phase 和重复进入仍不同 | | Algorithms/formulas | PARTIAL | DH2/CTR、sequence、time-sync、登录 key 已实现;deadline、retry 和 enter-game 时序没有 40250 等价证明 | | State transition order | PARTIAL | 参考由 phase callback + phase leave + account connector 串行推进;当前由 stream/session/stage/signal/AppFlow 分散推进 | | Constants/units | PARTIAL | 包大小、槽位、login key、sequence 表可对上;EnterGame 已不再使用没有 40250 依据的固定 1500ms 延迟 | | Timing/event sources | PARTIAL | 当前 `_process`/socket pump/协程/重连 UI;参考 `CPythonApplication::Process`、phase callback 和 timer owner 不同 | | Resource/data sources | PARTIAL | 账号地址、角色槽、login key 已定位;当前把密码保存在 `ClassicSession`/M2Client config,参考在发送后清理 | | Protocol side effects | PARTIAL | CG_LOGIN3/CG_LOGIN2/CG_PLAYER_SELECT/CG_ENTERGAME 主包存在;错误包、CLOSE、矩阵/Passpod/Hybrid 和 direct-enter副作用不等价 | | Interruption/failure/cleanup | PARTIAL | socket error 和 manual disconnect 有路径;超时、旧 auth/game stream、失败后重试、重复 enter 和统一 loading reset 缺闭环 | ## Test evidence and boundary 通过的协议回归: - `build/extension/net_classic_session_test` - `build/extension/net_classic_stream_test` - `build/extension/net_loopback_test` - `project/app_flow_lifecycle_test.gd` - `network.login.phase_flow/login-failure-retry-cleanup`:`net_classic_session_test` 覆盖 auth peer close -> Offline owner、pre-game/in-game peer close、旧 world 清理、DirectEnter 连接失败回 LoginPhase、半包和重复 phase。 - `network.login.phase_flow/auth-game-phase-matrix`:`net_classic_session_test` 覆盖半包、重复 `PHASE_AUTH/PHASE_LOGIN`、空角色列表、auth/game 登录拒绝和 `PHASE_CLOSE`。 - `network.login.phase_flow/auth-game-phase-matrix/phase-close-world-owner`:`net_classic_session_test` 覆盖游戏阶段 `PHASE_CLOSE` 的旧 actor 清理、宿主通知顺序和 LoginPhase 重入。 - `network.login.phase_flow/login-failure-retry-cleanup/ui-login-owner`:`app_flow_lifecycle_test.gd` 覆盖 LOGIN 阶段收到 `login_failed` 后保留原 LoginPhase UI、原密码字段和 Status 面板。 - `network.login.phase_flow/auth-game-phase-matrix/peer-close-login-owner`:`app_flow_lifecycle_test.gd` 和 `p10_test.gd` 覆盖游戏 peer close/phase-only LoginPhase 后释放 GameScene、隐藏重连遮罩、回到唯一 LoginPhase owner,且角色列表不会自动重入旧槽位。 - `network.login.phase_flow/ticket-slot-warp-order/direct-enter-login2-packet`:`net_classic_session_test` 覆盖 DirectEnter/GC_WARP 两次 `CG_LOGIN2` 的 header、sequence、login key、selected slot 保留、旧 `PHASE_GAME` 标记清除和新 `PHASE_GAME` 重新建立。 - `network.login.phase_flow/ticket-slot-warp-order/direct-enter-loading-select-packet`:`net_classic_session_test` 覆盖 DirectEnter 在 `PHASE_SELECT` 进入 Loading 前发送一个带 selected slot 的 `CG_PLAYER_SELECT`;实时探针覆盖该包之后继续到 `PHASE_GAME`。 - `network.login.phase_flow/ticket-slot-warp-order/gc-warp-live-package-order`:脱敏实时探针通过 GM `/warp` 触发真实 `GC_WARP`,观察到旧 world reset、replacement `CG_LOGIN2`、DirectEnter `CG_PLAYER_SELECT`、Loading 和第二次 `PHASE_GAME`;同时保留 e2e 可选命令入口用于回归。 - `network.login.phase_flow/auth-game-phase-matrix/auth-handshake-phase-dispatch`:`net_classic_session_test` 覆盖认证连接在 `PHASE_AUTH` 前收到 `GC_LOGIN_FAILURE` 时保持 Handshake owner、不触发登录失败回调,进入 `PHASE_AUTH` 后仍按正常 `CG_LOGIN3`/失败回调路径处理。 - `network.login.phase_flow/auth-game-phase-matrix/auth-success-result-authority`:`net_classic_session_test` 覆盖 `GC_AUTH_SUCCESS` 只按 `result/bResult` 判断成功;`result=1` 且 `login_key=0` 不触发 `BESAMEKEY`,认证 transport 仍按成功分支清理。 - `network.login.phase_flow/auth-game-phase-matrix/auth-phase-close-silent-owner`:`net_classic_session_test` 覆盖认证 transport 收到 `PHASE_CLOSE` 时保持 AUTH owner,不设置游戏侧 `phase_closed`、不伪造 `auth server closed phase` 错误;修复前专项回归失败 1 项,修复后通过。 - `network.login.phase_flow/auth-game-phase-matrix/phase-handshake-reentry-owner`:`net_classic_session_test` 覆盖游戏和认证 transport 收到 `PHASE_HANDSHAKE` 时重新进入 HandShake owner;从 GamePhase 重入时先清理旧 world 并通知宿主,后续等待新的 `PHASE_LOGIN/PHASE_AUTH`,不把旧阶段包继续按登录或游戏处理。 - `network.login.phase_flow/auth-game-phase-matrix/auth-peer-close-bridge-owner`:`project/auth_peer_close_bridge_test.gd` 覆盖真实本地 TCP 认证连接被主动关闭、LoginPhase owner 保持非终态、无 `login_failed`/`connect_failed`,以及第二次认证 transport 替换后再次 peer close。 - `network.login.phase_flow/auth-game-phase-matrix/peer-close-login-owner`:脱敏真实 Godot live smoke 使用 40250 服务端 `/logout`,已观察 `PHASE_GAME -> world_reset -> peer closed -> LoginPhase`;真实 AppFlow 报告 Login owner、GameScene 已释放、未保留 reconnect owner。 - `network.login.phase_flow/auth-game-phase-matrix/phase-close-server-fixture`:`app_flow_lifecycle_test.gd` 的修复前源级回归先证明旧 `M2Client::on_phase_close` 会错误发出 `disconnected`,修复后证明显式 `PHASE_CLOSE` 不再设置 `classic_remote_disconnect_pending`/发出传输断线信号;`net_classic_session_test` 和 Godot 生命周期回归继续证明旧 world 先清理、`LoginPhase` 接管。真实服务端主动 `PHASE_CLOSE` 包序仍未取证。 - `network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture`:`net_classic_e2e` 新增 `MT_CLASSIC_EXPECT_EMPTY=1`,收到 40250 的全空 `LOGIN_SUCCESS3/4 -> PHASE_SELECT` 时报告成功并停止,不发送过期 `CG_PLAYER_SELECT`;修复前 `app_flow_lifecycle_test.gd` 先失败,修复后通过。当前 live 账号只验证到“4 槽中 1 个占用、3 个为空”,全空账号的正向 live 结果仍缺。 - `network.login.phase_flow/login-failure-retry-cleanup/login-form-retry-owner`:`app_flow_lifecycle_test.gd` 的修复前测试先证明登录失败后点击连接仍走全量 `connect_to_server`;修复后由 `retry_login` 保留 LoginPhase/session owner,不触发重复 `world_reset`。`net_classic_session_test` 的 loopback fixture 进一步证明旧认证流被 `ClassicStream::connect` 清理,替换连接收到 `PHASE_AUTH` 后只发一份新 `CG_LOGIN3`。 - `network.login.phase_flow/login-failure-retry-cleanup/game-peer-close-retry-owner`:`net_classic_session_test` 的修复前断言先证明游戏态 peer close 没有保留 LoginWindow retry owner;修复后 loopback fixture 在同一 `ClassicSession` 上替换认证 transport,收到 `PHASE_AUTH` 后只发送一份使用新密码的 `CG_LOGIN3`,并保留旧 world 清理结果。 - `network.login.phase_flow/login-failure-retry-cleanup/loading-phase-silent`:`net_classic_session_test` 覆盖真实 live loopback 上进入 `PHASE_LOADING` 后收到未知 GC 头时,清理未读缓冲但保持 Loading phase/socket owner;独立 `net_classic_stream_test` 继续覆盖未设置 phase owner 时的严格 unknown-header transport failure。 - `network.login.phase_flow/login-failure-retry-cleanup/auth-unknown-header-owner`:`net_classic_session_test` 覆盖认证连接进入 `PHASE_AUTH` 后收到未知应用头时保持 AUTH owner 和认证 socket,不升级为 Failed;独立 `ClassicStream` 的默认未知头硬失败行为仍有回归。 - `network.login.phase_flow/login-failure-retry-cleanup/auth-connect-failure-owner`:`net_classic_session_test` 覆盖认证 TCP 非阻塞连接失败后回到 Offline owner,并在同一 session 上清理旧 stream、重新进入 `PHASE_AUTH`、只发送一次 `CG_LOGIN3`。 - `network.login.phase_flow/optional-security-branches/hybridcrypt-explicit-deployment-guard`:`net_classic_session_test` 覆盖 HybridCrypt 动态包被消费后发出显式部署能力事件、保留 LoginPhase owner,并让后续角色列表包继续按边界解析。 - `network.login.phase_flow/ticket-slot-warp-order/direct-enter-failure-login-owner-reset`:`net_classic_session_test` 覆盖 DirectEnter 连接失败后清除 DirectEnter 模式,并在替换 `PHASE_LOGIN` 时清理旧角色槽位 owner。 - `network.login.phase_flow/auth-game-phase-matrix/phase-close-login-retry-owner`:`net_classic_session_test` 覆盖显式 `PHASE_CLOSE` 后保留 LoginWindow retry owner、替换认证 transport 并在 `PHASE_AUTH` 只发送一份新的 `CG_LOGIN3`。 ## Implementation fix round 2026-09-21 — phase-close disconnect/login owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-close-disconnect-login-owner`。 本轮重新核对 40250 后确认,三个同名但语义不同的 `SetLoginPhase` 入口不能共用“发送登录包”的实现: - `RecvPhasePacket(PHASE_LOGIN) -> CPythonNetworkStream::SetLoginPhase -> SendLoginPacket/SendLoginPacketNew -> ClearLoginInfo`:这是新 LoginPhase 的原生登录包路径。 - `RecvPhasePacket(PHASE_CLOSE) -> ClosePhase -> networkmodule.SetLoginPhase -> net.Disconnect() -> LoginWindow`:这是关闭旧 transport 并交给登录窗口的路径,不在刚刚关闭的 socket 上发送 `CG_LOGIN`。 - `OnRemoteDisconnect -> networkmodule.SetLoginPhase -> net.Disconnect() -> LoginWindow`:同样只切换 owner,不在旧 socket 上伪造新的登录包。 - DirectEnter 的 `OnConnectFailure -> ClosePhase` 也属于第二类,不应把失败连接当作已进入新 LoginPhase。 修复前当前实现把 `on_remote_disconnect`、DirectEnter 连接失败、`PHASE_LOGIN` 和 `PHASE_CLOSE` 全部汇入 `ClassicSession::enter_login_phase(bool)`,无条件调用 `send_login()`。新增回归后,未修复代码明确失败: `FAIL: PHASE_CLOSE uses ClosePhase -> SetLoginPhase without sending CG_LOGIN on the closed stream`。 修复内容: - `ClassicSession::enter_login_phase` 增加显式的 `send_login_packet` 分支;只有 `PHASE_LOGIN` 走 `CG_LOGIN/CG_LOGIN2` 和发送后密码清理。 - `PHASE_CLOSE` 在 phase-leave/world 清理和 `on_phase_close` 通知后,以回调抑制状态断开旧 game stream,清空旧 receive/send 缓冲并保持 `LoggingIn` owner。 - peer close 和 DirectEnter connect failure 复用同一“不发送旧连接登录包”的 owner 语义;peer close 的密码断言同步改为保留到下一次 `LoginWindow.Connect`,与 40250 Python `SetLoginPhase` 不主动调用 `ClearLoginInfo` 一致。 修复后 `net_classic_session_test` 验证 `PHASE_CLOSE` 返回 LoginPhase、标记 `phase_closed`、旧 transport 为 Offline、无遗留 `CG_LOGIN`,并验证 peer-close 不伪造新的登录包。`ctest` 23/23、ASAN 专项和 Godot 生命周期/重连/网络回归通过;阶段分派、源码覆盖和 sequence 审计通过,packet registry 仍保留已批准的 `HDR_GC_SYMBOL_DATA` 适配项。 本轮只闭合本地 native 的 close/login owner 分支;真实 Windows LoginWindow 视觉时序、服务器主动 `PHASE_CLOSE` live 包序、完整跨阶段 retry 和 DirectEnter/GC_WARP 失败回退仍缺证据,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — game peer-close retry owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/game-peer-close-retry-owner`。 ### 40250 参考调用链 - 游戏连接建立后,`CPythonNetworkStream::OnRemoteDisconnect` 只调用 Python `networkmodule.SetLoginPhase`。 - `networkmodule.SetLoginPhase -> net.Disconnect() -> LoginWindow`:旧游戏 transport 被关闭, LoginWindow 成为下一次显式登录的 owner;该回调不会在已关闭的游戏 socket 上发送新的登录包。 - 用户再次点击登录时,`LoginWindow.Connect -> MainStream.Connect -> networkmodule.Connect`;在 `KEEP_ACCOUNT_CONNETION_ENABLE = 1` 下进入 `net.ConnectToAccountServer`,由 `CAccountConnector::Connect` 只清理/替换认证 transport。 - 新认证连接收到 `PHASE_AUTH` 后进入 `CAccountConnector::__AuthState_RecvPhase`,发送一份使用新输入 凭据的 `CG_LOGIN3`。原有主网络/session owner 不因游戏 peer close 被全局销毁。 ### 当前客户端调用链(修复前) - `ClassicSession::m_stream.on_remote_disconnect -> enter_login_phase(false, false)` 会清理旧 world 并回到 `LoggingIn`,但没有向上层暴露“可由 LoginWindow 重试的 session owner”。 - `AppFlow::_do_connect -> M2Client::retry_login` 检查不到 `login_retry_pending` 后,会回退到 `M2Client::connect_to_server`;该路径先执行全局 `disconnect_from_server()`,销毁旧 `ClassicSession`/world,再创建全新连接。 - 这与 40250 的 `LoginWindow -> AccountConnector` 认证 transport 替换边界不一致,且会改变旧 session 的 owner/清理顺序。 ### 修复前失败证据 在增加游戏态 peer-close 重试 owner 断言后、未修改生产代码运行: ```text build-debug/extension/net_classic_session_test FAIL: in-game peer close keeps the LoginWindow retry owner on the same session 1 check(s) failed ``` ### 修复内容 - `ClassicSession::m_stream.on_remote_disconnect` 在执行 40250 等价的 LoginPhase/world 清理后,设置 `m_login_retry_pending = true`。 - 现有 `M2Client::retry_login` 在 classic 模式下因此保留同一个 `ClassicSession`,更新新账号/密码, 由 `ClassicSession::retry_login -> ClassicSession::connect(auth)` 调用 `ClassicStream::connect` 替换认证 socket;不再触发全局 world teardown。 - 新增 loopback 回归:游戏 peer close 后在同一 session 上建立替换认证连接,收到 `PHASE_AUTH` 后 只发送一份 `CG_LOGIN3`,并断言其中使用重新输入的密码 `new-secret`。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;覆盖游戏态 peer close、旧 world 清理、retry owner 保留、认证 transport 替换和新密码 `CG_LOGIN3` 包序。 - 本地完整 native/CTest/Godot 回归和阶段/包结构审计继续按本轮结果记录在 `audit/history.jsonl`。 - 真实 Windows LoginWindow 的可视化时序、真实服务端“游戏断线后重新输入并成功登录”的 live 包序、 服务器主动 `PHASE_CLOSE` 和完整跨阶段 retry 仍未取证,因此合同保持 `PARTIAL`。 这些测试证明当前实现的 framing、握手、认证到游戏 happy path、角色选择/direct-enter,以及本轮本地 auth/game phase 矩阵部分行为可工作;它们没有证明 40250 的所有失败/可选分支、真实服务端包序或完整 UI retry 完全一致。 ## Deep audit round 2026-09-20 本轮重新执行登录阶段的 native/session 和 Godot 生命周期回归,并将阶段桥接的失败证据与参考调用链对应: - `net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test` 和 `app_flow_lifecycle_test.gd` 均退出码 0,说明 framing、握手/session、loopback happy path 和单生命周期 owner 没有回归。 - 初始回归发现 `p10_test.gd` 失败 3 项:`phase=game` 后 Loading 遮罩没有隐藏;无 SceneTree 构造选人界面时 `char_select_screen.gd:698` 对空 tree 调用 `create_timer`;因此选人和断线重连回放断言连带失败。 - 40250 `RecvPhasePacket` 的 `PHASE_CLOSE` 进入 `ClosePhase`,再按 phase leave 回到 Login/Offline 语义;当前已增加显式 `phase_closed`、Loading/Game phase-leave world 清理、`world_reset` 宿主通知和 M2Client owner 路由,但真实 UI 保留和失败后重试仍需验证。 - 当前已在 `CG_LOGIN/CG_LOGIN3` 成功写入后清除 `ClassicSession::m_pw`,并清除 M2Client 的 host retry password cache;无 login key 的 reconnect 现在必须重新显式登录,避免以空密码重试。`CG_LOGIN2` 本身不需要密码。 ## Implementation fix round 2026-09-21 按 40250 `RecvPhasePacket -> SetLoadingPhase/SetGamePhase` 的阶段属主修复 `LoadingScreen`:只有 `loading` 保持全屏遮罩,`login`、`select` 和 `game` 都立即释放遮罩与输入拦截;同时让选人确认在 tree-owned 界面离树时取消,在 headless/reconnect harness 尚未挂入 SceneTree 时直接发出一次 `select_requested`,避免空 tree 异常吞掉选人包。 回归:`godot --headless --path project --script p10_test.gd`、`godot --headless --path project --script app_flow_lifecycle_test.gd` 和 `git diff --check` 均通过。此次只闭合了阶段 UI owner 与测试/重建边界;`PHASE_CLOSE -> Login` 的完整 UI 重试、失败后 retry 清理、可选安全分支和真实服务端包序仍未达到 40250 统一语义,合同保持 `PARTIAL`。 ## Remaining - `network.login.phase_flow/auth-game-phase-matrix/live-package-order`:真实 auth/game 连接建立成功、四槽角色列表、选角、Loading 到 PHASE_GAME、错误密码回调、game connect failure 和一次建立连接后的 peer close 已取证;空角色、服务器主动 PHASE_CLOSE、完整服务器包序矩阵和跨阶段 retry 仍缺 live fixture;半包、重复 phase 和 PHASE_CLOSE 旧 world 的本地测试已覆盖。 - `network.login.phase_flow/login-failure-retry-cleanup`:登录阶段失败后的原 UI 属主、普通 Select/Game -> Login 的旧角色/旧 world 清理、认证/游戏 `PHASE_HANDSHAKE` 重入 owner、认证握手阶段忽略提前到达的 `GC_LOGIN_FAILURE`、认证成功按 `bResult` 而非 ticket 值判定、认证 `PHASE_CLOSE` 保持 AUTH owner 且静默、认证未知应用头保留 AUTH owner、认证 TCP 连接失败回到 Offline 并清理旧 stream、Loading/Game 未知头按 `RecvErrorPacket` 清理缓冲且保留 phase owner、游戏 peer-close 后 AppFlow 回 LoginPhase owner,以及同一 session 替换认证 transport 的 retry owner 已覆盖;仍需补充认证其它可选/静默分支、跨阶段失败后的完整 UI retry、真实重复进入包序和服务器主动 PHASE_CLOSE live 包序;不把参考未实际 enforced 的 phase timer 当作新增契约。 - `network.login.phase_flow/optional-security-branches`:Matrix、Passpod、Panama、HybridCrypt、HS/XTrap 等完整 UI/资源/安全 runtime 仍未实现;本轮已补齐静态与动态可选包的显式 deployment guard,保留 phase/socket owner,不静默断开。 - `network.login.phase_flow/ticket-slot-warp-order`:本地回归和实时探针已证明 login key、selected slot、DirectEnter、GC_WARP 的 replacement `CG_LOGIN2`/`CG_PLAYER_SELECT` 包序,以及新连接到达 `PHASE_GAME` 前不会继承旧的在线标记;仍缺失败后的真实 GC_WARP 回退包序。 - `network.login.phase_flow/real-server-sequence`:Ubuntu 40250 服务端已取证普通登录失败/成功、DirectEnter、一次成功 GC_WARP 到 `PHASE_GAME`,以及 `/logout` 后建立连接的 peer close;空角色、服务器主动 PHASE_CLOSE 和完整重试包序仍缺 fixture。 - `network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture`:本地 `LOGIN_SUCCESS4` 全空包、固定四槽解析、`PHASE_SELECT` owner 和 AppFlow 无自动选人已有回归;live 探针已支持 `MT_CLASSIC_EXPECT_EMPTY=1`,但当前可用账号不是全空账号,仍需无角色账号或等价真实服务端 fixture。 ## Implementation fix round 2026-09-21 — DirectEnter/GC_WARP connection owner 按 40250 `ConnectGameServer/RecvPhasePacket -> SetLoginPhase` 的连接生命周期补齐 `GC_WARP` 的阶段属主边界:复用原 `ClassicSession`、selected slot 和 login key,但新 `CNetworkStream::Connect()` 开始后立即清除旧连接的 `PHASE_GAME` 标记;只有替换连接再次收到 `PHASE_GAME`,才允许宿主把失败分类为已建立游戏连接断线。同步回归验证 DirectEnter 和 GC_WARP 都发送单个带 sequence 的 `CG_LOGIN2`,并保持同一 login key。 此前“DirectEnter 不发送 `CG_PLAYER_SELECT`”的判断已被重新定位的 40250 `introloading.py` 源码和实时探针证据否定;该结论不是本轮重复修复,而是原分支结论被新证据实质性推翻。 本轮新增回归先在未修复代码上失败(GC_WARP 继承旧 `GamePhase` 标记),修复后通过;GC_WARP 的真实包序仍未取证,因此合同继续保持 `PARTIAL`。 ## Status `PARTIAL`。本轮完成了登录/认证/选人/直连/Loading 的参考与当前完整调用链审计,协议回归通过,但可选分支、失败语义、超时、密码生命周期和真实服务端包序仍有材料差异,尚未按 40250 修改实现。 ## Implementation fix round 2026-09-21 — HybridCrypt deployment guard 问题/分支 ID:`network.login.phase_flow/optional-security-branches/hybridcrypt-explicit-deployment-guard`。 参考调用链: - `CAccountConnector::__AuthState_Process` / `CPythonNetworkStream::RecvDefaultPacket` - `__AnalyzeVarSizePacket(HEADER_GC_HYBRIDCRYPT_KEYS/SDB)` - `__AuthState_RecvHybridCryptKeys/SDB` - `CEterPackManager::RetrieveHybridCryptPackKeys/SDB` - 消费当前帧后返回,保留认证/游戏 phase owner,不把可选包当作 transport close 当前调用链(修复后): - `ClassicStream::dispatch` 识别动态 `HDR_GC_HYBRIDCRYPT_KEYS/SDB`,等待完整帧并消费固定头+payload - `ClassicSession::report_unsupported_security` 记录明确的部署能力缺口、登记未处理 header,并触发 `on_unsupported_security` - `M2Client` 转发为 `optional_security_required(header, reason)` 信号;phase/socket owner 保持不变,后续包仍从下一帧开始解析 修复前失败证据:新增 HybridCrypt 动态包 fixture 后,暂时移除可选包通知路径运行 `net_classic_session_test`,失败 2 项: - `FAIL: HybridCrypt packet raises an explicit deployment guard` - `FAIL: HybridCrypt deployment guard preserves the LoginPhase owner` 原因是旧 `ClassicStream` 只将 HybridCrypt 动态帧静默丢弃,既没有进入 40250 的 EterPack 密钥副作用链, 也没有向部署层报告当前客户端无法支持该服务器配置。 修复内容: - `extension/src/net/classic/classic_stream.h/.cpp`:增加可选安全帧通知,在保持 40250 consume-and-return 边界的同时显式暴露 HybridCrypt 缺口。 - `extension/src/net/classic/classic_session.h/.cpp`:统一静态 Matrix/Passpod/Panama/HS/XTrap 和动态 HybridCrypt 的 unsupported guard,保留当前 phase owner。 - `extension/src/net/classic/classic_parser.h`:允许 session 记录未处理 header,保持现有诊断队列。 - `extension/src/net/m2_client.cpp`:增加 `optional_security_required` 信号,供 Godot 部署层显示或阻断不兼容服务配置。 - `extension/tests/net_classic_session_test.cpp`:增加动态 HybridCrypt 分帧、显式通知、owner 保留和后续包对齐回归。 修复后验证:`net_classic_session_test` PASS。完整 HybridCrypt key/SDB 注入、Panama IV、Matrix/Passpod UI、HS/XTrap response 仍未实现, 因此本分支只关闭“不可见/误断线”的部署前置差异,合同继续保持 `PARTIAL`。 ## Audit round 2026-09-20 — login/phase failure matrix 本轮继续沿 40250 `AccountConnector::__AuthState_Process`、 `CPythonNetworkStream::SetLoginPhase`、`ConnectGameServer`、`SetSelectPhase`、 `SetLoadingPhase` 和 `PHASE_CLOSE` 分支核对当前实现,并重跑: - `build/extension/net_classic_session_test` - `build/extension/net_classic_stream_test` - `build/extension/net_loopback_test` - `project/app_flow_lifecycle_test.gd` - `project/p10_test.gd` 上述测试全部通过,证明当前握手、认证 happy path、半包 framing、DirectEnter 基础路径、Loading 遮罩和 AppFlow 生命周期没有回归;但它们没有覆盖真实账号服务端的失败/重试包序。 确认的结构差异: 1. `ClassicStream` 已有 3 秒 TCP connect deadline。重新核对 40250 后,`m_dwChangingPhaseTime` 只在 phase setter 中记录时间,没有发现实际比较/超时分支;因此不能把“缺少认证、登录、选人和 Loading phase deadline”登记为参考差异。认证/握手/Loading 静默失败矩阵仍需补测试。 2. 40250 `SetLoginPhase` 在发送 `CG_LOGIN/CG_LOGIN2/CG_LOGIN3` 后立即 `ClearLoginInfo()`;当前实现已在 `CG_LOGIN/CG_LOGIN3` 成功写入后清除 `ClassicSession::m_pw`,并同步清除 `M2Client::cfg_pw` 的 retry cache。 3. 40250 `PHASE_CLOSE` 通过 `ClosePhase`/phase leave 回到 Login 或 Offline 语义;当前实现已用 `phase_closed` 区分该路径,清理旧 stream,并由 `M2Client` 把登录侧 close 回到 Idle/Login owner,游戏侧 close 发 disconnected 以保留 login-key reconnect。真实 UI 重试和旧 stream 包级 fixture 仍缺。 4. 40250 认证阶段会消费并回调 Matrix、Runup Matrix、Passpod、OpenID、Panama Pack、HybridCrypt 等可选包;当前 parser/session 对这些包明确返回 unsupported/error。这不是静默解析,但如果部署启用了任一分支,行为不能与 40250 相同,必须补实现或在部署检查中明确阻断。 5. 当前 `M2Client::reconnect()` 会依赖 `cfg_pw` 重建整条认证链;虽然 `connect_warp()` 保留了 selected slot/login key,但真实 `GC_WARP`、认证失败、peer close、重复 phase 和重试时的旧 stream/阶段回调顺序仍没有包级 fixture。 结论:`network.login.phase_flow` 继续为 `PARTIAL`。密码发送边界和 `PHASE_CLOSE` owner 路由已修复;下一轮优先补 auth/game 失败、重复 phase、旧 stream 清理和真实服务端包序。可选安全包须选择真实实现或登记部署前置,不能宣称当前已和 40250 完全统一。 ## Implementation fix round 2026-09-21 — login secret and ClosePhase owner 按 40250 `CPythonNetworkStream::SetLoginPhase`、`AccountConnector::__AuthState_RecvPhase` 和 `PythonNetworkStream::ClosePhase` 修复登录阶段的两个已证实差异: - `ClassicSession::send_login` 与 `send_auth_login` 仅在固定包成功写入后清零并释放 `m_pw`;`M2Client::pump_classic` 同步清除 host 侧 `cfg_pw` retry cache,`reconnect()` 不再用空密码重建一条完整认证链。 - `PHASE_CLOSE` 设置显式 `phase_closed`,在回调抑制期间断开对应 auth/game stream,清空旧缓冲和连接 owner;M2Client 对登录侧 close 回到 Idle/Login owner,对已经在线的 game close 仍发出 `disconnected`,保留 login key 重连路径。 - 重新核对 40250 后没有新增 phase deadline:参考的 `m_dwChangingPhaseTime` 没有发现 enforced check,当前只保留已对齐的 TCP connect deadline,避免引入非参考行为。 回归通过: - `cmake --build build -j2 --target net_classic_session_test mtgodot` - `build/extension/net_classic_session_test` - `build/extension/net_classic_stream_test` - `build/extension/net_classic_wire_test` - `build/extension/net_loopback_test` - `godot --headless --path project --script app_flow_lifecycle_test.gd` - `godot --headless --path project --script p10_test.gd` - `godot --headless --path project --script netplay_test.gd` 仍为 `PARTIAL`:Matrix/Passpod/Hybrid 等可选认证分支未实现;auth/game 失败、重复 phase、旧 stream 乱序和真实 Ubuntu 40250 服务端包序仍缺 fixture;DirectEnter/GC_WARP 的真实服务端时序仍需验证。 ## Implementation fix round 2026-09-21 — auth-game-phase-matrix 问题 ID:`network.login.phase_flow/auth-game-phase-matrix`。 本轮按以下 40250 调用链修复并验证: - `CPythonNetworkStream::RecvPhasePacket -> ClosePhase -> SetLoginPhase` - `CPythonNetworkStream::LoginPhase -> __RecvLoginFailurePacket` - `CAccountConnector::__AuthState_RecvPhase` - `CAccountConnector::__AuthState_RecvAuthFailure` - `CAccountConnector::__AuthState_RecvAuthSuccess` - 当前 `ClassicStream::handle_control/dispatch -> ClassicSession::on_phase/on_auth_phase/on_packet/on_auth_packet` 修复内容: 1. `PHASE_CLOSE` 不再直接断开游戏 stream 并进入 `Failed`,而是保留 phase owner、切回 `LoggingIn` 并重新执行 LoginPhase;已进入游戏的 M2Client 阶段切换到 `GameLogin`。 2. `PHASE_LOGIN` 和 `PHASE_AUTH` 每次收到 phase 都重新执行 40250 对应 handler,因此重复 phase 不会被错误的一次发送保护吞掉。 3. 游戏侧 `GC_LOGIN_FAILURE` 改为触发登录失败回调并保留 `LoggingIn`,不再把合法的应用层拒绝变成传输失败。 4. auth 侧 `GC_LOGIN_FAILURE` 保留 AUTH owner;拒绝的 `GC_AUTH_SUCCESS` 报告 `BESAMEKEY`,并在当前 packet 消费后清理 auth stream。 5. 登录成功解析会清除旧的失败状态,空角色列表仍按固定槽位进入 `SelectPhase`。 修复前测试:新增矩阵断言先暴露 6 项失败,包括重复 `PHASE_LOGIN`、游戏 `GC_LOGIN_FAILURE`、`PHASE_CLOSE` 返回 LoginPhase 和重新发送 Login 包。 修复后测试:`net_classic_session_test` 通过,新增 auth/game 拒绝、半包、重复 phase、空角色和 `PHASE_CLOSE` 路径均通过;`net_classic_stream_test`、`net_loopback_test`、原生 CTest 23/23、`mtgodot` 构建、`app_flow_lifecycle_test.gd`、`p10_test.gd` 和 `git diff --check` 均通过。 仍未闭合:真实 Ubuntu 40250 服务端成功/拒绝/peer close/重复包序、旧 auth/game stream 竞态、失败后的完整 UI 重试、DirectEnter/GC_WARP live 包序和可选 Matrix/Passpod/Hybrid 分支。 ## Implementation fix round 2026-09-21 — peer-close-connect-failure-retry 本轮问题/分支 ID: - `network.login.phase_flow/login-failure-retry-cleanup` - 关联目标:`network.login.phase_flow/auth-game-phase-matrix` 参考调用链: - `CNetworkStream::Process -> OnRemoteDisconnect -> CPythonNetworkStream::OnRemoteDisconnect -> SetLoginPhase` - `CNetworkStream::Process -> OnConnectFailure -> CPythonNetworkStream::OnConnectFailure` - DirectEnter 失败:`OnConnectFailure -> ClosePhase -> SetLoginPhase` - `CPythonNetworkStream::SetLoginPhase -> phaseLeaveFunc -> __LeaveGamePhase -> ClearNetworkActorManager` 当前调用链(前一轮修复后、桥接修复前): - `ClassicStream::process -> on_remote_disconnect/on_connect_failure -> disconnect` - `ClassicSession` 回调中执行 `reset_for_map_change -> set_stage(LoggingIn) -> send_login`,再让旧 stream 清空缓冲 - `M2Client::pump_classic -> world_reset/disconnected`,并用 `classic_remote_disconnect_pending` 允许下一次 `reconnect()` 真正替换旧 socket 修复内容: 1. 建立连接后的 peer close 不再先进入 `Failed`;断线通知发生在 socket/buffer 清理之前,阶段 owner 回到 LoginPhase。 2. pre-game 和 in-game peer close 都清理旧角色/world 状态;in-game 路径保留 `disconnected` 重连入口。 3. DirectEnter 的连接失败不再永久停留在 `Failed`,按 40250 语义回到 LoginPhase;普通 game connect failure 也保留登录 owner并触发登录失败通知,失败 socket 的发送缓冲会在回调前清空。 4. `M2Client` 不再因 `GameLogin` 阶段保护而吞掉 peer close 后的第一次重连请求。 修复前失败证据: - 修改 peer-close 期望后,`net_classic_session_test` 失败 2 项:pre-game peer close 没有回 LoginPhase,密码没有按 LoginPhase 清理。 - 加入 DirectEnter 失败矩阵后,修复连接失败回调前再次失败 1 项:DirectEnter connect failure 仍停留在 Failed。 修复后证据: - `net_classic_session_test`:通过;覆盖 pre-game/in-game peer close、旧 world 清理、断线观察者顺序、DirectEnter connect failure、半包、重复 phase、空角色、auth/game 拒绝和 `PHASE_CLOSE`。 - `net_classic_stream_test`、`net_loopback_test`:通过。 - `ctest --test-dir build --output-on-failure -j1`:23/23 通过。 - `mtgodot` 构建、`app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`:通过。 - `audit_packet_registry.py`:`PARTIAL` 仅保留已登记的 `HDR_GC_SYMBOL_DATA` 平台/协议适配,无缺失 header、kind mismatch 或未批准动态包。 - `audit_phase_dispatch.py`:`PASS`,Login/Select/Loading/Game 的当前 payload 分派与 40250 对齐。 本地 peer-close/pre-game connect-failure 分支已具备等价调用链和自动测试证据;合同仍为 `PARTIAL`,因为真实 Ubuntu 40250 服务端包序、完整 UI retry、DirectEnter/GC_WARP live 顺序、旧 auth/game 竞态及可选安全分支尚未取证。 ## Implementation fix round 2026-09-21 — GB2312 wire text boundary 问题/规则 ID:`network.login.phase_flow/legacy-text-wire-encoding`。 参考依据:40250 `common/length.h::CHARACTER_NAME_MAX_LEN = 24`、 `TSimplePlayer::name[25]`、`TPacketGCLoginSuccess` 的固定槽位布局,以及 `LocaleService_GetCodePage` 的中文客户端行为。 修复内容: 1. 客户端内部继续使用 UTF-8;新增 `mtnet::to_wire`、`from_wire_str` 和 `wire_text_fits`,Windows 使用 CP936(GB2312 兼容子集),POSIX 使用 GB2312/GBK 转换。固定字段只按转换后的 wire 字节数截断或校验。 2. 角色名、公会名、NPC/玩家名、私聊目标、Messenger 名、商店签名、工会 字段和登录固定文本在发送前转 legacy 编码;接收端角色列表、实体、工会、 商店、聊天、私聊、任务和系统文本在进入世界模型前转回 UTF-8。 3. 明确 24 字节字段是 wire 容量:12 个常用 GB2312 汉字可完整放入;直接发送 UTF-8 会错误地把容量降到约 8 个汉字。地图名、任务名、系统提示等动态文本 使用自身协议容量,不套用角色名 24 字节限制。 4. 同步修正 `GCLoginSuccess` 的 40250 固定包布局:`SimplePlayer` 为 63 字节, 四槽 `GCLoginSuccess` 为 329 字节;此前使用 64 字节角色名会导致客户端在 收到真实 329 字节包后继续等待,表现为无法进入角色列表。 自动验证: - `net_text_codec_test`:12/13 个中文字符的 24-byte wire 边界、GB2312 往返、 非法/不完整尾部和 ASCII 回归均通过。 - `ctest --test-dir build --output-on-failure -j2`:23/23 通过。 - `app_flow_lifecycle_test.gd`、`netplay_test.gd`:通过。 - 真实 40250 服务端只读链路:认证、角色列表、选角、Loading、`PHASE_GAME` 均通过,实测收到四槽 `GC_LOGIN_SUCCESS4` 的 329 字节包并进入游戏。 本轮只闭合 legacy 文本编码和 40250 登录成功包布局;认证失败/重试的完整 UI 语义、DirectEnter/GC_WARP live 顺序、可选安全分支仍保持 `PARTIAL`,不得将本轮 证据解释为整个登录合同已完成。 ## Implementation fix round 2026-09-21 — legacy preflight boundary 问题/分支 ID:`network.login.phase_flow/legacy-text-wire-encoding/classic-preflight-callsite`。 本轮复核发现,底层 `ClassicSession` 已按 GB2312 wire 字节发送固定文本,但 `M2Client::open_private_shop()` 在进入协议专用发送函数之前仍直接用 UTF-8 字节数 校验商店签名。这会错误拒绝 16 个常用中文字符:UTF-8 为 48 字节,GB2312 wire 仅为 32 字节,正好可以放进 40250 的 32-byte 商店签名字段。 修复内容: 1. 新增 `mtnet::protocol_text_fits()`,统一根据 transport 选择 legacy-codepage wire 字节规则或原 m2dev 字节规则。 2. 商店签名入口改为按 40250 转换后的 wire 字节校验;角色名、聊天、私聊、公会 等双协议入口同步使用同一个 preflight 规则,发送端仍由 `ClassicSession` 在 固定字段写入前执行 UTF-8 → GB2312 转换。 3. 校验边界保持分层:角色/公会/NPC 等定长网络字段受各自字段容量限制;地图名、 任务名和系统提示仍使用自身动态包容量,不套用 24-byte 角色名限制。 修复前证据:调用点静态回归显示 `open_private_shop` 仍使用 `sign_wire.size()`,因而没有走 GB2312 wire 容量规则。 修复后验证:`net_text_codec_test` 验证 16 个中文在 32-byte classic sign 字段可用、 17 个中文被拒绝;`mtgodot`、`net_classic_session_test`、原生 CTest 23/23、 `app_flow_lifecycle_test.gd`、`netplay_test.gd`、`audit_ledger.py validate` 和 `git diff --check` 均通过。 本轮只修正一个已定位的 preflight 漏洞;可选安全分支、完整 UI retry、真实 DirectEnter/GC_WARP 包序和真实服务端失败矩阵仍保持 `PARTIAL`。 ## Live package-order probe round 2026-09-21 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/live-package-order`。 本轮没有重复修改已经闭合的本地 phase 矩阵,而是修正实时验证工具的观测边界: 1. `net_classic_e2e` 现在监听 `ClassicSession::on_login_failure`。这与 40250 `AccountConnector::__AuthState_RecvAuthFailure` 和 `PythonNetworkStream::__RecvLoginFailurePacket` 一致:`WRONGPWD`、 `BESAMEKEY` 属于应用层拒绝,回调登录 UI,但不进入终态 `Failed`。 2. `net_classic_e2e` 监听已经建立的 game 连接的 remote-disconnect,避免 `OnRemoteDisconnect -> SetLoginPhase` 的回退重试包遮盖原始断线原因。 3. 真实 Ubuntu 40250 服务端验证结果: - 有效账号:auth -> 四槽 `GC_LOGIN_SUCCESS4` -> 选角 -> Loading -> `PHASE_GAME`,退出码 0; - 错误密码:收到 `WRONGPWD`,立即报告 `FAIL login callback`,退出码 5, 不再误报为 timeout; - auth 成功但目标 game 端口不可用:立即报告 `FAIL login callback: connect failed`, 退出码 5。 本轮确认了真实成功包序和两种失败回调的可观测性,没有把失败回调错误改成 终态阶段。真实服务端的空角色账号、`PHASE_CLOSE`、peer close、DirectEnter/ `GC_WARP` 以及完整重试包序仍缺 fixture,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — auth peer-close owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/auth-peer-close-owner`。 参考调用链: - `CNetworkStream::Process -> CAccountConnector::OnRemoteDisconnect` - `CAccountConnector::OnRemoteDisconnect -> __OfflineState_Set` - `__OfflineState_Set -> __Inialize (STATE_OFFLINE)` 当前调用链: - `ClassicStream::process -> on_remote_disconnect -> disconnect` - `ClassicSession::m_auth_stream.on_remote_disconnect -> set_stage(Offline)` - `M2Client::pump_classic -> login_failed -> host Stage::Failed`,虽然保留显式登录重试入口, 但把认证 peer close 误当成凭据失败 修复前失败证据:新增 loopback auth peer-close 测试后,当前实现报告: `FAIL: auth peer close returns to the 40250 Offline owner`,共 1 项失败;原因是 原实现把认证端 TCP 断开直接转换成 `ClassicSession::Failed`。 修复内容: 1. 认证端 peer close 清除 auth-success/待清理标记并返回 `ClassicSession::Offline`, 不把传输断开伪装成应用层 `WRONGPWD/BESAMEKEY`。 2. `M2Client` 观察到认证 session Offline 时向 Godot 登录 owner 发出 `login_failed`,并把 host 阶段置为可重试的 `Failed`,避免 UI 停在 `GameLogin`;这一层 仍与 40250 `OnRemoteDisconnect` 的静默 Offline owner 不等价,后续由独立 bridge 分支修正。 3. 游戏端 peer close 的 `LoggingIn` 回退路径不改变,仍按 40250 `OnRemoteDisconnect -> SetLoginPhase` 清理旧 world 并等待重连。 修复后证据: - `net_classic_session_test`:通过,新增认证端 peer-close -> Offline owner 断言; - `mtgodot` 构建:通过,M2Client Offline-to-login-failure bridge 编译通过; - 原有 auth/game 拒绝、空角色、半包、重复 phase、`PHASE_CLOSE` 和游戏 peer-close 回归仍需与本轮完整 CTest 一起复跑。 本分支已闭合认证端 peer-close owner 差异,但登录失败后的完整 UI 重试、真实 `PHASE_CLOSE`/peer-close 包序和 DirectEnter/`GC_WARP` live 顺序仍未闭合,合同继续 保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — auth peer-close bridge owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-peer-close-bridge-owner`。 ### 40250 参考调用链 ```text CNetworkStream::Process -> CAccountConnector::OnRemoteDisconnect -> CAccountConnector::__OfflineState_Set -> __Inialize(STATE_OFFLINE) ``` 认证 socket 被对端关闭时,40250 不调用 `OnLoginFailure`,也不调用认证连接失败的 `OnConnectFailure`;它只把 `AccountConnector` 恢复到 Offline owner,等待 LoginWindow 的 下一次显式连接请求。凭据错误和 TCP peer close 必须保持不同的可观测分支。 ### 当前客户端调用链(修复前) ```text ClassicStream::process -> m_auth_stream.on_remote_disconnect -> ClassicSession::set_stage(Offline) -> M2Client::pump_classic -> login_failed -> M2Client Stage::Failed ``` 这会让真实认证 peer close 变成凭据失败,破坏 `LoginPhase` owner 语义;同时它与已经 修复的认证同步/异步 `OnConnectFailure -> connect_failed` 分支混淆。 ### 修复前失败证据 新增真实本地 TCP fixture:[project/auth_peer_close_bridge_test.gd],由测试服务器接受 认证连接后主动关闭;修复前输出: ```text FAIL: auth peer close is not reported as credential login failure FAIL: auth peer close retains a non-terminal LoginPhase owner 2 check(s) failed ``` ### 修改内容 - `ClassicSession` 增加独立 `on_auth_remote_disconnect` callback;认证 stream 回到 native `Offline` 后只通知该 transport-owner 事件,不复用游戏 `on_remote_disconnect`、 `on_login_failure` 或 `on_auth_connect_failure`。 - `M2Client` 增加 `classic_auth_remote_disconnect_pending`,认证 peer close 后保持公共 `AuthConnect`/LoginPhase owner,不发出 `login_failed` 或 `connect_failed`,并在显式 `retry_login`、新连接和 disconnect 时清理 pending 标记。 - `retry_login` 继续复用原 `ClassicSession`,只替换认证 transport;专项 fixture 随后再次 接受并关闭第二条认证连接,证明旧连接清理和重复 peer-close 不会进入终态 `Failed`。 - 游戏 peer close、认证 `OnConnectFailure` 和凭据 `GC_LOGIN_FAILURE` 仍使用各自 callback, 没有改变已经关闭的游戏 owner 或认证同步失败 owner。 ### 修复后验证与边界 - `project/auth_peer_close_bridge_test.gd`:PASS;初次及重试认证 peer close 都不产生 `login_failed`/`connect_failed`,并保持非终态 LoginPhase owner。 - 真实 40250 Windows/服务端主动 `PHASE_CLOSE`、完整 retry 包序、空角色 live、可选安全 runtime 和 Windows 登录窗口视觉表现仍未取证,因此合同继续保持 `PARTIAL`;后续不得 重复修改本分支的 auth remote-disconnect callback 或 pending owner。 ## Implementation fix round 2026-09-21 — login failure UI owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/ui-login-owner`。 参考调用链: - `CPythonNetworkStream::__RecvLoginFailurePacket -> intrologin.OnLoginFailure` - `intrologin.OnLoginFailure -> connectingDialog.Close -> PopupNotifyMessage` - `PopupNotifyMessage` 的默认回调 `SetPasswordEditLineFocus`,LoginPhase 窗口本身不重建 当前调用链(修复后): - `ClassicParser::on_gc -> ClassicSession::on_packet -> on_login_failure` - `M2Client::pump_classic -> login_failed` - `AppFlow::_on_login_failed -> _focus_login_password -> _set_status` 修复前失败证据:在 `app_flow_lifecycle_test.gd` 增加 LoginPhase 重试断言后,当前实现失败: - `login failure keeps the existing LoginPhase UI for retry` - `login failure returns focus to the password field` 失败原因是 `_on_login_failed` 无条件调用 `_goto_login()`,释放并重建整个登录层;这与 40250 只关闭 connecting dialog、保留登录 phase owner 的行为不同,也会使原登录控件上的 重试状态和回调失效。 修复内容: 1. LOGIN 阶段收到 `login_failed` 时不再重建 `_ui`,保留同一个登录 `CanvasLayer` 和输入控件; SELECT 或其它阶段收到登录失败时仍切回 Login,以保证没有登录 owner 时能恢复入口。 2. 为密码框设置稳定节点名 `Password`,统一由 `_focus_login_password()` 调用 `LineEdit.grab_focus()`,并在原 Status 面板显示错误文本。 3. 该修复只改变 UI owner 和重试入口,不改变 `ClassicSession` 保留 Login phase、密码发送后 清理和 transport cleanup 语义。 修复后验证: - 修复前 `app_flow_lifecycle_test.gd`:2 项断言失败; - 修复后 `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:PASS; - `godot --headless --path project --script p10_test.gd --quit-after 10`:PASS; - `godot --headless --path project --script netplay_test.gd --quit-after 10`:PASS; - `ctest --test-dir build --output-on-failure -j2`:23/23 PASS; - `audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配; - `audit_phase_dispatch.py`:PASS; - `git diff --check`:PASS。 headless 测试验证了原 UI、密码字段和状态面板保持不变;真实窗口焦点由代码调用 `grab_focus()`,仍需 Windows/可视化运行证据。认证/握手/Loading 静默、跨阶段 retry、真实 服务器失败包序、PHASE_CLOSE/peer close live 包序、DirectEnter/GC_WARP 和可选安全分支仍 保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — phase-close world owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-close-world-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE) -> ClosePhase -> SetLoginPhase` - `SetLoginPhase -> m_phaseLeaveFunc.Run()` - Game owner:`__LeaveGamePhase -> __ClearNetworkActorManager -> character/item Destroy` - Loading owner:`__LeaveLoadingPhase -> CPythonPlayer::Clear -> CFlyingManager/CEffectManager 清理` - 清理完成后才发送 `CG_LOGIN/CG_LOGIN2`。 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_phase(PHASE_CLOSE)` - `ClassicSession` 在旧 owner 为 `Loading`/`InGame` 时执行 `reset_for_map_change -> on_phase_close` - `M2Client::bind_classic_loading_reset::on_phase_close -> world_reset` - `ClassicSession::set_stage(LoggingIn) -> send_login` 修复前失败证据:新增游戏阶段 Close fixture 后,当前实现失败 1 项: `FAIL: game PHASE_CLOSE clears the old world before login retry`。 原因是原 `on_phase(PHASE_CLOSE)` 直接切到 `LoggingIn` 并发送登录包,没有执行 40250 `SetLoginPhase` 触发的旧 Game/Loading phase-leave 清理,native world 中的旧 actor 会继续存在;同时宿主 GameScene 没有收到对应的 `world_reset`。 修复内容: 1. 新增 `ClassicSession::on_phase_close` 宿主回调。 2. `PHASE_CLOSE` 仅在旧 owner 为 Loading/Game 时先执行 `reset_for_map_change()`,再通知宿主, 保证清理发生在 LoginPhase 重入和登录包发送之前;Login/Select owner 保持 40250 的空 leave 语义,不额外清理角色列表。 3. `M2Client` 将该回调映射到 `world_reset`,让保留的 Godot GameScene 与 native world 同步清空。 修复后验证: - 修复前 `net_classic_session_test`:1 项失败; - `cmake --build build -j2 --target net_classic_session_test mtgodot`:PASS; - `build/extension/net_classic_session_test`:PASS; - `ctest --test-dir build --output-on-failure -j2`:23/23 PASS; - `audit_phase_dispatch.py`:PASS; - `audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配; - `git diff --check`:PASS。 本分支已补齐本地 PHASE_CLOSE 的 phase-leave/world owner 语义;真实服务端的 PHASE_CLOSE、 peer close、旧新 socket 竞态和完整 UI retry 包序仍没有 fixture,因此合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — cross-phase login leave owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/cross-phase-login-leave-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_LOGIN) -> SetLoginPhase` - `SetLoginPhase`:旧 phase 不是 Login 时先执行 `m_phaseLeaveFunc.Run()` - Game owner:`__LeaveGamePhase -> __ClearNetworkActorManager -> character/item Destroy` - 普通 Login owner:发送 `CG_LOGIN/CG_LOGIN2` 后执行 `__ClearSelectCharacterData` - `CNetworkStream::OnRemoteDisconnect -> SetLoginPhase` 复用同一 phase-leave 入口 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_phase(PHASE_LOGIN)` - `ClassicSession::enter_login_phase(false)`:旧 Game owner 先 `reset_for_map_change -> on_phase_leave`,再发送登录包;普通非 DirectEnter 分支随后 `ClassicParser::clear_select_character_data` - 显式 `PHASE_CLOSE` 复用 `enter_login_phase(true)`,保留既有 `on_phase_close` 观察者 - `M2Client::bind_classic_loading_reset::on_phase_leave -> world_reset`,并在 `on_stage_change(LoggingIn)` 清除 `classic_list_emitted`,保证同一 pump 内 Login→Select 的新角色列表仍会发给宿主 修复前失败证据:新增跨阶段重试 fixture 后,`net_classic_session_test` 失败 2 项: - `PHASE_LOGIN clears the previous character-list owner before retry` - `PHASE_LOGIN runs the previous GamePhase leave cleanup before retry` 修复内容: 1. 新增统一的 `ClassicSession::enter_login_phase()`,让直接 `PHASE_LOGIN`、显式 `PHASE_CLOSE` 和 game peer close 共用 40250 的 LoginPhase owner 转换。 2. 新增 `ClassicParser::clear_select_character_data()`,按 40250 普通 Login 分支清除旧 LOGIN_SUCCESS 角色快照和角色事件,但保留固定四槽形状;DirectEnter 保留 slot/ticket 数据。 3. 新增通用 `on_phase_leave` 宿主回调,M2Client 在 native world 已清空后发出 `world_reset`; peer-close observer 只报告断线,不再重复清理/重复发出 `world_reset`;同时在 LoginPhase owner 重入时清除旧列表发射锁,避免新旧角色列表被去重逻辑吞掉。 修复后验证: - 修复前 `net_classic_session_test`:2 项失败; - `cmake --build build -j2 --target net_classic_session_test mtgodot`:PASS; - `build/extension/net_classic_session_test`:PASS; - `build/extension/net_classic_stream_test`、`build/extension/net_loopback_test`:PASS; - `ctest --test-dir build --output-on-failure -j2`:23/23 PASS; - `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、 `disconnect_world_reset_test.gd`:PASS; - `audit_phase_dispatch.py`:PASS; - `audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配; - `git diff --check`:PASS。 本分支闭合了本地直接 LoginPhase 重入的角色列表/world owner 清理;真实服务端的跨阶段 失败重试、旧新 socket 竞态、完整 UI retry 和 live 包序仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — Loading phase silent packet owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/loading-phase-silent`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket -> LoadingPhase` - `LoadingPhase` 未识别的应用包回退到 `GamePhase` - `RecvErrorPacket` -> `ClearRecvBuffer`,不关闭当前 socket,也不转移 Loading/Game phase owner 当前调用链(修复后): - `ClassicStream::dispatch` 解析到未知静态 GC header - `ClassicSession::on_unknown_header` 仅在 active phase owner 存在时接管;记录诊断信息,清空当前未读 burst,保持 `ClassicStream` 的连接状态和 `ClassicSession::Stage::Loading/Game` - 未绑定 phase owner 的独立 `ClassicStream` 仍走原有严格 framing failure/断开路径,避免把通用传输层错误语义改成静默吞包 修复前失败证据:新增 live loopback fixture 后,`net_classic_session_test` 失败 1 项: `FAIL: unknown Loading packet follows RecvErrorPacket without phase loss` 原因是当前 `ClassicStream::dispatch` 对未知 GC header 无条件设置 framing error 并断开, 没有体现 40250 active Loading/Game phase 的 `RecvErrorPacket` 语义。该修复按 phase owner 统一处理未知头,不针对单个 header 做特判。 修复内容: - `extension/src/net/classic/classic_stream.h/.cpp`:增加可选 `on_unknown_header` phase-owner hook;hook 接受时清空 receive burst 并保留连接,未设置或拒绝时保留严格默认行为。 - `extension/src/net/classic/classic_session.cpp/.h`:为 active session 安装统一 unknown-header owner,并增加析构前解绑所有 stream 回调,避免 stream 最终 `Offline` 通知写入已析构的 session 字段。 - `extension/tests/net_classic_session_test.cpp`:加入真实 loopback socket 上的 Loading 未知包回归。 修复后验证: - ASAN `net_classic_session_test`:PASS,无 heap-use-after-free; - `net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`:PASS; - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS; - Godot `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、`disconnect_world_reset_test.gd`:PASS/exit 0; - `audit_phase_dispatch.py`:PASS;`audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配; - 真实服务器 Loading 静默未知包、跨阶段 retry UI 和完整失败包序仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — Authentication connect-failure owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/auth-connect-failure-owner`。 参考调用链: - `CAccountConnector::Process -> CNetworkStream::Process` - 非阻塞连接超时/错误 -> `CAccountConnector::OnConnectFailure` - `OnConnectFailure` 通知登录 owner 后调用 `__OfflineState_Set`,不会把认证连接失败留在终态协议失败 owner 当前调用链(修复后): - `ClassicStream::process` 检测 auth socket 的 connect error/timeout - `ClassicSession::m_auth_stream.on_connect_failure` 清理认证状态并设置 `Stage::Offline` - `M2Client::pump_classic` 将 Offline owner 桥接为一次 `login_failed`,随后允许新的显式登录替换旧 stream 修复前失败证据:新增关闭 loopback 端口的非阻塞认证连接 fixture 后, `net_classic_session_test` 失败 1 项: `FAIL: auth connect failure returns to the 40250 Offline owner` 原因是认证 stream 没有 `on_connect_failure` owner 回调;连接失败后仅触发通用 Offline state callback, 其条件把 active auth owner 设置为 `Failed`,与 40250 `OnConnectFailure -> __OfflineState_Set` 不一致。 修复内容: - `extension/src/net/classic/classic_session.cpp`:增加 auth stream connect-failure 回调,清理 auth/game transition 标志、记录连接错误并回到 Offline owner。 - `extension/tests/net_classic_session_test.cpp`:加入死端口连接失败、旧 stream 清理、同 session 重试和重新发送单个 `CG_LOGIN3` 的回归。 修复后验证: - 修复前 `net_classic_session_test`:1 项失败; - 修复后 `build-debug/extension/net_classic_session_test`:PASS; - 认证 peer close、认证未知头、登录失败、重复 `PHASE_AUTH` 和 retry 包序继续 PASS; - 真实服务器连接失败 UI 包序和 Windows 可视化 retry 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — Authentication unknown packet owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/auth-unknown-header-owner`。 参考调用链: - `CAccountConnector::Process -> __StateProcess -> __AuthState_Process` - `__AuthState_Process` 逐项 `__AnalyzePacket/__AnalyzeVarSizePacket`;未匹配的应用头不调用 `OnConnectFailure`,认证 owner 仍由 `CAccountConnector` 持有 - 已建立 socket 的真正关闭才经 `OnRemoteDisconnect -> __OfflineState_Set`,连接建立失败才经 `OnConnectFailure` 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_unknown_header` - 同一 active phase-owner 回调同时绑定到 game stream 和 auth stream;未知应用头记录诊断、清空当前未读 burst,但不调用 `disconnect()`,保留 `Stage::LoggingIn`/socket owner - 只有 `ClassicStream` 未绑定 phase owner 或 owner 明确拒绝时,才保持独立传输层的严格 unknown-header failure 修复前失败证据:新增真实 loopback 认证 fixture 后,`net_classic_session_test` 失败 1 项: `FAIL: unknown auth packet keeps AUTH owner and socket alive` 当前认证 stream 没有 phase-owner unknown-header hook,未知头直接进入 `ClassicStream::dispatch` 的硬失败分支, 触发 Offline state callback 并把会话变成 `Failed`;这与 40250 的 `__AuthState_Process` 未匹配包不触发连接失败不一致。 修复内容: - `extension/src/net/classic/classic_session.cpp`:抽取统一 active phase-owner unknown-header 回调,并同时安装到游戏/认证 stream;不是针对 header 200 的单包特判。 - `extension/tests/net_classic_session_test.cpp`:加入 `PHASE_AUTH -> unknown application header` 的 live loopback 回归。 修复后验证: - 修复前 `net_classic_session_test`:1 项失败; - 修复后 `build-debug/extension/net_classic_session_test`:PASS; - 认证 peer close、登录失败、重复 `PHASE_AUTH` 和独立 stream 严格未知头路径继续 PASS; - 真实服务器可选认证包、跨阶段完整 UI retry、重复进入包序仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — auth handshake phase dispatch 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-handshake-phase-dispatch`。 参考调用链: - `CAccountConnector::Process -> __StateProcess` - `__HandshakeState_Process`:仅检查 `PHASE`、`HANDSHAKE`、`PING` 和已启用的加密控制包 - `PHASE_AUTH -> __AuthState_Set` - `__AuthState_Process`:此后才处理 `GC_LOGIN_FAILURE`、`GC_AUTH_SUCCESS` 和 `PHASE_AUTH` 下的登录包 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_auth_packet` - `ClassicSession::on_auth_packet` 先检查当前 owner 是否已进入 `Stage::LoggingIn` - 未收到 `PHASE_AUTH` 时保持 `Stage::Connecting`,不触发 `on_login_failure`;收到 `PHASE_AUTH` 后继续执行 `CG_LOGIN3`、认证失败和 login-key 分支 修复前失败证据:新增认证握手阶段提前收到 `GC_LOGIN_FAILURE` 的 fixture 后, `net_classic_session_test` 失败 1 项: `FAIL: auth handshake ignores GC_LOGIN_FAILURE before PHASE_AUTH`。 原因是当前 `on_auth_packet()` 原先只按 header 分派,不检查认证 phase,导致握手 owner 尚未切换到 `PHASE_AUTH` 时也调用登录失败回调;这与 40250 的两张状态分派表不一致。 修复内容:在认证包入口增加基于认证 owner 的统一阶段前置条件。该条件覆盖所有认证应用包, 不是针对 `GC_LOGIN_FAILURE` 的单包特判;握手阶段的延迟/乱序应用包不会污染登录 UI,正常进入 `PHASE_AUTH` 后原有认证包处理保持不变。 修复后验证: - 修复前 `net_classic_session_test`:1 项失败; - 修复后 `build-debug/extension/net_classic_session_test`:PASS; - 认证矩阵中的正常 `PHASE_AUTH -> CG_LOGIN3`、重复 `PHASE_AUTH`、错误登录回调:PASS; - 该分支只修改认证 phase dispatch,未修改飞行物、技能、战斗或 UI; - 认证/Loading 其它静默分支、真实服务器失败包序和完整 UI retry 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — phase-close reconnect owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/phase-close-reconnect-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE) -> ClosePhase` - `ClosePhase -> SetLoginPhase` - 旧 GamePhase 的 leave owner 完成 world 清理后,LoginPhase 接管;角色列表到达后由登录属主继续选人/重进 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_phase(PHASE_CLOSE)` - `ClassicSession::enter_login_phase(true)` 先执行 `on_phase_leave -> world_reset`,再执行 `on_phase_close` - `M2Client::bind_classic_loading_reset::on_phase_close` 在旧宿主仍为 `InGame` 时发出一次 `disconnected`,并保留 `classic_remote_disconnect_pending` 供 login-key 重连 - `M2Client::pump_classic -> phase_changed("login") -> AppFlow::_on_phase_changed` 作为阶段-only 适配兜底;`AppFlow`/`ReconnectUI` 保留 GameScene,角色列表到达后复用 remembered slot, `entered_game` 后关闭遮罩 修复前失败证据:`app_flow_lifecycle_test.gd` 新增 PHASE_CLOSE 等价阶段事件后,修复前失败 3 项: - `game-side LoginPhase transition enters reconnecting GAME` - `game-side LoginPhase transition shows reconnect overlay` - `game-side LoginPhase transition re-enters remembered character slot` 原因是当前端虽已把 native session 切到 `LoggingIn` 并发出 `phase_changed("login")`,但 AppFlow 没有消费游戏态的 LoginPhase;后续 `char_list` 直接执行 `_goto_select()`,销毁了仍应保留的 GameScene。 同时 `on_phase_close` 在 M2Client 中只是空观察者,没有给现有重连属主发送断线边界。 修复内容: 1. M2Client 在旧宿主为 `InGame` 的显式 PHASE_CLOSE 中,在 world leave/reset 完成后发出一次 `disconnected`,避免重复进入和重复发起重连。 2. AppFlow 监听 `phase_changed`;若游戏态收到 `login/close` 且尚未处于重连,则转为同一个 reconnect owner;ReconnectUI 增加统一的 `show_for_disconnect` 入口,确保阶段-only 事件与 transport disconnect 使用同一倒计时/重试属主。 3. 仅对游戏态处理该兜底;登录/选人/Loading 的 LoginPhase 仍按原有登录属主处理,不扩大为普通 transport failure。 修复后验证: - `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:PASS; - `net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`:PASS; - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS; - ASAN `net_classic_session_test`:PASS; - `p10_test.gd`、`disconnect_world_reset_test.gd`:PASS; - `audit_phase_dispatch.py`:PASS;`audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配;`git diff --check`:PASS。 本分支闭合了本地 PHASE_CLOSE 到游戏重连属主的阶段/UI 路由,合同仍为 `PARTIAL`:真实服务端 PHASE_CLOSE/peer-close 包序、完整跨阶段 UI retry、空角色和 DirectEnter/GC_WARP live 证据仍未完成。 ## Implementation fix round 2026-09-21 — phase-close loading owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-close-loading-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE) -> ClosePhase -> SetLoginPhase` - `SetLoginPhase` 在旧 phase 不是 Login 时先执行 `m_phaseLeaveFunc.Run()` - Loading owner:`__LeaveLoadingPhase` 清理旧的 player、actor/effect 和加载期 world 状态, 然后才进入 LoginPhase 并发送新的 `CG_LOGIN/CG_LOGIN2` 当前调用链(修复后): - `ClassicStream::dispatch -> ClassicSession::on_phase(PHASE_CLOSE)` - `ClassicSession::enter_login_phase(true)` 对旧 `Stage::Loading` 和 `Stage::InGame` 使用同一个 `reset_for_map_change -> on_phase_leave -> set_stage(LoggingIn) -> send_login` 顺序 - `M2Client::bind_classic_loading_reset::on_phase_leave` 在 native world 清空后通知 `world_reset`; Loading 阶段不会被误报为游戏态 `disconnected` 修复前失败证据:新增 LoadingPhase 持有旧主角后收到 `PHASE_CLOSE` 的 fixture, `net_classic_session_test` 失败 2 项: - `Loading PHASE_CLOSE clears the old world before login retry` - `Loading PHASE_CLOSE notifies the host after the old world is cleared` 原因是 `ClassicSession::enter_login_phase()` 原先只在旧 stage 为 `InGame` 时运行 phase-leave 清理; 旧 Loading actor 会留在 EntityStore,宿主也收不到与 40250 leave owner 对应的清理通知。 修复内容:将 phase-leave 条件从仅 `InGame` 扩展为 `Loading || InGame`,保持唯一的清理根机制, 不对某一个 packet 或 actor id 做特判。显式 close、peer close 和普通 `PHASE_LOGIN` 重入继续共用 同一 LoginPhase owner 转换。 修复后验证: - 修复前 `net_classic_session_test`:2 项失败; - 修复后 `build-debug/extension/net_classic_session_test`:PASS; - Loading/Game 的 phase-close world、主角替换、半包、重复 phase 和旧 world 清理回归继续通过; - 合同仍为 `PARTIAL`,因为真实服务端 Loading close/peer-close 包序、完整 UI retry 和 live DirectEnter/GC_WARP 仍未取证。 ## Implementation fix round 2026-09-21 — warp connect failure retry owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/warp-connect-failure-retry-owner`。 参考调用链: - `CPythonNetworkStream::RecvWarpPacket -> Connect(lAddr, wPort)` - 新连接失败 -> `CPythonNetworkStream::OnConnectFailure` - `OnConnectFailure -> SetLoginPhase`,保留登录阶段 owner;游戏侧由上层保留当前游戏场景并等待下一次重连 当前调用链(修复后): - `M2Client::pump_classic -> EntityStore::take_next_warp -> ClassicSession::connect_warp` - `M2Client::warp_to_game_server -> ClassicSession::connect_warp`(login-key 快速重连) - `ClassicSession::connect_warp` 失败后清理旧 stream、回到 `LoggingIn`;M2Client 两个失败入口都设置 `classic_remote_disconnect_pending` 并只发出一次 `disconnected`,不再把已回到 LoginPhase 的替换连接标记为终态 `Failed` 或发送错误的登录失败 owner - 下一次 `M2Client::reconnect()` 绕过 `GameLogin` 的幂等早返,复用同一 selected slot/login key 和 `ClassicSession`,重新替换 game transport;GC_WARP 失败前的 world、shop、mall、safebox 和 dead 状态 也在连接尝试前清理 修复前失败证据:扩展 `project/app_flow_lifecycle_test.gd` 的 native source invariant 后,修复前测试失败 2 项: - `fast reconnect connect failure keeps the native reconnect owner armed` - `GC_WARP connect failure keeps the native reconnect owner armed` 原因是两个 `connect_warp()` 失败入口都缺少宿主重连属主标记。尤其 GC_WARP 失败后,session 已经是 `LoggingIn`,宿主却可能落在 `GameLogin`;下一次 `reconnect()` 会命中 `GameLogin` 的幂等早返,实际不再 建立替换连接。旧实现还把快速重连失败错误地发为 `login_failed` 并设置宿主 `Failed`。 修复内容: - `extension/src/net/m2_client.cpp`:统一两个替换连接失败入口的 `disconnected`/retry-owner 语义;GC_WARP 在调用 `connect_warp()` 前清理阶段/UI latch,避免失败后残留旧地图状态。 - `extension/src/net/m2_client.h`:更新 `classic_remote_disconnect_pending` 的注释,明确覆盖 peer close 和 replacement-connect failure。 - `project/app_flow_lifecycle_test.gd`:加入修复前会失败的两个 native source invariant,同时保留现有 游戏态 LoginPhase/重连属主行为测试。 修复后验证: - 修复前 `app_flow_lifecycle_test.gd`:2 项失败; - `cmake --build build-debug --target net_classic_session_test net_classic_stream_test net_loopback_test mtgodot -j4`:PASS; - `build-debug/extension/net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`:PASS; - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS; - ASAN `net_classic_session_test`:PASS; - Godot `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、`disconnect_world_reset_test.gd`:PASS/exit 0; - `audit_phase_dispatch.py`:PASS;`audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配; - `git diff --check`:PASS。 本分支闭合了本地同步 GC_WARP/快速重连失败后的宿主 retry-owner 差异;真实服务端 GC_WARP 失败包序、 跨阶段完整 UI retry、peer-close/PHASE_CLOSE live 顺序和 DirectEnter/GC_WARP live 证据仍未取证,合同继续 保持 `PARTIAL`。 ## Live evidence refresh 2026-09-21 — configured 40250 channel 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/live-package-order`。 本轮没有重复修改已经完成的本地阶段矩阵,只补充当前部署端口的只读真实包序证据。使用 `build-debug/extension/net_classic_e2e` 连接当前 40250 配置的游戏通道,结果为: - 正常账号:`CG_LOGIN3` 认证成功,收到固定四槽角色列表,选中第 0 槽后依次进入 `PHASE_LOADING -> PHASE_GAME`;探针观察到 `main_vid=18062`、21 个实体和 21 个背包槽位。 - 错误密码:服务端即时回调 `WRONGPWD`,探针在连接超时前结束,退出码为预期的失败码 5。 - 游戏连接失败:认证成功后连接不可用的游戏端口,探针即时收到 `connect failed`,没有等待到超时, 退出码为预期的失败码 5。 这组证据确认当前配置下成功登录、角色列表、选角、Loading、PHASE_GAME、应用层登录拒绝和游戏连接失败 的观察边界均有效;它没有覆盖空角色账号、服务器主动 `PHASE_CLOSE`、建立连接后的 peer close、 DirectEnter/GC_WARP 或完整重试包序,因此不从 `remaining` 中移除这些分支,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — empty reconnect select owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/empty-reconnect-select-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_SELECT)` - `CPythonNetworkStream::SetSelectPhase` - `networkModule.SetSelectCharacterPhase` - `SelectCharacterWindow.Refresh`:固定四槽逐项读取;没有有效角色时仍由选人窗口持有阶段 当前调用链(修复后): - `M2Client/ClassicSession` 收到替换连接的 `PHASE_SELECT` 和角色列表 - `AppFlow::_on_char_list` 验证记忆槽位仍存在;只有精确匹配的角色才调用 `_enter_character` - 空列表或记忆槽已不存在时,先解除 `_reconnecting_game`、关闭重连遮罩、解绑旧 `GameScene` 的 生命周期引用并释放旧场景,再调用 `_goto_select` 让选人界面成为唯一 owner 修复前失败证据:扩展 `project/app_flow_lifecycle_test.gd` 后,修复前测试失败: - `empty reconnect character list returns to the SELECT owner` 修复前 `AppFlow::_on_char_list` 在重连分支对空角色列表直接返回;由于 `_selected_char_slot` 仍是非负值, 即使列表为空也可能向 native bridge 发送一个过期槽位,或者保留旧 GameScene/重连遮罩,无法回到可操作的 选人 owner。这同时违反了 40250 `PHASE_SELECT` 的阶段归属和角色槽位有效性前置条件。 修复内容: - `project/app_flow.gd`:重连时只接受角色列表中仍存在的记忆槽位;空列表或槽位失效时进入 SELECT, 不发送 phantom select,不保留旧游戏场景。 - `project/app_flow_lifecycle_test.gd`:加入修复前会失败的空角色重连、旧场景释放和无 phantom select 回归。 修复后验证: - 修复前 `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:1 项失败; - 修复后同一测试:PASS; - 相关真实服务器“空角色账号”仍未取得,因此不能把本地 owner 修复等同于 live 空角色证据。 本分支的本地实现差异已闭合;合同仍为 `PARTIAL`,因为真实空角色包序、PHASE_CLOSE/peer-close、 DirectEnter/GC_WARP 和完整跨阶段 UI retry 仍在 `remaining` 中。 ## Implementation fix round 2026-09-21 — pre-game phase Login owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/pre-game-phase-login-ui-owner`。 参考调用链: - `CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE)` - `CPythonNetworkStream::ClosePhase -> SetLoginPhase` - `networkModule.SetPhaseWindow(introLogin.LoginWindow)` - `introLogin.LoginWindow.OnConnectFailure/OnLoginFailure` 继续持有登录重试 owner 当前调用链(修复后): - `ClassicSession::on_phase` 将 `PHASE_CLOSE` 和后续 `PHASE_LOGIN` 通过 `M2Client` 的 `phase_changed("login")` 观察事件桥接到宿主 - `AppFlow::_on_phase_changed` 在旧 owner 为 `SELECT`(包括 Loading 遮罩覆盖的选人状态)时调用 `_goto_login`,替换旧选人 CanvasLayer;在 `GAME` 且已有场景时仍保留原有 disconnected/reconnect owner - `LoadingScreen` 在 LoginPhase 事件上隐藏遮罩,登录界面成为唯一可交互 owner 修复前失败证据:扩展 `project/app_flow_lifecycle_test.gd` 后,修复前测试失败: - `pre-game LoginPhase transition replaces the SELECT owner` 修复前 `AppFlow::_on_phase_changed` 只处理 `_state == GAME` 且存在 `_game` 的情况;来自 Loading/Select 的 LoginPhase 事件会直接返回,旧选人 UI 在读取遮罩隐藏后继续可见。这与 40250 `SetLoginPhase` 无条件 替换当前 phase window 的行为不一致,也会让后续登录失败回调找不到正确的 LoginPhase owner。 修复内容: - `project/app_flow.gd`:统一处理 Login/Close phase-only 事件;SELECT/Loading owner 切换到 LOGIN, GAME owner 继续走已有的保留场景重连路径,缺失游戏场景时安全回 LOGIN。 - `project/app_flow_lifecycle_test.gd`:加入修复前失败的 pre-game phase-only Login owner 回归, 并保留空角色重连和游戏态 PHASE_CLOSE 测试。 修复后验证: - 修复前 `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:1 项失败; - 修复后同一测试:PASS; - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS; - ASAN `net_classic_session_test`:PASS; - P10、Netplay、断线 world 清理测试:PASS/exit 0。 本分支的本地阶段 owner 差异已闭合;真实服务端 PHASE_CLOSE/peer-close UI 包序、完整跨阶段 retry、 DirectEnter/GC_WARP live 包序和可选安全 runtime 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation/live evidence refresh 2026-09-21 — DirectEnter Loading select packet 问题/分支 ID:`network.login.phase_flow/ticket-slot-warp-order/direct-enter-loading-select-packet`。 本分支不是重复处理 `direct-enter-login2-packet`。重新核对 40250 源码后发现,旧分支关于 “DirectEnter 不发送 `CG_PLAYER_SELECT`”的结论不成立,因此按技能规则以新证据建立独立分支。 参考调用链: - `CPythonNetworkStream::ConnectGameServer` 保存 selected slot,设置 DirectEnter,连接角色端点。 - `CPythonNetworkStreamPhaseSelect::SetSelectPhase` 在 DirectEnter 下进入 `SetLoadingPhase`。 - `Eternexus/root/introloading.py::LoadingWindow.Open` 读取 `GetCharacterSlot()`,调用 `net.SendSelectCharacterPacket(chrSlot)`,发送带 slot 的 `CG_PLAYER_SELECT`。 - `PythonNetworkStreamPhaseLoading.cpp::SendEnterGame` 后续发送 `CG_ENTERGAME`,再进入 `PHASE_GAME`。 修复前当前调用链: - `ClassicSession::on_phase(PHASE_SELECT)` 的 DirectEnter 分支仅设置 `Loading`,没有发送 `CG_PLAYER_SELECT`。 - 因而服务端等待角色选择包,实时 DirectEnter 探针停留在 `loading` 并超时;新增的 native fixture 也在修复前失败:`DirectEnter SetLoadingPhase sends the reference CG_PLAYER_SELECT`。 修复后的当前调用链: - `ClassicSession::on_phase(PHASE_SELECT)` 先调用 `send_select_char(m_direct_enter_slot)`,再设置 `Loading` owner;失败时进入 `Failed` 并记录发送错误。 - Loading timer 随后保持既有的 `send_enter_game()` 顺序,避免用连接地址中的 slot 替代协议包。 修改文件: - `extension/src/net/classic/classic_session.cpp` - `extension/tests/net_classic_session_test.cpp` 修复后验证: - 修复前 `net_classic_session_test`:1 项失败,缺少 DirectEnter 的 `CG_PLAYER_SELECT`。 - 修复后 `build-debug/extension/net_classic_session_test`:PASS;检查 header、长度和 selected slot。 - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS。 - ASAN `net_classic_session_test`:PASS。 - `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、`disconnect_world_reset_test.gd`:PASS/exit 0。 - `audit_phase_dispatch.py`:PASS;`audit_packet_registry.py`:PARTIAL,仅保留已登记的 `HDR_GC_SYMBOL_DATA` 适配。 - 脱敏的真实 DirectEnter 探针:认证成功、四槽角色列表、发送 selected slot 后进入 `PHASE_LOADING -> PHASE_GAME`,观察到 21 个实体、21 个背包槽位和等级 105 的主角;退出码 0。 本分支闭合了 DirectEnter 本地 `CG_PLAYER_SELECT` 和实时进入游戏的差异。合同仍为 `PARTIAL`: 空角色账号、服务器 `PHASE_CLOSE`/peer close、完整跨阶段 UI retry、 可选安全 runtime 和 40250 的完整在线失败矩阵仍未闭合。 ## Live evidence refresh 2026-09-21 — GC_WARP replacement transport 问题/分支 ID:`network.login.phase_flow/ticket-slot-warp-order/gc-warp-live-package-order`。 本轮先重新核对 40250 服务端和客户端的完整链路: - 40250 `CHARACTER::WarpSet -> TPacketGCWarp -> CPythonNetworkStream::RecvWarpPacket`。 - `RecvWarpPacket` 读取 `lAddr/wPort`,保留 `m_dwSelectedCharacterIndex`,设置 DirectEnter,立即调用 `CNetworkStream::Connect` 替换旧 game transport。 - 新 socket 重新经过 `PHASE_HANDSHAKE -> PHASE_LOGIN -> CG_LOGIN2`;DirectEnter 的 `SetSelectPhase -> introloading.LoadingWindow.Open` 再发送 selected slot 的 `CG_PLAYER_SELECT`, 最后由 LoadingPhase 发送 `CG_ENTERGAME`。 当前端链路: - `ClassicParser::on_gc(HDR_GC_WARP)` 将固定包放入 `EntityStore::WarpCue`,并由 `ClassicSession::on_packet` 清理当前帧后的旧 socket tail。 - `M2Client::pump_classic -> take_next_warp` 先执行 `reset_for_map_change/world_reset`,再调用 `ClassicSession::connect_warp`;该方法复用 selected slot/login key、清除旧 `PHASE_GAME` 标记, 新连接按 `CG_LOGIN2 -> CG_PLAYER_SELECT -> CG_ENTERGAME` 重新进入游戏。 实时验证: - 使用当前配置 40250 服务端、脱敏账号和可逆 GM 命令 `/warp 9593 2696` 触发真实 `GC_WARP`。 - Godot 客户端观察到 `world_reset`、旧主角清理、`stage=game_connect`、第二次握手/登录/Loading, 随后第二次 `entered_game`;重建主角、属性、技能和快捷栏均通过。 - `net_classic_e2e` 的 `MT_CLASSIC_WARP_COMMAND` 入口也通过同一服务器回归:第一次进入游戏后发送 命令,消费 `GC_WARP`,重连到 `192.168.21.203:13002`,第二次 `PHASE_GAME` 成功。 本轮没有发现生产代码与 40250 的 GC_WARP 成功链路差异,因此没有制造一个虚假的“修复前失败”或修改 连接逻辑;新增的是可重复的 live/e2e 验证入口。该分支的成功包序已取证,但真实连接失败、服务器主动 `PHASE_CLOSE`、peer close 和空角色仍在合同 `remaining` 中,合同保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — peer-close LoginPhase owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/peer-close-login-owner`。 ### 40250 参考语义 - `EterLib/NetStream.cpp` 在已建立连接的读写失败时先调用 `CPythonNetworkStream::OnRemoteDisconnect`,再清理旧 socket/buffer。 - `PythonNetworkStreamEvent.cpp::OnRemoteDisconnect` 无条件调用 Python `SetLoginPhase()`。 - `networkmodule.py::SetLoginPhase` 先 `net.Disconnect()`,再通过 `SetPhaseWindow(introLogin.LoginWindow(...))` 关闭旧 phase window、打开 LoginWindow。 - 因此 peer close 和 `RecvPhasePacket(PHASE_CLOSE) -> ClosePhase` 都以 LoginPhase 为唯一 UI 属主;旧 GameWindow/地图对象不在登录窗口后面保留,角色列表也不会自动重入旧 slot。 ### 修复前差异和失败证据 当前 `AppFlow::_on_disconnected` 在 GAME 且存在 GameScene 时设置 `_reconnecting_game`、显示 `ReconnectUI` 并等待角色列表自动重入;`_on_phase_changed` 对 GAME 的 LoginPhase 事件也走同一 重连分支。这与 40250 无条件 SetLoginPhase 不一致。 先把 `project/app_flow_lifecycle_test.gd` 的断言改为 40250 行为,再未修改生产代码运行: ```text godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10 PRE_FIX_RC=1 FAIL: game peer close returns to LoginPhase and releases GameScene FAIL: game peer close does not retain a reconnect overlay FAIL: post-disconnect character list enters SELECT without phantom re-entry FAIL: game-side PHASE_CLOSE transition returns to LoginPhase owner FAIL: empty character list returns to the SELECT owner ``` ### 修复内容 - `project/app_flow.gd` 的断线处理统一释放 GameScene、清除重连状态并进入 LoginPhase。 - AppFlow 不再把 `ReconnectUI` 挂接到 `disconnected`,避免 LoginPhase 已接管后又被第二个重连属主覆盖。 - `PHASE_CLOSE`/phase-only `login` 事件与 peer close 使用同一 LoginPhase owner;非游戏阶段仍可从 SELECT/Loading 回到 Login,游戏阶段不再保留旧场景。 - 角色列表按普通 Login/Select 流程处理,不再由断线分支自动发送旧角色 slot;空列表保持 SELECT owner。 - native `M2Client` 的 login key 和显式 `reconnect()` 能力保留,但不再被 AppFlow 的自动重连遮罩消费。 ### 修复后验证 - `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:PASS。 - `godot --headless --path project --script p10_test.gd --quit-after 10`:PASS。 - 重新构建 `mtgodot` 和 release macOS 包:PASS。 - 真实 40250 Godot live smoke,`MT_SMOKE_PHASE_COMMAND=/logout`: `PHASE_GAME -> world_reset -> stage=game_login -> peer closed -> phase=login`,报告验证 LoginPhase owner、GameScene 已释放、未保留 reconnect owner:PASS。 - live smoke 外层脚本的功能报告为 PASS,但退出日志仍触发现有的 4 个 `ParticlesShaderRD`/Material RID leak gate;该渲染资源泄漏与本分支的网络/phase owner 行为无关, 因此不能把外层 shell 的最终结果记为全量 PASS。 本分支已闭合当前客户端的 peer-close/游戏态 phase-only LoginPhase owner 差异;合同仍为 `PARTIAL`,因为真实服务端主动 `PHASE_CLOSE` 包序、空角色 fixture、完整跨阶段 retry、重复进入 live 包序和可选安全 runtime 尚未取证。 ## Implementation fix round 2026-09-21 — LoginPhase retry transport owner 问题/分支 ID:`network.login.phase_flow/login-failure-retry-cleanup/login-form-retry-owner`。 ### 40250 参考调用链 - `intrologin.LoginWindow.__OnClickLoginButton -> LoginWindow.Connect(id, pwd)`。 - `MainStream.SetLoginInfo -> MainStream.Connect`。 - `networkmodule.Connect -> net.ConnectToAccountServer`。 - `CAccountConnector::Connect` 先执行 `__OfflineState_Set`,再由 `CNetworkStream::Connect` 清理并替换 AccountConnector 自己的 socket;它不会走 `CPythonNetworkStream::OnRemoteDisconnect`,也不会执行 Game/World 的全局清理。 - 新认证连接收到 `PHASE_AUTH` 后重新发送一份 `CG_LOGIN3`;认证成功再由 `CPythonNetworkStream::Connect` 清理/替换游戏连接。 ### 修复前失败证据 先在 `project/app_flow_lifecycle_test.gd` 增加登录失败后再次点击连接的断言,再未修改生产代码运行: ```text godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10 FAIL: login form retry replaces only the authentication transport PRE_FIX_RC=1 ``` 失败原因是 `_do_connect` 只有 `connect_to_server` 入口,而 `M2Client::connect_to_server` 开头无条件调用 `disconnect_from_server()`,会销毁整个 `ClassicSession` 并发出 `world_reset`;这不是 40250 的 LoginWindow/AccountConnector 重试边界。 ### 修改内容 - `ClassicSession` 增加 `login_retry_pending` 和 `retry_login`。认证应用失败、认证连接失败、认证 peer close、游戏登录失败都会标记一个可显式重试的 LoginPhase owner。 - `M2Client::retry_login` 在存在待重试 classic session 时更新新输入的账号/密码,保留现有回调和 session,只调用 `ClassicSession::retry_login` 替换认证流;不调用全局 `disconnect_from_server()`,不重复发出 `world_reset`。 - `AppFlow::_do_connect` 优先调用 `retry_login`;无待重试 owner 时由 M2Client 回退到初始完整连接。 - 首次登录、非 LoginPhase 的手动连接和已有 GC_WARP/peer-close 的其他重连路径保持原有入口,不 把本修复扩散到飞行物、技能、战斗或表现系统。 ### 修复后验证与边界 - `app_flow_lifecycle_test.gd`:修复后 PASS,确认 LoginPhase 重试调用 `retry_login`,不走 fake client 的完整 `connect_to_server`。 - `net_classic_session_test`:loopback fixture PASS,确认失败后旧认证 socket 被替换,新的 `PHASE_AUTH` 只产生一份 `CG_LOGIN3`,并使用重新输入的密码。 - 真实 Windows LoginWindow 可视化重试和真实服务端“失败->重新输入->成功”的完整包序仍未取证; 因此本分支证明的是当前 native/UI retry owner 和 socket-free/loopback 等价行为,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — explicit PHASE_CLOSE transport-owner separation 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-close-server-fixture`。 ### 40250 参考语义 - `PythonNetworkStream::RecvPhasePacket` 收到 `PHASE_CLOSE` 后调用 `ClosePhase()`。 - `ClosePhase()` 通过 `SetLoginPhase()` 回到 LoginPhase;它与 `PythonNetworkStreamEvent::OnRemoteDisconnect()` 是两条不同的来源链。 - 因此显式服务器阶段关闭只改变 phase/UI owner,不能被桥接成 peer close,也不能触发远程断线重试属主。 ### 修复前失败证据 在不改生产代码的情况下,先把 `project/app_flow_lifecycle_test.gd` 增加到 40250 语义:检查 `M2Client::on_phase_close` 的 callback body 不得设置 `classic_remote_disconnect_pending`,也不得发出 `disconnected`。修复前结果: ```text godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10 FAIL: explicit PHASE_CLOSE does not synthesize transport disconnected PRE_FIX_RC=1 ``` ### 修复内容 - `extension/src/net/m2_client.cpp::bind_classic_loading_reset` 的 `on_phase_close` 只保留 语义注释,不再设置 `classic_remote_disconnect_pending`,不再发出 `disconnected`。 - `pump_classic` 在显式 `phase_closed()` 后即使重新进入 LoginPhase 的发送失败,也统一发出 `login_failed`;不再用 `was_online_lost` 把阶段关闭误报为传输断线。 - LoginPhase 的 owner 仍由 `ClassicSession` 阶段变化和 AppFlow 的 `phase_changed("login")` 接管; Loading/Game 的旧 world 清理由已有 phase-leave 回调完成。 ### 修复后验证 - `godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10`:PASS。 - `cmake --build build-debug --target net_classic_session_test net_classic_stream_test net_loopback_test mtgodot -j4`:PASS。 - `net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`:PASS。 - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS。 - `p10_test.gd`、`netplay_test.gd`、`disconnect_world_reset_test.gd`:PASS/exit 0。 本分支闭合了当前 native bridge 将显式 `PHASE_CLOSE` 错报为 transport disconnect 的结构差异, 但没有伪造真实服务器证据;真实主动 `PHASE_CLOSE` 包序、空角色 live fixture、完整跨阶段 retry 和重复进入 live 包序仍使合同保持 `PARTIAL`。 ## Implementation hardening round 2026-09-21 — empty-role live probe 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture`。 ### 40250 参考语义 - `PythonNetworkStreamPhaseLogin::__RecvLoginSuccessPacket3/4` 将固定槽位数组复制到客户端, 空槽保持零初始化;该包不是登录失败。 - 随后的 `RecvPhasePacket(PHASE_SELECT) -> SetSelectPhase` 只切换到 Select owner;非 DirectEnter 不会因为所有槽位为空而发送 `CG_PLAYER_SELECT`,选人窗口继续负责建号/选槽。 ### 当前实现与缺口 - `ClassicParser::parse_login_success` 同样固定产生四槽数组,`ClassicSession::on_phase(PHASE_SELECT)` 进入 `CharSelect` 并回调空列表,`AppFlow::_on_char_list` 只进入 SELECT,不自动选择槽位;本地行为已由 `net_classic_session_test` 和 `app_flow_lifecycle_test.gd` 覆盖。 - 原 `extension/tools/net_classic_e2e.cpp` 在没有可选角色时直接返回“account has no selectable character”退出码 3,无法区分合法全空账号和登录失败,阻塞了真实空角色包序审计。 ### 修复前失败证据 先在 `project/app_flow_lifecycle_test.gd` 加入 live-probe 合同断言,再未修改探针运行: ```text godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10 FAIL: live probe treats an all-empty character list as a valid SelectPhase result PRE_FIX_RC=1 ``` ### 修改内容 - `net_classic_e2e` 增加 `MT_CLASSIC_EXPECT_EMPTY=1` 模式。 - 该模式要求所有固定槽位为空,观察到 `CharSelect` 后输出 `PASS EMPTY CHARACTER LIST PHASE_SELECT` 并立即断开;若发现占用槽位则返回失败。 - 非该模式仍保持原有选择第一个可用角色的 live 流程,避免改变常规成功探针。 ### 验证与边界 - 修复后 `app_flow_lifecycle_test.gd`:PASS。 - `cmake --build build-debug --target net_classic_e2e mtgodot -j4`:PASS。 - 当前 40250 live 账号在新模式下正确拒绝为“全空”(槽 0 有角色、槽 1–3 为空),退出码 3; 这证明模式不会误报部分空列表为全空,但不是全空账号的正向证据。 - 本地完整网络回归继续覆盖空 `LOGIN_SUCCESS4`、半包、重复 phase 和 SELECT owner;全空角色 live 仍需安全的无角色账号或等价真实服务器 fixture,合同保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — stale auth phase after auth-success 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order`。 ### 40250 参考调用链 ```text CAccountConnector::Process -> __AuthState_Process -> __AuthState_RecvAuthSuccess -> CAccountConnector::Disconnect -> CNetworkStream::Clear -> 清空旧 AccountConnector 接收缓冲 ``` 认证成功处理在 `AccountConnector.cpp:451-476` 内立即断开认证连接;因此同一次 TCP 读取中跟在 `GC_AUTH_SUCCESS` 后面的旧 `PHASE_AUTH` 不会再次进入 `__AuthState_RecvPhase`,也不会发送第二份 `CG_LOGIN3`。认证失败的 `__AuthState_RecvAuthFailure` 则不主动断开,允许 LoginWindow 在原 AUTH owner 上等待用户重试; 这两个分支不能合并成“所有失败包都清缓冲”。 ### 当前客户端调用链(修复前) ```text ClassicStream::feed/process -> ClassicStream::dispatch -> ClassicSession::on_auth_packet(GC_AUTH_SUCCESS) -> 仅设置 m_auth_succeeded / m_auth_close_after_packet -> dispatch 继续消费同一接收缓冲 -> ClassicSession::on_auth_phase(PHASE_AUTH) -> send_auth_login -> 第二份 CG_LOGIN3 ``` 这与 40250 的旧认证连接边界不等价;`pump()` 后续才断开认证流,已经晚于同一缓冲区中 迟到 phase 的处理。 ### 修复前失败证据 在生产修改前加入 loopback fixture,向同一个认证流一次性送入 `GC_AUTH_SUCCESS + PHASE_AUTH`: ```text build-debug/extension/net_classic_session_test FAIL: auth-success clears a pipelined stale PHASE_AUTH without a duplicate CG_LOGIN3 1 check(s) failed ``` ### 修改内容 - `ClassicSession::on_auth_packet` 在接受或拒绝的 `GC_AUTH_SUCCESS` 当前帧处理完成后,调用 `m_auth_stream.clear_remaining_after_current()`;`ClassicStream` 先消费当前完整帧,再清空其余旧 AccountConnector 接收缓冲。 - 认证应用失败 `GC_LOGIN_FAILURE` 不调用该清理,继续保留 40250 的 LoginWindow/AUTH owner, 让后续显式 `retry_login` 替换认证 transport。 - 没有改变 `PHASE_AUTH` 正常包、认证失败包或游戏流的通用 phase 分支;本轮只修复认证成功后的 旧连接尾包边界,不涉及飞行物、技能、战斗或 UI。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;认证成功后同一 burst 中的迟到 `PHASE_AUTH` 被丢弃,认证流 不产生第二份 `CG_LOGIN3`。 - 认证失败后的原 AUTH owner、重复 `PHASE_AUTH`、半包、peer close、登录表单 retry 和游戏阶段 phase 清理回归继续 PASS。 - 真实 Windows/真实服务端认证成功后管线化尾包无法直接注入,完整跨阶段 UI retry、游戏登录失败 后的迟到 game phase、真实重复进入包序和服务器主动 `PHASE_CLOSE` live 证据仍未闭合;合同继续 保持 `PARTIAL`,下一步应处理独立的 game-login stale-tail 分支,避免重复修改本轮认证 owner。 ## Implementation fix round 2026-09-21 — game LoginPhase stale tail 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order-game-login-stale-tail`。 ### 40250 参考调用链 ```text CPythonNetworkStream::OnProcess -> m_phaseProcessFunc.Run() -> CPythonNetworkStream::LoginPhase -> CheckPacket -> __RecvLoginFailurePacket -> LoginWindow.OnLoginFailure -> return ``` `LoginPhase()` 每次处理一个当前应用包;`GC_LOGIN_FAILURE` 的回调完成后立即返回, 因此同一 TCP 读取中管线化的 `PHASE_LOGIN` 不会在本次 tick 继续执行,而是留给下一次 `OnProcess`。`SelectPhase()` 和 Loading 阶段自己的包也使用同样的 phase-local return 边界;Loading 转交 `GamePhase()` 的游戏包不套用该单包限制。 ### 当前客户端调用链(修复前) ```text ClassicStream::feed/process -> ClassicStream::dispatch -> ClassicSession::on_packet(GC_LOGIN_FAILURE) -> dispatch 继续循环同一 receive buffer -> ClassicSession::on_phase(PHASE_LOGIN) -> 立即再次发送 CG_LOGIN ``` 这会把 40250 的两个 `OnProcess` 周期压缩成一个周期,造成登录失败回调和新的登录阶段 包顺序不一致;同样的通用分发问题还会让 Select/Loading 自有包越过 phase-local return。 ### 修复前失败证据 在生产修改前加入 `net_classic_session_test` loopback/socket-free fixture,向游戏流一次性 送入 `GC_LOGIN_FAILURE + PHASE_LOGIN`: ```text build-debug/extension/net_classic_session_test FAIL: LoginPhase stops after login failure before a pipelined PHASE_LOGIN FAIL: deferred PHASE_LOGIN is processed on the next tick 2 check(s) failed ``` ### 修改内容 - `ClassicStream` 增加 `return_after_current_packet()`,应用包当前帧消费完成后返回,保留未读 receive tail 到下一次 `process/feed`;disconnect、错误清理和 `clear_remaining_after_current` 路径会同步清除该一次性边界标记。 - `ClassicSession::on_packet` 在 LoginPhase、SelectPhase、DirectEnter 的 Select owner 和 Loading 自有包成功处理后设置该边界;Loading 转交 GamePhase 的包继续由游戏批处理路径处理。 - 测试补充登录失败后迟到 `PHASE_LOGIN` 的“本次不重发、下次才重试”断言,并将已有选择阶段 的填充字节/第二包测试按 40250 的下一 tick 包序执行。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;登录失败 burst 不再立即产生第二份 `CG_LOGIN`,下一次 feed 才执行保留的 `PHASE_LOGIN`。 - 该轮只修复 phase-local 应用包边界,不改变 GamePhase 的三包/安全缓冲批处理,也不涉及 飞行物、技能、战斗或 UI。 - 真实 Windows/真实服务端的失败后重新输入成功、重复进入包序、服务器主动 `PHASE_CLOSE` 和全量 live retry 仍未闭合,合同保持 `PARTIAL`;后续不得重复修改本分支的单包返回机制。 ## Implementation fix round 2026-09-21 — synchronous auth connect failure owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-connect-immediate-failure-owner`。 ### 40250 参考调用链 ```text CAccountConnector::Connect -> CNetworkStream::Connect -> connect/socket immediate error -> CNetworkStream::Clear -> CAccountConnector::OnConnectFailure -> LoginWindow.OnConnectFailure -> CAccountConnector::__OfflineState_Set ``` `CNetworkStream::Connect` 在 socket 创建或同步 `connect()` 错误时会主动调用 `OnConnectFailure` 后返回 `false`;`CAccountConnector::OnConnectFailure` 仍把认证 owner 置为 Offline。该状态允许登录窗口保留并再次调用 `Connect`,不能把地址解析/同步连接失败 当作协议失败进入终态 Failed。 ### 当前客户端调用链(修复前) ```text ClassicSession::connect(auth_host, auth_port, ...) -> ClassicStream::connect -> getaddrinfo/socket immediate error -> return false -> ClassicSession::connect 直接 set_stage(Failed) ``` 当前 `ClassicStream::connect` 的异步失败已经通过 `on_connect_failure` 回调处理,但同步 失败直接返回,未进入同一个认证 owner 清理路径;因此同步与异步错误的阶段结果不一致。 ### 修复前失败证据 在生产修改前加入无效认证地址测试: ```text build-debug/extension/net_classic_session_test FAIL: synchronous auth connect failure returns to the 40250 Offline owner FAIL: synchronous auth connect failure keeps the LoginPhase retry owner 2 check(s) failed ``` ### 修改内容 - `ClassicSession` 增加 `handle_auth_connect_failure()`,统一清理认证成功标记、游戏启动标记、 关闭待处理标记、phase-close/loading/enter-game 状态,并设置 `m_login_retry_pending` 后 返回 `Stage::Offline`。 - `m_auth_stream.on_connect_failure` 和认证 `connect()` 的同步失败共同调用该函数;没有改变 游戏连接失败、协议包失败或全局 world teardown 的处理。 - `net_classic_session_test` 覆盖无效认证地址的返回值、Offline owner 和可重试标记。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;同步认证失败返回 `false`,阶段为 `Offline`,并保持可重试 的 LoginPhase owner。 - 真实 Windows UI 的 `LoginWindow.OnConnectFailure` 可视化显示、DNS/WinSock 错误文本和真实 服务端重试仍未取证;合同保持 `PARTIAL`。后续不得重复修改本分支的同步/异步认证 owner 合并机制。 ## Implementation fix round 2026-09-21 — game connect immediate failure owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/game-connect-immediate-failure-owner`。 ### 40250 参考调用链 ```text CNetworkStream::Connect / CNetworkStream::Process -> Clear() -> CPythonNetworkStream::OnConnectFailure() -> if DirectEnter: ClosePhase() -> SetLoginPhase() -> else: LoginWindow.OnConnectFailure() ``` `PythonNetworkStreamPhaseLogin.cpp:211-221` 明确把游戏连接失败与 `__RecvLoginFailurePacket -> LoginWindow.OnLoginFailure` 分开。普通登录路径只通知 `LoginWindow.OnConnectFailure` 并保留登录窗口作为重试属主;DirectEnter 路径经过 `ClosePhase -> SetLoginPhase`,不能伪造成凭据错误或普通 peer-close。 ### 当前客户端调用链(修复前) ```text ClassicStream::connect / ClassicStream::process -> ClassicSession::connect* / pump -> handle_game_connect_failure(bool) -> ordinary: set LoggingIn + on_login_failure(last_error) -> DirectEnter: set LoggingIn, but no enter_login_phase(true)/on_phase_close ``` 因此普通认证成功后游戏地址拒绝会触发错误的 `login_failed`,DirectEnter 的同步或异步 连接失败也不会走 40250 的 `ClosePhase` observer;GC_WARP 的桥接层还会额外伪造 `disconnected`。 ### 修复前失败证据 在生产修改前加入两个 loopback/无效地址 fixture: ```text build-debug/extension/net_classic_session_test FAIL: DirectEnter connect failure follows OnConnectFailure -> ClosePhase FAIL: DirectEnter connect failure is not reported as credential login failure 2 check(s) failed build-debug/extension/net_classic_session_test FAIL: ordinary game-connect failure is not reported as credential login failure 1 check(s) failed ``` 前一个 fixture 覆盖 DirectEnter 连接失败,后一个完整驱动 `PHASE_AUTH -> CG_LOGIN3 -> GC_AUTH_SUCCESS -> game connect`,证明差异来自跨阶段 game owner,而不是单独调用连接函数。 ### 修改内容 - `ClassicSession::handle_game_connect_failure()` 合并同步 `Connect` 和异步 `Process` 失败入口,先清理失败 transport,再保留 `m_login_retry_pending`。 - DirectEnter 改为 `enter_login_phase(true)`,触发 phase-leave/`on_phase_close`,并明确不调用 `on_login_failure`;无 socket 时丢弃不可发送的暂存登录字节,显式 retry 再建立新 transport。 - 普通游戏连接失败新增独立 `on_connect_failure` callback;`M2Client` 暴露 `connect_failed` signal,`AppFlow`/独立登录入口只更新 LoginPhase 状态和密码框焦点,保持 40250 的 `OnConnectFailure` 与凭据失败事件分离。 - `M2Client` 的快速重连和 GC_WARP DirectEnter 失败不再伪造 `disconnected`,交给 native `ClosePhase -> SetLoginPhase` owner;没有改变已经建立连接后的真实 peer-close owner。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;DirectEnter 失败走 ClosePhase、普通 game connect 失败走独立 callback,二者均保持 LoginPhase retry owner。 - `project/app_flow_lifecycle_test.gd`:PASS;`connect_failed` 保留现有登录界面,不把 `ECONNREFUSED` 显示为错误密码。 - native/CTest/ASAN/Godot 阶段和包结构回归均 PASS;`audit_phase_dispatch.py` PASS, `audit_packet_registry.py` 保持既有单项 `HDR_GC_SYMBOL_DATA` platform adaptation 的 PARTIAL。 - 真实 Windows `LoginWindow.OnConnectFailure` 视觉表现、真实 DirectEnter/GC_WARP 失败后的 服务器回退包序,以及初始认证同步失败在 `M2Client::connect_to_server` 中仍可能被桥接为 终态 `Failed` 的独立 auth bridge 分支,尚未在本轮关闭;合同保持 `PARTIAL`。后续选择新的 `auth-connect-bridge-owner` 或 live failure package-order 分支,不重复修改本轮 native `handle_game_connect_failure()`。 ## Implementation fix round 2026-09-21 — auth connect bridge owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-connect-bridge-owner`。 ### 40250 参考调用链 ```text CAccountConnector::Connect -> CNetworkStream::Connect -> immediate socket/DNS failure or Process connect failure -> Clear() -> CAccountConnector::OnConnectFailure() -> LoginWindow.OnConnectFailure() -> __OfflineState_Set() ``` 认证连接失败属于 `OnConnectFailure`/Offline owner,不属于凭据校验失败 `OnLoginFailure`。`CAccountConnector` 清理的是认证 transport,但保留登录窗口作为显式重试 属主;因此重试应重新建立认证连接,不应把整个登录流程置为终态失败。 ### 当前客户端调用链(修复前) 同步失败路径: ```text M2Client::connect_to_server -> ClassicSession::connect(auth) -> ClassicStream::connect() == false -> ClassicSession::handle_auth_connect_failure() -> native Offline -> M2Client false branch -> login_failed -> Failed -> classic_sess.reset() ``` 异步失败路径也存在同一桥接问题: ```text ClassicSession::pump() -> auth stream OnConnectFailure -> native Offline -> M2Client::pump_classic() -> login_failed -> Failed ``` 这会丢失 40250 `AccountConnector` 的认证 Offline owner,使 `LoginWindow.Connect` 无法在 原认证 owner 上进行第二次尝试;同时还把连接不可达错误误报为凭据登录失败。 ### 修复前失败证据 新增专项回归 `project/auth_connect_bridge_test.gd`,修复前关键断言失败: ```text FAIL: initial auth connect failure reaches the distinct OnConnectFailure bridge FAIL: initial auth connect failure does not enter terminal Failed stage FAIL: retry_login replaces only the auth transport after auth connect failure FAIL: auth connect retry does not enter terminal Failed stage 4 check(s) failed ``` ### 修改内容 - `ClassicSession` 新增 `on_auth_connect_failure`,在认证连接失败回到 native `Stage::Offline` 前通知上层,保持 40250 的 `OnConnectFailure -> Offline` 顺序;它与 `on_login_failure`、游戏连接失败 callback 分离。 - `M2Client` 新增 `classic_auth_connect_failure_pending`,把该 callback 映射为独立的 `connect_failed` signal 和非终态 `AuthConnect` 公共阶段;同步 `connect_to_server`、异步 `pump_classic` 和 `retry_login` 都不再把这类失败转换为 `login_failed -> Failed`。 - 同步失败不再销毁 `classic_sess`;重试前只清除认证失败 pending 标记,复用现有 `ClassicSession` 并由 `ClassicSession::retry_login` 替换认证 transport。新的完整连接、显式 disconnect 和新一轮连接都会重置该标记,避免旧失败状态污染后续阶段。 - 认证失败 callback 在初始认证连接和 `GC_WARP`/重连创建的 session 上使用同一 owner 规则; 没有改变本轮已关闭的游戏连接失败 owner、GC_WARP `ClosePhase` 或真实 peer-close owner。 ### 修复后验证与边界 - `project/auth_connect_bridge_test.gd`:PASS;初始认证连接失败和第二次 `retry_login` 均 发送独立 `connect_failed`,不发送 `login_failed`,并保持非终态 retry owner。 - `net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`、 `net_entity_test`:PASS;CTest `23/23` PASS。 - `ASAN_OPTIONS=detect_leaks=0 build-asan/extension/net_classic_session_test`:PASS; Godot `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、 `disconnect_world_reset_test.gd` 和阶段/包结构审计通过。 - 修复后的本地桥接 owner 已关闭本分支;真实 Windows `LoginWindow.OnConnectFailure` 视觉 文本、真实服务端失败后重新输入成功的包序、服务器主动 `PHASE_CLOSE` live 包序、完整跨阶段 retry 和其它可选安全 runtime 仍未取证,合同继续保持 `PARTIAL`。后续应选择新的 live package-order 分支,不重复修改本分支的认证 callback、pending 标记或 session 保留逻辑。 ## Live evidence refresh 2026-09-21 — server-initiated phase-close probe 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`。 本轮是证据刷新,不是生产代码修复。分支此前没有历史记录,本次探针结果已经登记,后续在 服务端 fixture 不变的情况下不得重复执行同一个 `/logout` 探针。 ### 40250 参考调用链 ```text GM command "logout" -> SCMD_LOGOUT -> CHARACTER::Logout / timed logout event -> DESC::SetPhase(PHASE_CLOSE) -> close input processor / transport close boundary ``` 源码审计确认 `cmd.cpp` 将 `logout` 映射到 `SCMD_LOGOUT`,`cmd_general.cpp` 的 logout 分支最终调用 `DESC::SetPhase(PHASE_CLOSE)`;`desc.cpp` 在设置 phase 后切换到 `m_inputClose`。这证明服务端有主动关闭阶段的参考状态链,但不自动证明客户端一定收到一个 可观察的 `HEADER_GC_PHASE(PHASE_CLOSE)` 包。 ### 当前客户端与真实服务端观察 使用配置的 40250 Ubuntu 服务端和 Godot live smoke: ```text auth -> PHASE_LOGIN -> PHASE_SELECT -> PHASE_LOADING -> PHASE_GAME -> MT_SMOKE_PHASE_COMMAND=/logout -> peer close / MAIN_LOST -> world_reset -> LoginPhase owner ``` 功能报告为 PASS:角色进入游戏、执行 `/logout`、释放 GameScene、回到唯一 LoginPhase owner 均成功。开启 `MT_NET_TRACE=1` 后,实时 phase 记录为 `1,2,3,1,2,3,4,5`,随后只有 `DISCONNECTED peer closed`;没有观察到 `[classic:game] phase -> 0`,即没有观察到客户端收到 `GC_PHASE(PHASE_CLOSE)`。 对照服务端 `desc.cpp` 可见,`DESC::SetPhase` 先把 `m_iPhase` 设为 `PHASE_CLOSE`,而 `DESC::Packet` 在 `m_iPhase == PHASE_CLOSE` 时直接返回;因此当前服务端 build 的 `/logout` 路径实际提供的是 peer-close 证据,而不是可区分的客户端 `PHASE_CLOSE` 包 fixture。这个 服务端实现边界不应通过修改客户端来“补齐”或伪造。 外层 smoke wrapper 仍因已有的 4 个 `ParticlesShaderRD`/RID 泄漏门禁返回失败,但报告中的 网络功能状态为 PASS;该渲染资源泄漏与 PHASE_CLOSE 包是否发送是两个独立问题。 ### 结论与未闭合边界 - 当前客户端对“服务端关闭连接后回到 LoginPhase owner”的可观察行为已取得真实证据,且与 已关闭的 peer-close owner 分支一致;本轮没有发现需要再次修改 `ClassicSession` 或 `AppFlow` 的新差异。 - 不能把本轮结果记为真实服务器主动 `PHASE_CLOSE` 包序通过,因为 trace 中没有 phase 0。 - 该子分支保持 `PARTIAL`。要继续关闭它,必须提供一个真正发送客户端可观察 `GC_PHASE(PHASE_CLOSE)` 的 40250 server build/fixture,并在同一 fixture 上验证失败后重新 输入成功、完整跨阶段 retry 以及 DirectEnter/GC_WARP 失败回退包序;在 fixture 改变前不重复 `/logout` 运行或修改已关闭的 close/peer-close owner。 ## Implementation fix round 2026-09-21 — auth-success transport handoff order 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-success-game-before-auth-clear`。 ### 40250 参考调用链 ```text CAccountConnector::__AuthState_RecvAuthSuccess -> CPythonNetworkStream::SetLoginKey -> CPythonNetworkStream::Connect(game address) -> CAccountConnector::Disconnect -> CAccountConnector::__OfflineState_Set ``` `AccountConnector.cpp` 的成功认证分支先把 login key 交给主网络流并启动游戏连接,随后才 清理 AccountConnector。这里是两个独立 transport owner 的交接边界;不能先清空认证 owner 再建立主连接,否则失败/重试期间的可观察顺序与 40250 不同。 ### 当前客户端调用链(修复前) ```text ClassicSession::pump -> m_auth_stream.disconnect() -> m_stream.connect(game_host, game_port) ``` 修复前认证 transport 的 `Offline` 状态先于游戏 transport 的 `Connecting` 状态发生,和 40250 的主连接先启动、认证连接后清理相反。 ### 修复前失败证据 在生产修改前,`net_classic_session_test` 增加两个 transport 的状态观察 fixture,关键断言 失败: ```text FAIL: auth success starts game transport before clearing auth transport like 40250 1 check(s) failed ``` ### 修改内容 - `ClassicSession::pump()` 在认证成功交接中先调用 `m_stream.connect()`,再调用 `m_auth_stream.disconnect()`;共享的 transition guard 仍覆盖两个清理/连接回调,未改变 游戏连接失败、peer close 或登录重试 owner。 - `extension/tests/net_classic_session_test.cpp` 增加真实 loopback 双 transport fixture, 验证 `game:connecting -> auth:offline` 的顺序,并继续接受替换后的游戏连接,覆盖连接成功、 认证清理和旧 transport 边界。 ### 修复后验证与边界 - 修复后 `net_classic_session_test`:PASS;认证成功交接严格观察到先建立 game transport, 再清理 auth transport。 - 本轮只改认证成功的 transport handoff 顺序,没有修改飞行物、技能、战斗或 UI,也没有重开 已关闭的 peer-close/PHASE_CLOSE owner 分支。 - 真实 Windows/真实服务端失败后重新输入成功、完整跨阶段 retry、全空角色 live、服务器主动 `PHASE_CLOSE` 包序和 DirectEnter/GC_WARP 失败回退仍未闭合;合同保持 `PARTIAL`。下一步应 继续取新的失败重试或服务端 fixture 证据,不重复本轮 handoff 顺序。 ## Implementation fix round 2026-09-21 — game-login retry process boundary 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/game-login-retry-process-boundary`。 ### 40250 参考调用链 ```text CPythonApplication::Process -> CPythonNetworkStream::Process -> CAccountConnector::Process -> CAccountConnector::__AuthState_RecvAuthSuccess -> CPythonNetworkStream::Connect(game address) -> return from the current application frame -> next frame: CPythonNetworkStream::Process(new game transport) ``` 40250 的登录表单重试只替换 AccountConnector transport;游戏主流仍是同一个 `CPythonNetworkStream`。因此在“游戏 LoginPhase 失败、等待重新输入”期间,主流先按 `CPythonApplication::Process` 的顺序处理,随后才处理 AccountConnector。认证成功时主流的 `Connect()` 发生在主流本帧的处理机会之后,新主流不能在同一帧继续消费 `PHASE_LOGIN`。 ### 当前客户端调用链(修复前) ```text ClassicSession::pump -> auth_stream.process() -> m_stream.connect(replacement game transport) -> m_stream.process() // 同一帧再次处理新主流 -> consume replacement PHASE_LOGIN ``` 这会把替换连接上的阶段包提前到认证成功的同一帧处理,改变 40250 的阶段/事件边界;同时 游戏登录失败后的旧主流没有明确保留“在 AccountConnector 之前处理”的语义。 ### 修复前失败证据 在生产代码修改前,`net_classic_session_test` 增加 loopback 重试 fixture:旧游戏流收到 `GC_LOGIN_FAILURE` 后保留,认证成功回调期间替换服务器同步发送 `PHASE_LOGIN`。修复前输出: ```text FAIL: auth success starts the replacement game transport without processing it in the same frame 1 check(s) failed ``` ### 修改内容 - `ClassicSession` 增加 `m_auth_retry_with_existing_game_stream`,只在重试确实仍有旧主流 transport 时保留该处理语义;认证失败/初次认证/已清理的连接不会误处理旧 socket。 - `ClassicSession::pump()` 调整为先处理当前有效游戏流,再处理认证流,符合 `CPythonApplication::Process` 的主流→AccountConnector 顺序。 - 认证成功建立替换游戏 transport 后,本次 pump 设置 handoff 边界并跳过新主流处理;下一次 pump 才消费替换连接的 `PHASE_LOGIN`,并保留旧 transport 在认证成功前可处理、成功后由 `ClassicStream::connect()` 清理的 40250 语义。 - 新增 `net_classic_session_test`,覆盖游戏 LoginPhase 失败、同 session auth retry、旧流保留、 auth success、替换 game transport 和下一帧才处理 `PHASE_LOGIN`。 ### 修复后验证与边界 - `net_classic_session_test`:PASS;第一帧保持 `Connecting`,第二帧才进入 `LoggingIn`。 - `net_classic_stream_test`、`net_loopback_test`、`net_entity_test`:PASS;CTest `23/23` PASS。 - ASAN `net_classic_session_test`:PASS;Godot `app_flow_lifecycle_test.gd`、 `auth_connect_bridge_test.gd`、`auth_peer_close_bridge_test.gd`、`p10_test.gd`、 `netplay_test.gd`、`disconnect_world_reset_test.gd`:PASS。 - `audit_phase_dispatch.py`:PASS;`audit_sequence_table.py`:PASS; `audit_source_coverage.py`:PASS(524 个 active 文件、0 个未归属行为候选); `audit_packet_registry.py` 仍为已批准的 `HDR_GC_SYMBOL_DATA` 适配导致的 `PARTIAL`。 - 本地“重试处理帧边界”分支已闭合,但真实 Windows LoginWindow、真实服务端失败后重试成功 包序、完整跨阶段 retry、全空角色、服务器主动 `PHASE_CLOSE` live 和 DirectEnter/GC_WARP 失败回退仍未闭合;合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — GB2312 UI input boundary 问题/分支 ID:`network.login.phase_flow/legacy-text-wire-encoding/ui-gb2312-input-capacity`。 ### 差异 40250 的角色名字段容量是 24 个 GB2312 wire 字节;“龙驹简体中文免费版”虽然占 27 个 UTF-8 字节,但只占 18 个 GB2312 字节,应该可以进入创建/改名流程。native `ClassicSession` 已在发包前转换,但选人页 `CharSelectScreen` 仍按 UTF-8 字节数把输入框 限制为 24 字节,导致合法名称在到达 native 转码前被错误拒绝。 ### 修改 - `project/ui/char_select_screen.gd` 改为只限制 Unicode 字符数量和控制字符;输入框不再按 UTF-8 字节截断,最终 24-byte GB2312 可表示性由 `M2Client/ClassicSession` 统一校验。 - 创建和改名输入框使用 24 个 Unicode 字符作为编辑上限,提示文本明确中文角色名实际按 GB2312 wire 字节校验;这不会把 13 个中文字符误判为合法,native 仍会拒绝超过 24 GB2312 字节的名称。 - `net_text_codec_test` 增加“龙驹简体中文免费版”的 27 UTF-8 / 18 GB2312 往返断言; `char_create_delete_test.gd` 增加选人页不提前拒绝该名称的回归。 ### 验证 - `build-debug/extension/net_text_codec_test`:PASS。 - `build-debug/extension/net_classic_session_test`:PASS;固定角色名创建/改名仍按 GB2312 字节写入 24-byte 字段并保留 NUL。 - `MT_PROTOCOL=classic godot --headless --path project --script char_create_delete_test.gd`: PASS;UI 保留“龙驹简体中文免费版”并交给创建接口。 本轮只修复 UI 输入层与 GB2312 wire 边界的重复校验差异,没有把地图名、任务名或系统提示 错误套用 24-byte 角色名限制;合同仍保持 `PARTIAL`,其它 legacy/可选安全和真实服务端 中文建号包序证据仍未闭合。 ## Implementation fix round 2026-09-21 — Loading EnterGame event boundary 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/loading-enter-game-event-boundary`。 ### 40250 参考调用链 ```text CPythonNetworkStream::SetLoadingPhase -> LoadingPhase -> RecvMainCharacter / RecvMainCharacter3_BGM / RecvMainCharacter4_BGM_VOL -> networkmodule.SetLoadingPhase 的 LoadingWindow/GameWindow.LoadData -> net.SendEnterGamePacket -> CPythonNetworkStream::SendEnterGame ``` `introloading.py::LoadingWindow.Open()` 在 DirectEnter 的 `PHASE_SELECT` 阶段发送 `CG_PLAYER_SELECT`;真正收到 `GC_MAIN_CHARACTER` 后,`game.py::GameWindow.LoadData()` 的最后 一步才发送 `CG_ENTERGAME`。参考实现没有在 `PHASE_LOADING` 进入后按墙钟延迟发送该包。 ### 当前客户端差异(修复前) ```text ClassicSession::on_phase(PHASE_LOADING) -> m_loading_since = m_now -> pump() 等待 m_entergame_delay(M2Client 默认 1500ms) -> enter_game() ``` 这会让 `CG_ENTERGAME` 脱离 `GC_MAIN_CHARACTER`/LoadingWindow 的事件边界,在主角包尚未到达时 可能过早发送,或在主角包已经完成加载后仍额外等待;也无法证明与 40250 的 GameWindow 初始化 顺序一致。 ### 修复前失败证据 先加入专项测试并在生产修复前运行: ```text build-debug/extension/net_classic_session_test FAIL: GC_MAIN_CHARACTER triggers version then CG_ENTERGAME in the same loading event 1 check(s) failed ``` 测试关闭兼容定时器后注入 `PHASE_LOADING + GC_MAIN_CHARACTER`,要求版本包后紧接一次 `CG_ENTERGAME`;修复前当前实现只发版本包,不会在主角事件中发送 EnterGame。 ### 修改内容 - `ClassicSession::on_packet()` 将四个可用主角包中的 40250 主角/BGM 变体作为 Loading 事件边界; 解析成功并发送客户端版本包后立即调用 `enter_game()`。 - 删除 ClassicSession 的 `m_loading_since`、`m_entergame_delay` 和 `MT_CLASSIC_ENTER_DELAY` 注入;保留 `m_entergame_sent` 作为同一 Loading 周期的幂等保护, 重复 `GC_MAIN_CHARACTER` 不会重复发送。 - `net_classic_session_test` 改为验证事件顺序和 sequence,世界插值测试改用显式服务器时间, 不再借用 EnterGame 定时器推进测试时钟;`net_classic_e2e` 不再暴露定时器覆盖项。 ### 修复后验证与边界 - `build-debug/extension/net_classic_session_test`:PASS;版本包后同一主角事件发送一次 `CG_ENTERGAME`,重复主角包保持幂等。 - `net_classic_stream_test`、`net_loopback_test`:PASS。 - 本分支只修复 Loading/EnterGame 的事件源和包顺序,没有修改飞行物、技能、战斗或 UI; 真实 Windows GameWindow 资源加载耗时、真实服务端完整 Loading 包序、可选安全分支和其它 跨阶段 retry/PHASE_CLOSE live 证据仍属于合同的 `remaining`,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — DirectEnter failure resets LoginPhase owner 问题/分支 ID:`network.login.phase_flow/ticket-slot-warp-order/direct-enter-failure-login-owner-reset`。 ### 40250 参考调用链 ```text CPythonNetworkStream::ConnectGameServer / RecvWarpPacket -> __DirectEnterMode_Set -> CNetworkStream::Connect -> CPythonNetworkStream::OnConnectFailure -> ClosePhase -> networkmodule.SetLoginPhase -> net.Disconnect -> CPythonNetworkStream::SetOffLinePhase -> __DirectEnterMode_Initialize -> 后续 PHASE_LOGIN 的普通 SetLoginPhase 分支 -> __ClearSelectCharacterData ``` 40250 的 DirectEnter 连接失败不是凭据失败。`ClosePhase` 交给 Python `networkmodule.SetLoginPhase` 后,`net.Disconnect()` 会先回 Offline 并清除 DirectEnter 模式;之后真正的新连接收到 `PHASE_LOGIN` 时才重新进入普通 LoginPhase。普通分支发送 `CG_LOGIN/CG_LOGIN2` 后清理旧角色列表 owner。 ### 当前差异与修复前失败证据 当前 `ClassicSession::handle_game_connect_failure()` 通过 `enter_login_phase(true, false)` 保留了 `m_direct_enter`,只把 stage 改成了 `LoggingIn`。因此替换连接的 `PHASE_LOGIN` 仍可能沿用 DirectEnter 状态,跳过普通 LoginPhase 的角色列表清理。新增回归在生产修复前运行: ```text build-debug/extension/net_classic_session_test FAIL: DirectEnter connect failure clears the stale slot before replacement LoginPhase 1 check(s) failed ``` ### 修改内容 - `ClassicSession::enter_login_phase(bool, bool)` 在“不在旧 transport 上发送登录包”的 ClosePhase/remote-disconnect owner 切换中清除 `m_direct_enter` 和 `m_direct_enter_slot`;这对应 40250 `net.Disconnect -> SetOffLinePhase -> __DirectEnterMode_Initialize`,不影响真正 `PHASE_LOGIN` 的 `CG_LOGIN2` ticket 路径。 - 增加 native 回归:DirectEnter 目标连接失败后注入替换 `PHASE_LOGIN`,验证旧角色槽位 被普通 LoginPhase 清除;既有 DirectEnter/GC_WARP 成功、半包、重复 phase、peer close 和 PHASE_CLOSE 测试继续保留。 ### 修复后验证与边界 - 修复后 `build-debug/extension/net_classic_session_test`:PASS。 - 本轮只修复 DirectEnter/GC_WARP 失败后的 phase-owner 状态,不修改飞行物、技能、战斗 或 UI;真实 Windows LoginWindow 表现、真实服务端失败回退包序、空角色 live、服务器 主动 `PHASE_CLOSE` 和完整跨阶段 retry 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — PHASE_CLOSE LoginWindow retry owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-close-login-retry-owner`。 ### 40250 参考调用链 ```text CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE) -> ClosePhase -> networkmodule.SetLoginPhase -> net.Disconnect -> LoginWindow.Connect -> networkmodule.Connect -> net.ConnectToAccountServer -> CAccountConnector::Connect ``` `PHASE_CLOSE` 不是凭据错误,也不是终态 Failed。40250 先关闭旧主连接并创建 LoginWindow; 用户再次提交登录时,`KEEP_ACCOUNT_CONNETION_ENABLE=1` 使 `ConnectToAccountServer` 只替换 认证 transport,旧主连接/session owner 不由 UI 再次全局销毁。 ### 当前差异与修复前失败证据 当前 `ClassicSession::on_phase(PHASE_CLOSE)` 能回到 `LoggingIn` 并清理旧游戏流,但没有设置 `m_login_retry_pending`。因此 `AppFlow::_do_connect -> M2Client::retry_login` 会无法复用 当前 LoginPhase owner,退回全量 `connect_to_server` 路径。新增 loopback 回归在生产修复前运行: ```text build-debug/extension/net_classic_session_test FAIL: PHASE_CLOSE keeps the LoginWindow retry owner for a replacement auth transport 1 check(s) failed ``` ### 修改内容 - `ClassicSession::on_phase(PHASE_CLOSE)` 设置 `m_login_retry_pending = true`,明确把 `ClosePhase -> LoginWindow` 交给下一次显式 Connect;不在已关闭的 game socket 上发送 `CG_LOGIN`,也不发送 `disconnected`。 - 增加 loopback 回归:显式 `PHASE_CLOSE` 后在同一 session 上建立替换认证 transport,收到 `PHASE_AUTH` 时只发送一份新的 `CG_LOGIN3`;既有 peer-close、DirectEnter failure、半包、 重复 phase 和旧 world 清理测试继续保留。 ### 修复后验证与边界 - 修复后 `build-debug/extension/net_classic_session_test`:PASS。 - 本轮只补齐显式 PHASE_CLOSE 后的 retry owner,不修改飞行物、技能、战斗或 UI;真实 Windows LoginWindow 视觉时序、真实服务端主动 `PHASE_CLOSE` 包序、空角色 live、完整 跨阶段 retry 和可选安全 runtime 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — PHASE_HANDSHAKE re-entry owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-handshake-reentry-owner`。 ### 40250 参考调用链 ```text CPythonNetworkStream::RecvPhasePacket(PHASE_HANDSHAKE) -> SetHandShakePhase -> previous phase-leave callback -> HandShakePhase CAccountConnector::__AuthState_RecvPhase(PHASE_HANDSHAKE) -> __HandshakeState_Set -> __HandshakeState_Process ``` 40250 收到重新进入的 `PHASE_HANDSHAKE` 后,不会继续沿用之前的 Login/Select/Loading/Game 处理器。若上一阶段是 Loading/Game,phase-leave owner 先清理地图对象;认证连接则回到 Handshake 状态,只有下一次 `PHASE_AUTH` 才能发送新的认证登录包。 ### 当前差异与修复前失败证据 修复前 `ClassicSession::on_phase` 对 `PHASE_HANDSHAKE` 走默认分支,游戏仍停留在 `InGame` 且旧 world 未清理;`on_auth_phase` 也忽略该阶段,认证仍停留在 `LoggingIn`。 新增专项回归在生产修复前运行: ```text build-debug/extension/net_classic_session_test FAIL: PHASE_HANDSHAKE re-enters the 40250 HandShake owner FAIL: PHASE_HANDSHAKE runs the previous phase leave cleanup before ownership changes FAIL: auth PHASE_HANDSHAKE re-enters the 40250 HandShake owner 3 check(s) failed ``` ### 修改内容 - `ClassicSession::on_phase(PHASE_HANDSHAKE)` 现在在 Loading/Game owner 上先执行 `reset_for_map_change()` 和 `on_phase_leave`,再关闭 time-sync 模式、清除本次 `CG_ENTERGAME` 事件状态并进入 `Connecting/HandShake` owner。 - `ClassicSession::on_auth_phase(PHASE_HANDSHAKE)` 现在回到认证 Handshake owner,阻止 旧 LoginPhase 应用包在新的 `PHASE_AUTH` 之前触发登录副作用。 - 没有修改飞行物、技能、战斗、角色名编码或 UI;真正的登录发送仍只由后续 `PHASE_LOGIN/PHASE_AUTH` 触发。 ### 修复后验证与边界 - `build-debug/extension/net_classic_session_test`:PASS。 - `ctest --test-dir build-debug --output-on-failure -j2`:23/23 PASS。 - ASAN `net_classic_session_test`:PASS。 - Godot `app_flow_lifecycle_test.gd`、`p10_test.gd`、`netplay_test.gd`、 `disconnect_world_reset_test.gd`:PASS。 - `audit_phase_dispatch.py`、`audit_sequence_table.py`、`audit_source_coverage.py`:PASS; `audit_packet_registry.py` 仍仅保留已批准的 `HDR_GC_SYMBOL_DATA` 平台适配差异。 - 真实 Windows/服务端主动 `PHASE_CLOSE`、空角色 live、完整跨阶段 retry 和可选安全 runtime 仍未取证,合同继续保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — GC_AUTH_SUCCESS result authority 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-success-result-authority`。 ### 40250 参考调用链 ```text CAccountConnector::__AuthState_Process -> __AuthState_RecvAuthSuccess -> if (!kAuthSuccessPacket.bResult) OnLoginFailure("BESAMEKEY") -> else SetLoginKey(dwLoginKey) -> CPythonNetworkStream::Connect(game_addr, game_port) -> CAccountConnector::Disconnect ``` 40250 的认证成功/失败分支只由 `bResult` 决定。`dwLoginKey` 是认证成功后交给游戏连接的 票据值,不是第二个凭据失败条件;认证成功分支完成后仍会清理 AccountConnector。 ### 当前客户端差异与修复前失败证据 修复前 `ClassicSession::on_auth_packet` 使用 `p.result == 0 || m_auth_login_key == 0`, 导致 `result=1`、`login_key=0` 被错误转换为 `BESAMEKEY`。专项测试在恢复修复前条件后失败: ```text build-debug/extension/net_classic_session_test FAIL: GC_AUTH_SUCCESS result=1 remains success even when the ticket value is zero 1 check(s) failed ``` ### 当前客户端修复后的调用链 ```text ClassicStream::dispatch -> ClassicSession::on_auth_packet(GC_AUTH_SUCCESS) -> if (result == 0) on_login_failure("BESAMEKEY") -> else m_auth_succeeded = true / retain login_key -> ClassicSession::pump -> connect game transport first -> disconnect auth transport ``` ### 修改内容与验证 - 移除 `login_key == 0` 的伪造失败条件,只保留 40250 的 `result == 0` 拒绝分支。 - 保留失败回调、auth stream 清理、成功后的 game transport handoff 和重复包清理行为。 - `build-debug/extension/net_classic_session_test`:PASS;`result=1/login_key=0` 不再发出 `BESAMEKEY`,认证 transport 仍完成成功分支清理。 - 本轮未修改飞行物、技能、战斗或 UI。 - 真实服务端不会主动构造“成功但零票据”的异常包,完整 Windows/服务端重试、主动 `PHASE_CLOSE`、空角色 live 和可选安全 runtime 仍未闭合,合同保持 `PARTIAL`。 ## Implementation fix round 2026-09-21 — auth PHASE_CLOSE silent owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/auth-phase-close-silent-owner`。 ### 40250 参考调用链 ```text CAccountConnector::__AuthState_Process -> __AuthState_RecvPhase -> if (phase == PHASE_HANDSHAKE) __HandshakeState_Set() -> else if (phase == PHASE_AUTH) send CG_LOGIN3 / __AuthState_Set() -> PHASE_CLOSE falls through and returns true ``` 认证连接的 `PHASE_CLOSE` 不走游戏侧 `ClosePhase`,也不调用 `OnLoginFailure`、`OnConnectFailure` 或 `__OfflineState_Set`;AUTH owner 继续由认证 socket 的真实 disconnect/failure 事件决定。 ### 当前客户端差异与修复前失败证据 修复前 `ClassicSession::on_auth_phase` 对认证 `PHASE_CLOSE` 写入 `m_last_error = "auth server closed phase"` 并设置全局 `m_phase_closed`。这把认证阶段 的静默 phase 值错误地解释成了游戏 transport 的显式关闭,污染后续 owner/retry 判断。 专项回归在恢复旧实现后失败: ```text build-debug/extension/net_classic_session_test FAIL: auth PHASE_CLOSE is silent and does not mark game phase closure 1 check(s) failed ``` ### 修改内容与验证 - `on_auth_phase` 每次先清除游戏侧 `m_phase_closed`;只保留 `PHASE_HANDSHAKE` 和 `PHASE_AUTH` 的认证 owner 分支,移除认证 `PHASE_CLOSE` 的错误标记和错误文本。 - 游戏 transport 的 `on_phase(PHASE_CLOSE)` 保持原有显式关闭、旧 world 清理和 LoginPhase retry owner 语义,未把两条 phase handler 合并。 - 修复后 `net_classic_session_test` 通过;完整 CTest、ASAN、Godot 生命周期/网络回归 和结构审计随后执行并登记在本轮 history,合同仍保持 `PARTIAL`。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。 ## Implementation fix round 2026-09-21 — PHASE_DEAD no-op time-sync owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-dead-noop-time-sync`。 ### 40250 参考调用链 ```text CPythonNetworkStream::RecvPhasePacket(PHASE_DEAD) -> switch(PHASE_DEAD) -> break -> return true ``` 40250 的 `PHASE_DEAD` 分支没有调用 phase setter,也没有修改 handshake/time-sync owner。 因此在尚未进入 Login/Select/Loading/Game 的阶段收到该值时,后续握手仍按当前 owner 的普通 `HANDSHAKE` 回包处理。 ### 当前客户端差异与修复前失败证据 修复前 `ClassicSession::on_phase()` 在分派到 `PHASE_DEAD` 之前把 `ClassicStream::m_time_sync_mode` 设置为 `true`。这会让随后重复握手错误地发出 `CG_TIME_SYNC`,而不是保持 40250 的普通 `HANDSHAKE` 回包;同时通用的 `phase != PHASE_CLOSE` 清理条件还会错误清除显式关闭 owner 标记。 专项测试在恢复修复前条件后失败: ```text build-debug/extension/net_classic_session_test FAIL: PHASE_DEAD is a no-op and keeps the plain handshake owner 1 check(s) failed ``` 补充的修复前回归还失败于: ```text FAIL: PHASE_DEAD does not clear the explicit close owner marker 1 check(s) failed ``` ### 修改内容与验证 - 从阶段到 time-sync owner 的条件中移除 `PHASE_DEAD`,并只允许真正安装新阶段 owner 的 `PHASE_HANDSHAKE/LOGIN/SELECT/LOADING/GAME` 清除 `m_phase_closed`;该阶段继续由 `switch` 的默认空分支保持无状态变化。 - 新增 `PHASE_DEAD → 重复 HANDSHAKE` 回归,验证回包头仍为 `HDR_HANDSHAKE`,避免将 `PHASE_DEAD` 与真正的 Login/Select/Loading/Game phase setter 混用;同时验证 `PHASE_CLOSE → PHASE_DEAD` 保留显式关闭 owner 标记。 - 修复后 `net_classic_session_test` 通过;本轮完整 CTest、ASAN、Godot 回归和结构审计完成后 登记到 history,合同仍保持 `PARTIAL`。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。 ## Implementation fix round 2026-09-21 — PHASE_SELECT re-entry owner 问题/分支 ID:`network.login.phase_flow/auth-game-phase-matrix/phase-select-reentry-owner`。 ### 40250 参考调用链 ```text CPythonNetworkStream::RecvPhasePacket(PHASE_SELECT) -> SetSelectPhase -> if (old phase != Select) m_phaseLeaveFunc.Run() -> __LeaveGamePhase -> clear old map-local actors/items/effects -> install SelectPhase owner ``` `PythonNetworkStreamPhaseSelect.cpp:9-12` 明确要求先运行旧阶段的离开函数;当旧 owner 是 Game 时,`__LeaveGamePhase` 负责清理旧地图状态,然后才进入选人阶段。 ### 当前客户端差异与修复前失败证据 修复前 `ClassicSession::on_phase(PHASE_SELECT)` 直接设置 `CharSelect`/DirectEnter 的下一阶段, 没有运行 Game owner 的 phase-leave 清理,因此旧 `EntityStore` 和宿主的 phase-leave 通知会残留。 专项测试在恢复修复前逻辑后失败: ```text build-debug/extension/net_classic_session_test FAIL: PHASE_SELECT runs the previous GamePhase leave cleanup FAIL: PHASE_SELECT notifies the host after GamePhase leave cleanup 2 check(s) failed ``` ### 修改内容与验证 - `ClassicSession::on_phase(PHASE_SELECT)` 在当前 owner 为 `InGame` 时先调用 `reset_for_map_change()`,再调用 `on_phase_leave`,随后才安装 `CharSelect` 或 DirectEnter 的 Select owner。 - 新增 Game → Select 的旧 world、阶段 owner 和宿主通知回归,避免把 Loading 的空离开函数误套成 Game 清理逻辑。 - 修复后 `net_classic_session_test` 通过;完整 CTest、ASAN、Godot 生命周期/网络回归和结构审计 随后执行并登记在本轮 history,合同仍保持 `PARTIAL`。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。