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