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

143 KiB
Raw Blame History

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 断言后、未修改生产代码运行:

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 参考调用链

CNetworkStream::Process
  -> CAccountConnector::OnRemoteDisconnect
  -> CAccountConnector::__OfflineState_Set
  -> __Inialize(STATE_OFFLINE)

认证 socket 被对端关闭时,40250 不调用 OnLoginFailure,也不调用认证连接失败的 OnConnectFailure;它只把 AccountConnector 恢复到 Offline owner,等待 LoginWindow 的 下一次显式连接请求。凭据错误和 TCP peer close 必须保持不同的可观测分支。

当前客户端调用链(修复前)

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],由测试服务器接受 认证连接后主动关闭;修复前输出:

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 行为,再未修改生产代码运行:

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 增加登录失败后再次点击连接的断言,再未修改生产代码运行:

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。修复前结果:

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 合同断言,再未修改探针运行:

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 参考调用链

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 上等待用户重试; 这两个分支不能合并成“所有失败包都清缓冲”。

当前客户端调用链(修复前)

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:

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 参考调用链

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() 的游戏包不套用该单包限制。

当前客户端调用链(修复前)

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:

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 参考调用链

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。

当前客户端调用链(修复前)

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 清理路径;因此同步与异步错误的阶段结果不一致。

修复前失败证据

在生产修改前加入无效认证地址测试:

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 参考调用链

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。

当前客户端调用链(修复前)

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:

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 参考调用链

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,但保留登录窗口作为显式重试 属主;因此重试应重新建立认证连接,不应把整个登录流程置为终态失败。

当前客户端调用链(修复前)

同步失败路径:

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()

异步失败路径也存在同一桥接问题:

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,修复前关键断言失败:

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 参考调用链

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:

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 参考调用链

CAccountConnector::__AuthState_RecvAuthSuccess
  -> CPythonNetworkStream::SetLoginKey
  -> CPythonNetworkStream::Connect(game address)
  -> CAccountConnector::Disconnect
  -> CAccountConnector::__OfflineState_Set

AccountConnector.cpp 的成功认证分支先把 login key 交给主网络流并启动游戏连接,随后才 清理 AccountConnector。这里是两个独立 transport owner 的交接边界;不能先清空认证 owner 再建立主连接,否则失败/重试期间的可观察顺序与 40250 不同。

当前客户端调用链(修复前)

ClassicSession::pump
  -> m_auth_stream.disconnect()
  -> m_stream.connect(game_host, game_port)

修复前认证 transport 的 Offline 状态先于游戏 transport 的 Connecting 状态发生,和 40250 的主连接先启动、认证连接后清理相反。

修复前失败证据

在生产修改前,net_classic_session_test 增加两个 transport 的状态观察 fixture,关键断言 失败:

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 参考调用链

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。

当前客户端调用链(修复前)

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。修复前输出:

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 参考调用链

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 进入后按墙钟延迟发送该包。

当前客户端差异(修复前)

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 初始化 顺序一致。

修复前失败证据

先加入专项测试并在生产修复前运行:

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 参考调用链

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 的角色列表清理。新增回归在生产修复前运行:

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 参考调用链

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 回归在生产修复前运行:

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 参考调用链

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。 新增专项回归在生产修复前运行:

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 参考调用链

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。专项测试在恢复修复前条件后失败:

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

当前客户端修复后的调用链

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 参考调用链

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 判断。 专项回归在恢复旧实现后失败:

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 参考调用链

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 标记。 专项测试在恢复修复前条件后失败:

build-debug/extension/net_classic_session_test
FAIL: PHASE_DEAD is a no-op and keeps the plain handshake owner
1 check(s) failed

补充的修复前回归还失败于:

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 参考调用链

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 通知会残留。 专项测试在恢复修复前逻辑后失败:

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。