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

2412 lines
143 KiB
Markdown
Raw Blame History

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