- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
143 KiB
network.login.phase_flow
Scope
对比 40250 从连接、握手、认证、登录、角色列表、直连选人、Loading 到 GAME 的阶段属主、包顺序、登录 key、失败回调、可选安全包、超时和重试清理,与当前 ClassicSession / M2Client / AppFlow 的实现。
Reference call chain
游戏连接与握手
UserInterface/PythonNetworkStream.cpp::ConnectUserInterface/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/OnConnectFailureUserInterface/AccountConnector.cpp::__AuthState_RecvPhasePHASE_AUTH -> CG_LOGIN3/ 相关 client keyGC_AUTH_SUCCESS -> SetLoginKey -> CPythonNetworkStream::Connect- auth close/failure 回调登录 UI
UserInterface/PythonNetworkStreamPhaseLogin.cpp::SetLoginPhase/LoginPhaseCG_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/SendEnterGameUserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhase
Current call chain
project/app_flow.gd::_do_connect/_enter_characterextension/src/net/m2_client.cpp::connect_to_server/pump_auth/pump_game/pump_classicextension/src/net/classic/classic_session.cppconnect/retry_login/connect_with_login_key/connect_direct_enteron_auth_phase/on_auth_packet/on_phase/on_packetsend_auth_login/send_login/send_login_by_key/send_select_char/enter_game
extension/src/net/classic/classic_stream.cpp::process/handle_control/dispatchextension/src/net/classic/classic_parser.cpp::on_gcextension/tools/net_classic_e2e.cpp(真实包序探针,支持全空角色账号只验证 SelectPhase)project/ui/loading_screen.gd,project/ui/reconnect_ui.gd
登录失败后的重试还经过:
project/app_flow.gd::_do_connect -> M2Client::retry_loginM2Client::retry_login -> ClassicSession::retry_login -> ClassicSession::connectClassicSession::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_testbuild/extension/net_classic_stream_testbuild/extension/net_loopback_testproject/app_flow_lifecycle_test.gdnetwork.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、replacementCG_LOGIN2、DirectEnterCG_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 缓冲并保持LoggingInowner。- peer close 和 DirectEnter connect failure 复用同一“不发送旧连接登录包”的 owner 语义;peer close 的密码断言同步改为保留到下一次
LoginWindow.Connect,与 40250 PythonSetLoginPhase不主动调用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只调用 Pythonnetworkmodule.SetLoginPhase。 networkmodule.SetLoginPhase -> net.Disconnect() -> LoginWindow:旧游戏 transport 被关闭, LoginWindow 成为下一次显式登录的 owner;该回调不会在已关闭的游戏 socket 上发送新的登录包。- 用户再次点击登录时,
LoginWindow.Connect -> MainStream.Connect -> networkmodule.Connect;在KEEP_ACCOUNT_CONNETION_ENABLE = 1下进入net.ConnectToAccountServer,由CAccountConnector::Connect只清理/替换认证 transport。 - 新认证连接收到
PHASE_AUTH后进入CAccountConnector::__AuthState_RecvPhase,发送一份使用新输入 凭据的CG_LOGIN3。原有主网络/session owner 不因游戏 peer close 被全局销毁。
当前客户端调用链(修复前)
ClassicSession::m_stream.on_remote_disconnect -> enter_login_phase(false, false)会清理旧 world 并回到LoggingIn,但没有向上层暴露“可由 LoginWindow 重试的 session owner”。AppFlow::_do_connect -> M2Client::retry_login检查不到login_retry_pending后,会回退到M2Client::connect_to_server;该路径先执行全局disconnect_from_server(),销毁旧ClassicSession/world,再创建全新连接。- 这与 40250 的
LoginWindow -> AccountConnector认证 transport 替换边界不一致,且会改变旧 session 的 owner/清理顺序。
修复前失败证据
在增加游戏态 peer-close 重试 owner 断言后、未修改生产代码运行:
build-debug/extension/net_classic_session_test
FAIL: in-game peer close keeps the LoginWindow retry owner on the same session
1 check(s) failed
修复内容
ClassicSession::m_stream.on_remote_disconnect在执行 40250 等价的 LoginPhase/world 清理后,设置m_login_retry_pending = true。- 现有
M2Client::retry_login在 classic 模式下因此保留同一个ClassicSession,更新新账号/密码, 由ClassicSession::retry_login -> ClassicSession::connect(auth)调用ClassicStream::connect替换认证 socket;不再触发全局 world teardown。 - 新增 loopback 回归:游戏 peer close 后在同一 session 上建立替换认证连接,收到
PHASE_AUTH后 只发送一份CG_LOGIN3,并断言其中使用重新输入的密码new-secret。
修复后验证与边界
net_classic_session_test:PASS;覆盖游戏态 peer close、旧 world 清理、retry owner 保留、认证 transport 替换和新密码CG_LOGIN3包序。- 本地完整 native/CTest/Godot 回归和阶段/包结构审计继续按本轮结果记录在
audit/history.jsonl。 - 真实 Windows LoginWindow 的可视化时序、真实服务端“游戏断线后重新输入并成功登录”的 live 包序、
服务器主动
PHASE_CLOSE和完整跨阶段 retry 仍未取证,因此合同保持PARTIAL。
这些测试证明当前实现的 framing、握手、认证到游戏 happy path、角色选择/direct-enter,以及本轮本地 auth/game phase 矩阵部分行为可工作;它们没有证明 40250 的所有失败/可选分支、真实服务端包序或完整 UI retry 完全一致。
Deep audit round 2026-09-20
本轮重新执行登录阶段的 native/session 和 Godot 生命周期回归,并将阶段桥接的失败证据与参考调用链对应:
net_classic_session_test、net_classic_stream_test、net_loopback_test和app_flow_lifecycle_test.gd均退出码 0,说明 framing、握手/session、loopback happy path 和单生命周期 owner 没有回归。- 初始回归发现
p10_test.gd失败 3 项:phase=game后 Loading 遮罩没有隐藏;无 SceneTree 构造选人界面时char_select_screen.gd:698对空 tree 调用create_timer;因此选人和断线重连回放断言连带失败。 - 40250
RecvPhasePacket的PHASE_CLOSE进入ClosePhase,再按 phase leave 回到 Login/Offline 语义;当前已增加显式phase_closed、Loading/Game phase-leave world 清理、world_reset宿主通知和 M2Client owner 路由,但真实 UI 保留和失败后重试仍需验证。 - 当前已在
CG_LOGIN/CG_LOGIN3成功写入后清除ClassicSession::m_pw,并清除 M2Client 的 host retry password cache;无 login key 的 reconnect 现在必须重新显式登录,避免以空密码重试。CG_LOGIN2本身不需要密码。
Implementation fix round 2026-09-21
按 40250 RecvPhasePacket -> SetLoadingPhase/SetGamePhase 的阶段属主修复 LoadingScreen:只有 loading 保持全屏遮罩,login、select 和 game 都立即释放遮罩与输入拦截;同时让选人确认在 tree-owned 界面离树时取消,在 headless/reconnect harness 尚未挂入 SceneTree 时直接发出一次 select_requested,避免空 tree 异常吞掉选人包。
回归:godot --headless --path project --script p10_test.gd、godot --headless --path project --script app_flow_lifecycle_test.gd 和 git diff --check 均通过。此次只闭合了阶段 UI owner 与测试/重建边界;PHASE_CLOSE -> Login 的完整 UI 重试、失败后 retry 清理、可选安全分支和真实服务端包序仍未达到 40250 统一语义,合同保持 PARTIAL。
Remaining
network.login.phase_flow/auth-game-phase-matrix/live-package-order:真实 auth/game 连接建立成功、四槽角色列表、选角、Loading 到 PHASE_GAME、错误密码回调、game connect failure 和一次建立连接后的 peer close 已取证;空角色、服务器主动 PHASE_CLOSE、完整服务器包序矩阵和跨阶段 retry 仍缺 live fixture;半包、重复 phase 和 PHASE_CLOSE 旧 world 的本地测试已覆盖。network.login.phase_flow/login-failure-retry-cleanup:登录阶段失败后的原 UI 属主、普通 Select/Game -> Login 的旧角色/旧 world 清理、认证/游戏PHASE_HANDSHAKE重入 owner、认证握手阶段忽略提前到达的GC_LOGIN_FAILURE、认证成功按bResult而非 ticket 值判定、认证PHASE_CLOSE保持 AUTH owner 且静默、认证未知应用头保留 AUTH owner、认证 TCP 连接失败回到 Offline 并清理旧 stream、Loading/Game 未知头按RecvErrorPacket清理缓冲且保留 phase owner、游戏 peer-close 后 AppFlow 回 LoginPhase owner,以及同一 session 替换认证 transport 的 retry owner 已覆盖;仍需补充认证其它可选/静默分支、跨阶段失败后的完整 UI retry、真实重复进入包序和服务器主动 PHASE_CLOSE live 包序;不把参考未实际 enforced 的 phase timer 当作新增契约。network.login.phase_flow/optional-security-branches:Matrix、Passpod、Panama、HybridCrypt、HS/XTrap 等完整 UI/资源/安全 runtime 仍未实现;本轮已补齐静态与动态可选包的显式 deployment guard,保留 phase/socket owner,不静默断开。network.login.phase_flow/ticket-slot-warp-order:本地回归和实时探针已证明 login key、selected slot、DirectEnter、GC_WARP 的 replacementCG_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_SELECTowner 和 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/SDBCEterPackManager::RetrieveHybridCryptPackKeys/SDB- 消费当前帧后返回,保留认证/游戏 phase owner,不把可选包当作 transport close
当前调用链(修复后):
ClassicStream::dispatch识别动态HDR_GC_HYBRIDCRYPT_KEYS/SDB,等待完整帧并消费固定头+payloadClassicSession::report_unsupported_security记录明确的部署能力缺口、登记未处理 header,并触发on_unsupported_securityM2Client转发为optional_security_required(header, reason)信号;phase/socket owner 保持不变,后续包仍从下一帧开始解析
修复前失败证据:新增 HybridCrypt 动态包 fixture 后,暂时移除可选包通知路径运行
net_classic_session_test,失败 2 项:
FAIL: HybridCrypt packet raises an explicit deployment guardFAIL: 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_testbuild/extension/net_classic_stream_testbuild/extension/net_loopback_testproject/app_flow_lifecycle_test.gdproject/p10_test.gd
上述测试全部通过,证明当前握手、认证 happy path、半包 framing、DirectEnter 基础路径、Loading 遮罩和 AppFlow 生命周期没有回归;但它们没有覆盖真实账号服务端的失败/重试包序。
确认的结构差异:
ClassicStream已有 3 秒 TCP connect deadline。重新核对 40250 后,m_dwChangingPhaseTime只在 phase setter 中记录时间,没有发现实际比较/超时分支;因此不能把“缺少认证、登录、选人和 Loading phase deadline”登记为参考差异。认证/握手/Loading 静默失败矩阵仍需补测试。- 40250
SetLoginPhase在发送CG_LOGIN/CG_LOGIN2/CG_LOGIN3后立即ClearLoginInfo();当前实现已在CG_LOGIN/CG_LOGIN3成功写入后清除ClassicSession::m_pw,并同步清除M2Client::cfg_pw的 retry cache。 - 40250
PHASE_CLOSE通过ClosePhase/phase leave 回到 Login 或 Offline 语义;当前实现已用phase_closed区分该路径,清理旧 stream,并由M2Client把登录侧 close 回到 Idle/Login owner,游戏侧 close 发 disconnected 以保留 login-key reconnect。真实 UI 重试和旧 stream 包级 fixture 仍缺。 - 40250 认证阶段会消费并回调 Matrix、Runup Matrix、Passpod、OpenID、Panama Pack、HybridCrypt 等可选包;当前 parser/session 对这些包明确返回 unsupported/error。这不是静默解析,但如果部署启用了任一分支,行为不能与 40250 相同,必须补实现或在部署检查中明确阻断。
- 当前
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_pwretry 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 mtgodotbuild/extension/net_classic_session_testbuild/extension/net_classic_stream_testbuild/extension/net_classic_wire_testbuild/extension/net_loopback_testgodot --headless --path project --script app_flow_lifecycle_test.gdgodot --headless --path project --script p10_test.gdgodot --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 -> SetLoginPhaseCPythonNetworkStream::LoginPhase -> __RecvLoginFailurePacketCAccountConnector::__AuthState_RecvPhaseCAccountConnector::__AuthState_RecvAuthFailureCAccountConnector::__AuthState_RecvAuthSuccess- 当前
ClassicStream::handle_control/dispatch -> ClassicSession::on_phase/on_auth_phase/on_packet/on_auth_packet
修复内容:
PHASE_CLOSE不再直接断开游戏 stream 并进入Failed,而是保留 phase owner、切回LoggingIn并重新执行 LoginPhase;已进入游戏的 M2Client 阶段切换到GameLogin。PHASE_LOGIN和PHASE_AUTH每次收到 phase 都重新执行 40250 对应 handler,因此重复 phase 不会被错误的一次发送保护吞掉。- 游戏侧
GC_LOGIN_FAILURE改为触发登录失败回调并保留LoggingIn,不再把合法的应用层拒绝变成传输失败。 - auth 侧
GC_LOGIN_FAILURE保留 AUTH owner;拒绝的GC_AUTH_SUCCESS报告BESAMEKEY,并在当前 packet 消费后清理 auth stream。 - 登录成功解析会清除旧的失败状态,空角色列表仍按固定槽位进入
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 -> SetLoginPhaseCNetworkStream::Process -> OnConnectFailure -> CPythonNetworkStream::OnConnectFailure- DirectEnter 失败:
OnConnectFailure -> ClosePhase -> SetLoginPhase CPythonNetworkStream::SetLoginPhase -> phaseLeaveFunc -> __LeaveGamePhase -> ClearNetworkActorManager
当前调用链(前一轮修复后、桥接修复前):
ClassicStream::process -> on_remote_disconnect/on_connect_failure -> disconnectClassicSession回调中执行reset_for_map_change -> set_stage(LoggingIn) -> send_login,再让旧 stream 清空缓冲M2Client::pump_classic -> world_reset/disconnected,并用classic_remote_disconnect_pending允许下一次reconnect()真正替换旧 socket
修复内容:
- 建立连接后的 peer close 不再先进入
Failed;断线通知发生在 socket/buffer 清理之前,阶段 owner 回到 LoginPhase。 - pre-game 和 in-game peer close 都清理旧角色/world 状态;in-game 路径保留
disconnected重连入口。 - DirectEnter 的连接失败不再永久停留在
Failed,按 40250 语义回到 LoginPhase;普通 game connect failure 也保留登录 owner并触发登录失败通知,失败 socket 的发送缓冲会在回调前清空。 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 的中文客户端行为。
修复内容:
- 客户端内部继续使用 UTF-8;新增
mtnet::to_wire、from_wire_str和wire_text_fits,Windows 使用 CP936(GB2312 兼容子集),POSIX 使用 GB2312/GBK 转换。固定字段只按转换后的 wire 字节数截断或校验。 - 角色名、公会名、NPC/玩家名、私聊目标、Messenger 名、商店签名、工会 字段和登录固定文本在发送前转 legacy 编码;接收端角色列表、实体、工会、 商店、聊天、私聊、任务和系统文本在进入世界模型前转回 UTF-8。
- 明确 24 字节字段是 wire 容量:12 个常用 GB2312 汉字可完整放入;直接发送 UTF-8 会错误地把容量降到约 8 个汉字。地图名、任务名、系统提示等动态文本 使用自身协议容量,不套用角色名 24 字节限制。
- 同步修正
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 商店签名字段。
修复内容:
- 新增
mtnet::protocol_text_fits(),统一根据 transport 选择 legacy-codepage wire 字节规则或原 m2dev 字节规则。 - 商店签名入口改为按 40250 转换后的 wire 字节校验;角色名、聊天、私聊、公会
等双协议入口同步使用同一个 preflight 规则,发送端仍由
ClassicSession在 固定字段写入前执行 UTF-8 → GB2312 转换。 - 校验边界保持分层:角色/公会/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 矩阵,而是修正实时验证工具的观测边界:
net_classic_e2e现在监听ClassicSession::on_login_failure。这与 40250AccountConnector::__AuthState_RecvAuthFailure和PythonNetworkStream::__RecvLoginFailurePacket一致:WRONGPWD、BESAMEKEY属于应用层拒绝,回调登录 UI,但不进入终态Failed。net_classic_e2e监听已经建立的 game 连接的 remote-disconnect,避免OnRemoteDisconnect -> SetLoginPhase的回退重试包遮盖原始断线原因。- 真实 Ubuntu 40250 服务端验证结果:
- 有效账号:auth -> 四槽
GC_LOGIN_SUCCESS4-> 选角 -> Loading ->PHASE_GAME,退出码 0; - 错误密码:收到
WRONGPWD,立即报告FAIL login callback,退出码 5, 不再误报为 timeout; - auth 成功但目标 game 端口不可用:立即报告
FAIL login callback: connect failed, 退出码 5。
- 有效账号:auth -> 四槽
本轮确认了真实成功包序和两种失败回调的可观测性,没有把失败回调错误改成
终态阶段。真实服务端的空角色账号、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::OnRemoteDisconnectCAccountConnector::OnRemoteDisconnect -> __OfflineState_Set__OfflineState_Set -> __Inialize (STATE_OFFLINE)
当前调用链:
ClassicStream::process -> on_remote_disconnect -> disconnectClassicSession::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。
修复内容:
- 认证端 peer close 清除 auth-success/待清理标记并返回
ClassicSession::Offline, 不把传输断开伪装成应用层WRONGPWD/BESAMEKEY。 M2Client观察到认证 session Offline 时向 Godot 登录 owner 发出login_failed,并把 host 阶段置为可重试的Failed,避免 UI 停在GameLogin;这一层 仍与 40250OnRemoteDisconnect的静默 Offline owner 不等价,后续由独立 bridge 分支修正。- 游戏端 peer close 的
LoggingIn回退路径不改变,仍按 40250OnRemoteDisconnect -> SetLoginPhase清理旧 world 并等待重连。
修复后证据:
net_classic_session_test:通过,新增认证端 peer-close -> Offline owner 断言;mtgodot构建:通过,M2Client Offline-to-login-failure bridge 编译通过;- 原有 auth/game 拒绝、空角色、半包、重复 phase、
PHASE_CLOSE和游戏 peer-close 回归仍需与本轮完整 CTest 一起复跑。
本分支已闭合认证端 peer-close owner 差异,但登录失败后的完整 UI 重试、真实
PHASE_CLOSE/peer-close 包序和 DirectEnter/GC_WARP live 顺序仍未闭合,合同继续
保持 PARTIAL。
Implementation fix round 2026-09-21 — auth peer-close bridge owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-peer-close-bridge-owner。
40250 参考调用链
CNetworkStream::Process
-> CAccountConnector::OnRemoteDisconnect
-> CAccountConnector::__OfflineState_Set
-> __Inialize(STATE_OFFLINE)
认证 socket 被对端关闭时,40250 不调用 OnLoginFailure,也不调用认证连接失败的
OnConnectFailure;它只把 AccountConnector 恢复到 Offline owner,等待 LoginWindow 的
下一次显式连接请求。凭据错误和 TCP peer close 必须保持不同的可观测分支。
当前客户端调用链(修复前)
ClassicStream::process
-> m_auth_stream.on_remote_disconnect
-> ClassicSession::set_stage(Offline)
-> M2Client::pump_classic
-> login_failed -> M2Client Stage::Failed
这会让真实认证 peer close 变成凭据失败,破坏 LoginPhase owner 语义;同时它与已经
修复的认证同步/异步 OnConnectFailure -> connect_failed 分支混淆。
修复前失败证据
新增真实本地 TCP fixture:[project/auth_peer_close_bridge_test.gd],由测试服务器接受 认证连接后主动关闭;修复前输出:
FAIL: auth peer close is not reported as credential login failure
FAIL: auth peer close retains a non-terminal LoginPhase owner
2 check(s) failed
修改内容
ClassicSession增加独立on_auth_remote_disconnectcallback;认证 stream 回到 nativeOffline后只通知该 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.OnLoginFailureintrologin.OnLoginFailure -> connectingDialog.Close -> PopupNotifyMessagePopupNotifyMessage的默认回调SetPasswordEditLineFocus,LoginPhase 窗口本身不重建
当前调用链(修复后):
ClassicParser::on_gc -> ClassicSession::on_packet -> on_login_failureM2Client::pump_classic -> login_failedAppFlow::_on_login_failed -> _focus_login_password -> _set_status
修复前失败证据:在 app_flow_lifecycle_test.gd 增加 LoginPhase 重试断言后,当前实现失败:
login failure keeps the existing LoginPhase UI for retrylogin failure returns focus to the password field
失败原因是 _on_login_failed 无条件调用 _goto_login(),释放并重建整个登录层;这与
40250 只关闭 connecting dialog、保留登录 phase owner 的行为不同,也会使原登录控件上的
重试状态和回调失效。
修复内容:
- LOGIN 阶段收到
login_failed时不再重建_ui,保留同一个登录CanvasLayer和输入控件; SELECT 或其它阶段收到登录失败时仍切回 Login,以保证没有登录 owner 时能恢复入口。 - 为密码框设置稳定节点名
Password,统一由_focus_login_password()调用LineEdit.grab_focus(),并在原 Status 面板显示错误文本。 - 该修复只改变 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 -> SetLoginPhaseSetLoginPhase -> 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_closeM2Client::bind_classic_loading_reset::on_phase_close -> world_resetClassicSession::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。
修复内容:
- 新增
ClassicSession::on_phase_close宿主回调。 PHASE_CLOSE仅在旧 owner 为 Loading/Game 时先执行reset_for_map_change(),再通知宿主, 保证清理发生在 LoginPhase 重入和登录包发送之前;Login/Select owner 保持 40250 的空 leave 语义,不额外清理角色列表。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) -> SetLoginPhaseSetLoginPhase:旧 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 retryPHASE_LOGIN runs the previous GamePhase leave cleanup before retry
修复内容:
- 新增统一的
ClassicSession::enter_login_phase(),让直接PHASE_LOGIN、显式PHASE_CLOSE和 game peer close 共用 40250 的 LoginPhase owner 转换。 - 新增
ClassicParser::clear_select_character_data(),按 40250 普通 Login 分支清除旧 LOGIN_SUCCESS 角色快照和角色事件,但保留固定四槽形状;DirectEnter 保留 slot/ticket 数据。 - 新增通用
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 -> LoadingPhaseLoadingPhase未识别的应用包回退到GamePhaseRecvErrorPacket->ClearRecvBuffer,不关闭当前 socket,也不转移 Loading/Game phase owner
当前调用链(修复后):
ClassicStream::dispatch解析到未知静态 GC headerClassicSession::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_headerphase-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/timeoutClassicSession::m_auth_stream.on_connect_failure清理认证状态并设置Stage::OfflineM2Client::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_packetClassicSession::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) -> ClosePhaseClosePhase -> 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_closeM2Client::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 GAMEgame-side LoginPhase transition shows reconnect overlaygame-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 中只是空观察者,没有给现有重连属主发送断线边界。
修复内容:
- M2Client 在旧宿主为
InGame的显式 PHASE_CLOSE 中,在 world leave/reset 完成后发出一次disconnected,避免重复进入和重复发起重连。 - AppFlow 监听
phase_changed;若游戏态收到login/close且尚未处于重连,则转为同一个 reconnect owner;ReconnectUI 增加统一的show_for_disconnect入口,确保阶段-only 事件与 transport disconnect 使用同一倒计时/重试属主。 - 仅对游戏态处理该兜底;登录/选人/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 -> SetLoginPhaseSetLoginPhase在旧 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 retryLoading 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_warpM2Client::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 armedGC_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::SetSelectPhasenetworkModule.SetSelectCharacterPhaseSelectCharacterWindow.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 -> SetLoginPhasenetworkModule.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 ownerLoadingScreen在 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),再设置Loadingowner;失败时进入Failed并记录发送错误。- Loading timer 随后保持既有的
send_enter_game()顺序,避免用连接地址中的 slot 替代协议包。
修改文件:
extension/src/net/classic/classic_session.cppextension/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无条件调用 PythonSetLoginPhase()。networkmodule.py::SetLoginPhase先net.Disconnect(),再通过SetPhaseWindow(introLogin.LoginWindow(...))关闭旧 phase window、打开 LoginWindow。- 因此 peer close 和
RecvPhasePacket(PHASE_CLOSE) -> ClosePhase都以 LoginPhase 为唯一 UI 属主;旧 GameWindow/地图对象不在登录窗口后面保留,角色列表也不会自动重入旧 slot。
修复前差异和失败证据
当前 AppFlow::_on_disconnected 在 GAME 且存在 GameScene 时设置 _reconnecting_game、显示
ReconnectUI 并等待角色列表自动重入;_on_phase_changed 对 GAME 的 LoginPhase 事件也走同一
重连分支。这与 40250 无条件 SetLoginPhase 不一致。
先把 project/app_flow_lifecycle_test.gd 的断言改为 40250 行为,再未修改生产代码运行:
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10
PRE_FIX_RC=1
FAIL: game peer close returns to LoginPhase and releases GameScene
FAIL: game peer close does not retain a reconnect overlay
FAIL: post-disconnect character list enters SELECT without phantom re-entry
FAIL: game-side PHASE_CLOSE transition returns to LoginPhase owner
FAIL: empty character list returns to the SELECT owner
修复内容
project/app_flow.gd的断线处理统一释放 GameScene、清除重连状态并进入 LoginPhase。- AppFlow 不再把
ReconnectUI挂接到disconnected,避免 LoginPhase 已接管后又被第二个重连属主覆盖。 PHASE_CLOSE/phase-onlylogin事件与 peer close 使用同一 LoginPhase owner;非游戏阶段仍可从 SELECT/Loading 回到 Login,游戏阶段不再保留旧场景。- 角色列表按普通 Login/Select 流程处理,不再由断线分支自动发送旧角色 slot;空列表保持 SELECT owner。
- native
M2Client的 login key 和显式reconnect()能力保留,但不再被 AppFlow 的自动重连遮罩消费。
修复后验证
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10:PASS。godot --headless --path project --script p10_test.gd --quit-after 10:PASS。- 重新构建
mtgodot和 release macOS 包:PASS。 - 真实 40250 Godot live smoke,
MT_SMOKE_PHASE_COMMAND=/logout:PHASE_GAME -> world_reset -> stage=game_login -> peer closed -> phase=login,报告验证 LoginPhase owner、GameScene 已释放、未保留 reconnect owner:PASS。 - live smoke 外层脚本的功能报告为 PASS,但退出日志仍触发现有的 4 个
ParticlesShaderRD/Material RID leak gate;该渲染资源泄漏与本分支的网络/phase owner 行为无关, 因此不能把外层 shell 的最终结果记为全量 PASS。
本分支已闭合当前客户端的 peer-close/游戏态 phase-only LoginPhase owner 差异;合同仍为
PARTIAL,因为真实服务端主动 PHASE_CLOSE 包序、空角色 fixture、完整跨阶段 retry、重复进入
live 包序和可选安全 runtime 尚未取证。
Implementation fix round 2026-09-21 — LoginPhase retry transport owner
问题/分支 ID:network.login.phase_flow/login-failure-retry-cleanup/login-form-retry-owner。
40250 参考调用链
intrologin.LoginWindow.__OnClickLoginButton -> LoginWindow.Connect(id, pwd)。MainStream.SetLoginInfo -> MainStream.Connect。networkmodule.Connect -> net.ConnectToAccountServer。CAccountConnector::Connect先执行__OfflineState_Set,再由CNetworkStream::Connect清理并替换 AccountConnector 自己的 socket;它不会走CPythonNetworkStream::OnRemoteDisconnect,也不会执行 Game/World 的全局清理。- 新认证连接收到
PHASE_AUTH后重新发送一份CG_LOGIN3;认证成功再由CPythonNetworkStream::Connect清理/替换游戏连接。
修复前失败证据
先在 project/app_flow_lifecycle_test.gd 增加登录失败后再次点击连接的断言,再未修改生产代码运行:
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10
FAIL: login form retry replaces only the authentication transport
PRE_FIX_RC=1
失败原因是 _do_connect 只有 connect_to_server 入口,而
M2Client::connect_to_server 开头无条件调用 disconnect_from_server(),会销毁整个
ClassicSession 并发出 world_reset;这不是 40250 的 LoginWindow/AccountConnector 重试边界。
修改内容
ClassicSession增加login_retry_pending和retry_login。认证应用失败、认证连接失败、认证 peer close、游戏登录失败都会标记一个可显式重试的 LoginPhase owner。M2Client::retry_login在存在待重试 classic session 时更新新输入的账号/密码,保留现有回调和 session,只调用ClassicSession::retry_login替换认证流;不调用全局disconnect_from_server(),不重复发出world_reset。AppFlow::_do_connect优先调用retry_login;无待重试 owner 时由 M2Client 回退到初始完整连接。- 首次登录、非 LoginPhase 的手动连接和已有 GC_WARP/peer-close 的其他重连路径保持原有入口,不 把本修复扩散到飞行物、技能、战斗或表现系统。
修复后验证与边界
app_flow_lifecycle_test.gd:修复后 PASS,确认 LoginPhase 重试调用retry_login,不走 fake client 的完整connect_to_server。net_classic_session_test:loopback fixture PASS,确认失败后旧认证 socket 被替换,新的PHASE_AUTH只产生一份CG_LOGIN3,并使用重新输入的密码。- 真实 Windows LoginWindow 可视化重试和真实服务端“失败->重新输入->成功”的完整包序仍未取证;
因此本分支证明的是当前 native/UI retry owner 和 socket-free/loopback 等价行为,合同继续保持
PARTIAL。
Implementation fix round 2026-09-21 — explicit PHASE_CLOSE transport-owner separation
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/phase-close-server-fixture。
40250 参考语义
PythonNetworkStream::RecvPhasePacket收到PHASE_CLOSE后调用ClosePhase()。ClosePhase()通过SetLoginPhase()回到 LoginPhase;它与PythonNetworkStreamEvent::OnRemoteDisconnect()是两条不同的来源链。- 因此显式服务器阶段关闭只改变 phase/UI owner,不能被桥接成 peer close,也不能触发远程断线重试属主。
修复前失败证据
在不改生产代码的情况下,先把 project/app_flow_lifecycle_test.gd 增加到 40250 语义:检查
M2Client::on_phase_close 的 callback body 不得设置
classic_remote_disconnect_pending,也不得发出 disconnected。修复前结果:
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10
FAIL: explicit PHASE_CLOSE does not synthesize transport disconnected
PRE_FIX_RC=1
修复内容
extension/src/net/m2_client.cpp::bind_classic_loading_reset的on_phase_close只保留 语义注释,不再设置classic_remote_disconnect_pending,不再发出disconnected。pump_classic在显式phase_closed()后即使重新进入 LoginPhase 的发送失败,也统一发出login_failed;不再用was_online_lost把阶段关闭误报为传输断线。- LoginPhase 的 owner 仍由
ClassicSession阶段变化和 AppFlow 的phase_changed("login")接管; Loading/Game 的旧 world 清理由已有 phase-leave 回调完成。
修复后验证
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10:PASS。cmake --build build-debug --target net_classic_session_test net_classic_stream_test net_loopback_test mtgodot -j4:PASS。net_classic_session_test、net_classic_stream_test、net_loopback_test:PASS。ctest --test-dir build-debug --output-on-failure -j2:23/23 PASS。p10_test.gd、netplay_test.gd、disconnect_world_reset_test.gd:PASS/exit 0。
本分支闭合了当前 native bridge 将显式 PHASE_CLOSE 错报为 transport disconnect 的结构差异,
但没有伪造真实服务器证据;真实主动 PHASE_CLOSE 包序、空角色 live fixture、完整跨阶段 retry
和重复进入 live 包序仍使合同保持 PARTIAL。
Implementation hardening round 2026-09-21 — empty-role live probe
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture。
40250 参考语义
PythonNetworkStreamPhaseLogin::__RecvLoginSuccessPacket3/4将固定槽位数组复制到客户端, 空槽保持零初始化;该包不是登录失败。- 随后的
RecvPhasePacket(PHASE_SELECT) -> SetSelectPhase只切换到 Select owner;非 DirectEnter 不会因为所有槽位为空而发送CG_PLAYER_SELECT,选人窗口继续负责建号/选槽。
当前实现与缺口
ClassicParser::parse_login_success同样固定产生四槽数组,ClassicSession::on_phase(PHASE_SELECT)进入CharSelect并回调空列表,AppFlow::_on_char_list只进入 SELECT,不自动选择槽位;本地行为已由net_classic_session_test和app_flow_lifecycle_test.gd覆盖。- 原
extension/tools/net_classic_e2e.cpp在没有可选角色时直接返回“account has no selectable character”退出码 3,无法区分合法全空账号和登录失败,阻塞了真实空角色包序审计。
修复前失败证据
先在 project/app_flow_lifecycle_test.gd 加入 live-probe 合同断言,再未修改探针运行:
godot --headless --path project --script app_flow_lifecycle_test.gd --quit-after 10
FAIL: live probe treats an all-empty character list as a valid SelectPhase result
PRE_FIX_RC=1
修改内容
net_classic_e2e增加MT_CLASSIC_EXPECT_EMPTY=1模式。- 该模式要求所有固定槽位为空,观察到
CharSelect后输出PASS EMPTY CHARACTER LIST PHASE_SELECT并立即断开;若发现占用槽位则返回失败。 - 非该模式仍保持原有选择第一个可用角色的 live 流程,避免改变常规成功探针。
验证与边界
- 修复后
app_flow_lifecycle_test.gd:PASS。 cmake --build build-debug --target net_classic_e2e mtgodot -j4:PASS。- 当前 40250 live 账号在新模式下正确拒绝为“全空”(槽 0 有角色、槽 1–3 为空),退出码 3; 这证明模式不会误报部分空列表为全空,但不是全空账号的正向证据。
- 本地完整网络回归继续覆盖空
LOGIN_SUCCESS4、半包、重复 phase 和 SELECT owner;全空角色 live 仍需安全的无角色账号或等价真实服务器 fixture,合同保持PARTIAL。
Implementation fix round 2026-09-21 — stale auth phase after auth-success
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order。
40250 参考调用链
CAccountConnector::Process
-> __AuthState_Process
-> __AuthState_RecvAuthSuccess
-> CAccountConnector::Disconnect
-> CNetworkStream::Clear
-> 清空旧 AccountConnector 接收缓冲
认证成功处理在 AccountConnector.cpp:451-476 内立即断开认证连接;因此同一次 TCP
读取中跟在 GC_AUTH_SUCCESS 后面的旧 PHASE_AUTH 不会再次进入
__AuthState_RecvPhase,也不会发送第二份 CG_LOGIN3。认证失败的
__AuthState_RecvAuthFailure 则不主动断开,允许 LoginWindow 在原 AUTH owner 上等待用户重试;
这两个分支不能合并成“所有失败包都清缓冲”。
当前客户端调用链(修复前)
ClassicStream::feed/process
-> ClassicStream::dispatch
-> ClassicSession::on_auth_packet(GC_AUTH_SUCCESS)
-> 仅设置 m_auth_succeeded / m_auth_close_after_packet
-> dispatch 继续消费同一接收缓冲
-> ClassicSession::on_auth_phase(PHASE_AUTH)
-> send_auth_login
-> 第二份 CG_LOGIN3
这与 40250 的旧认证连接边界不等价;pump() 后续才断开认证流,已经晚于同一缓冲区中
迟到 phase 的处理。
修复前失败证据
���生产修改前加入 loopback fixture,向同一个认证流一次性送入
GC_AUTH_SUCCESS + PHASE_AUTH:
build-debug/extension/net_classic_session_test
FAIL: auth-success clears a pipelined stale PHASE_AUTH without a duplicate CG_LOGIN3
1 check(s) failed
修改内容
ClassicSession::on_auth_packet在接受或拒绝的GC_AUTH_SUCCESS当前帧处理完成后,调用m_auth_stream.clear_remaining_after_current();ClassicStream先消费当前完整帧,再清空其余旧 AccountConnector 接收缓冲。- 认证应用失败
GC_LOGIN_FAILURE不调用该清理,继续保留 40250 的 LoginWindow/AUTH owner, 让后续显式retry_login替换认证 transport。 - 没有改变
PHASE_AUTH正常包、认证失败包或游戏流的通用 phase 分支;本轮只修复认证成功后的 旧连接尾包边界,不涉及飞行物、技能、战斗或 UI。
修复后验证与边界
net_classic_session_test:PASS;认证成功后同一 burst 中的迟到PHASE_AUTH被丢弃,认证流 不产生第二份CG_LOGIN3。- 认证失败后的原 AUTH owner、重复
PHASE_AUTH、半包、peer close、登录表单 retry 和游戏阶段 phase 清理回归继续 PASS。 - 真实 Windows/真实服务端认证成功后管线化尾包无法直接注入,完整跨阶段 UI retry、游戏登录失败
后的迟到 game phase、真实重复进入包序和服务器主动
PHASE_CLOSElive 证据仍未闭合;合同继续 保持PARTIAL,下一步应处理独立的 game-login stale-tail 分支,避免重复修改本轮认证 owner。
Implementation fix round 2026-09-21 — game LoginPhase stale tail
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order-game-login-stale-tail。
40250 参考调用链
CPythonNetworkStream::OnProcess
-> m_phaseProcessFunc.Run()
-> CPythonNetworkStream::LoginPhase
-> CheckPacket
-> __RecvLoginFailurePacket
-> LoginWindow.OnLoginFailure
-> return
LoginPhase() 每次处理一个当前应用包;GC_LOGIN_FAILURE 的回调完成后立即返回,
因此同一 TCP 读取中管线化的 PHASE_LOGIN 不会在本次 tick 继续执行,而是留给下一次
OnProcess。SelectPhase() 和 Loading 阶段自己的包也使用同样的 phase-local return
边界;Loading 转交 GamePhase() 的游戏包不套用该单包限制。
当前客户端调用链(修复前)
ClassicStream::feed/process
-> ClassicStream::dispatch
-> ClassicSession::on_packet(GC_LOGIN_FAILURE)
-> dispatch 继续循环同一 receive buffer
-> ClassicSession::on_phase(PHASE_LOGIN)
-> 立即再次发送 CG_LOGIN
这会把 40250 的两个 OnProcess 周期压缩成一个周期,造成登录失败回调和新的登录阶段
包顺序不一致;同样的通用分发问题还会让 Select/Loading 自有包越过 phase-local return。
修复前失败证据
在生产修改前加入 net_classic_session_test loopback/socket-free fixture,向游戏流一次性
送入 GC_LOGIN_FAILURE + PHASE_LOGIN:
build-debug/extension/net_classic_session_test
FAIL: LoginPhase stops after login failure before a pipelined PHASE_LOGIN
FAIL: deferred PHASE_LOGIN is processed on the next tick
2 check(s) failed
修改内容
ClassicStream增加return_after_current_packet(),应用包当前帧消费完成后返回,保留未读 receive tail 到下一次process/feed;disconnect、错误清理和clear_remaining_after_current路径会同步清除该一次性边界标记。ClassicSession::on_packet在 LoginPhase、SelectPhase、DirectEnter 的 Select owner 和 Loading 自有包成功处理后设置该边界;Loading 转交 GamePhase 的包继续由游戏批处理路径处理。- 测试补充登录失败后迟到
PHASE_LOGIN的“本次不重发、下次才重试”断言,并将已有选择阶段 的填充字节/第二包测试按 40250 的下一 tick 包序执行。
修复后验证与边界
net_classic_session_test:PASS;登录失败 burst 不再立即产生第二份CG_LOGIN,下一次 feed 才执行保留的PHASE_LOGIN。- 该轮只修复 phase-local 应用包边界,不改变 GamePhase 的三包/安全缓冲批处理,也不涉及 飞行物、技能、战斗或 UI。
- 真实 Windows/真实服务端的失败后重新输入成功、重复进入包序、服务器主动
PHASE_CLOSE和全量 live retry 仍未闭合,合同保持PARTIAL;后续不得重复修改本分支的单包返回机制。
Implementation fix round 2026-09-21 — synchronous auth connect failure owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-connect-immediate-failure-owner。
40250 参考调用链
CAccountConnector::Connect
-> CNetworkStream::Connect
-> connect/socket immediate error
-> CNetworkStream::Clear
-> CAccountConnector::OnConnectFailure
-> LoginWindow.OnConnectFailure
-> CAccountConnector::__OfflineState_Set
CNetworkStream::Connect 在 socket 创建或同步 connect() 错误时会主动调用
OnConnectFailure 后返回 false;CAccountConnector::OnConnectFailure 仍把认证 owner
置为 Offline。该状态允许登录窗口保留并再次调用 Connect,不能把地址解析/同步连接失败
当作协议失败进入终态 Failed。
当前客户端调用链(修复前)
ClassicSession::connect(auth_host, auth_port, ...)
-> ClassicStream::connect
-> getaddrinfo/socket immediate error
-> return false
-> ClassicSession::connect 直接 set_stage(Failed)
当前 ClassicStream::connect 的异步失败已经通过 on_connect_failure 回调处理,但同步
失败直接返回,未进入同一个认证 owner 清理路径;因此同步与异步错误的阶段结果不一致。
修复前失败证据
在生产修改前加入无效认证地址测试:
build-debug/extension/net_classic_session_test
FAIL: synchronous auth connect failure returns to the 40250 Offline owner
FAIL: synchronous auth connect failure keeps the LoginPhase retry owner
2 check(s) failed
修改内容
ClassicSession增加handle_auth_connect_failure(),统一清理认证成功标记、游戏启动标记、 关闭待处理标记、phase-close/loading/enter-game 状态,并设置m_login_retry_pending后 返回Stage::Offline。m_auth_stream.on_connect_failure和认证connect()的同步失败共同调用该函数;没有改变 游戏连接失败、协议包失败或全局 world teardown 的处理。net_classic_session_test覆盖无效认证地址的返回值、Offline owner 和可重试标记。
修复后验证与边界
net_classic_session_test:PASS;同步认证失败返回false,阶段为Offline,并保持可重试 的 LoginPhase owner。- 真实 Windows UI 的
LoginWindow.OnConnectFailure可视化显示、DNS/WinSock 错误文本和真实 服务端重试仍未取证;合同保持PARTIAL。后续不得重复修改本分支的同步/异步认证 owner 合并机制。
Implementation fix round 2026-09-21 — game connect immediate failure owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/game-connect-immediate-failure-owner。
40250 参考调用链
CNetworkStream::Connect / CNetworkStream::Process
-> Clear()
-> CPythonNetworkStream::OnConnectFailure()
-> if DirectEnter: ClosePhase() -> SetLoginPhase()
-> else: LoginWindow.OnConnectFailure()
PythonNetworkStreamPhaseLogin.cpp:211-221 明确把游戏连接失败与
__RecvLoginFailurePacket -> LoginWindow.OnLoginFailure 分开。普通登录路径只通知
LoginWindow.OnConnectFailure 并保留登录窗口作为重试属主;DirectEnter 路径经过
ClosePhase -> SetLoginPhase,不能伪造成凭据错误或普通 peer-close。
当前客户端调用链(修复前)
ClassicStream::connect / ClassicStream::process
-> ClassicSession::connect* / pump
-> handle_game_connect_failure(bool)
-> ordinary: set LoggingIn + on_login_failure(last_error)
-> DirectEnter: set LoggingIn, but no enter_login_phase(true)/on_phase_close
因此普通认证成功后游戏地址拒绝会触发错误的 login_failed,DirectEnter 的同步或异步
连接失败也不会走 40250 的 ClosePhase observer;GC_WARP 的桥接层还会额外伪造
disconnected。
修复前失败证据
在生产修改前加入两个 loopback/无效地址 fixture:
build-debug/extension/net_classic_session_test
FAIL: DirectEnter connect failure follows OnConnectFailure -> ClosePhase
FAIL: DirectEnter connect failure is not reported as credential login failure
2 check(s) failed
build-debug/extension/net_classic_session_test
FAIL: ordinary game-connect failure is not reported as credential login failure
1 check(s) failed
前一个 fixture 覆盖 DirectEnter 连接失败,后一个完整驱动
PHASE_AUTH -> CG_LOGIN3 -> GC_AUTH_SUCCESS -> game connect,证明差异来自跨阶段
game owner,而不是单独调用连接函数。
修改内容
ClassicSession::handle_game_connect_failure()合并同步Connect和异步Process失败入口,先清理失败 transport,再保留m_login_retry_pending。- DirectEnter 改为
enter_login_phase(true),触发 phase-leave/on_phase_close,并明确不调用on_login_failure;无 socket 时丢弃不可发送的暂存登录字节,显式 retry 再建立新 transport。 - 普通游戏连接失败新增独立
on_connect_failurecallback;M2Client暴露connect_failedsignal,AppFlow/独立登录入口只更新 LoginPhase 状态和密码框焦点,保持 40250 的OnConnectFailure与凭据失败事件分离。 M2Client的快速重连和 GC_WARP DirectEnter 失败不再伪造disconnected,交给 nativeClosePhase -> SetLoginPhaseowner;没有改变已经建立连接后的真实 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.pyPASS,audit_packet_registry.py保持既有单项HDR_GC_SYMBOL_DATAplatform adaptation 的 PARTIAL。 - 真实 Windows
LoginWindow.OnConnectFailure视觉表现、真实 DirectEnter/GC_WARP 失败后的 服务器回退包序,以及初始认证同步失败在M2Client::connect_to_server中仍可能被桥接为 终态Failed的独立 auth bridge 分支,尚未在本轮关闭;合同保持PARTIAL。后续选择新的auth-connect-bridge-owner或 live failure package-order 分支,不重复修改本轮 nativehandle_game_connect_failure()。
Implementation fix round 2026-09-21 — auth connect bridge owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-connect-bridge-owner。
40250 参考调用链
CAccountConnector::Connect
-> CNetworkStream::Connect
-> immediate socket/DNS failure or Process connect failure
-> Clear()
-> CAccountConnector::OnConnectFailure()
-> LoginWindow.OnConnectFailure()
-> __OfflineState_Set()
认证连接失败属于 OnConnectFailure/Offline owner,不属于凭据校验失败
OnLoginFailure。CAccountConnector 清理的是认证 transport,但保留登录窗口作为显式重试
属主;因此重试应重新建立认证连接,不应把整个登录流程置为终态失败。
当前客户端调用链(修复前)
同步失败路径:
M2Client::connect_to_server
-> ClassicSession::connect(auth)
-> ClassicStream::connect() == false
-> ClassicSession::handle_auth_connect_failure() -> native Offline
-> M2Client false branch -> login_failed -> Failed -> classic_sess.reset()
异步失败路径也存在同一桥接问题:
ClassicSession::pump()
-> auth stream OnConnectFailure
-> native Offline
-> M2Client::pump_classic()
-> login_failed -> Failed
这会丢失 40250 AccountConnector 的认证 Offline owner,使 LoginWindow.Connect 无法在
原认证 owner 上进行第二次尝试;同时还把连接不可达错误误报为凭据登录失败。
修复前失败证据
新增专项回归 project/auth_connect_bridge_test.gd,修复前关键断言失败:
FAIL: initial auth connect failure reaches the distinct OnConnectFailure bridge
FAIL: initial auth connect failure does not enter terminal Failed stage
FAIL: retry_login replaces only the auth transport after auth connect failure
FAIL: auth connect retry does not enter terminal Failed stage
4 check(s) failed
修改内容
ClassicSession新增on_auth_connect_failure,在认证连接失败回到 nativeStage::Offline前通知上层,保持 40250 的OnConnectFailure -> Offline顺序;它与on_login_failure、游戏连接失败 callback 分离。M2Client新增classic_auth_connect_failure_pending,把该 callback 映射为独立的connect_failedsignal 和非终态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_WARPClosePhase或真实 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;CTest23/23PASS。ASAN_OPTIONS=detect_leaks=0 build-asan/extension/net_classic_session_test:PASS; Godotapp_flow_lifecycle_test.gd、p10_test.gd、netplay_test.gd、disconnect_world_reset_test.gd和阶段/包结构审计通过。- 修复后的本地桥接 owner 已关闭本分支;真实 Windows
LoginWindow.OnConnectFailure视觉 文本、真实服务端失败后重新输入成功的包序、服务器主动PHASE_CLOSElive 包序、完整跨阶段 retry 和其它可选安全 runtime 仍未取证,合同继续保持PARTIAL。后续应选择新的 live package-order 分支,不重复修改本分支的认证 callback、pending 标记或 session 保留逻辑。
Live evidence refresh 2026-09-21 — server-initiated phase-close probe
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close。
本轮是证据刷新,不是生产代码修复。分支此前没有历史记录,本次探针结果已经登记,后续在
服务端 fixture 不变的情况下不得重复执行同一个 /logout 探针。
40250 参考调用链
GM command "logout"
-> SCMD_LOGOUT
-> CHARACTER::Logout / timed logout event
-> DESC::SetPhase(PHASE_CLOSE)
-> close input processor / transport close boundary
源码审计确认 cmd.cpp 将 logout 映射到 SCMD_LOGOUT,cmd_general.cpp 的 logout
分支最终调用 DESC::SetPhase(PHASE_CLOSE);desc.cpp 在设置 phase 后切换到
m_inputClose。这证明服务端有主动关闭阶段的参考状态链,但不自动证明客户端一定收到一个
可观察的 HEADER_GC_PHASE(PHASE_CLOSE) 包。
当前客户端与真实服务端观察
使用配置的 40250 Ubuntu 服务端和 Godot live smoke:
auth -> PHASE_LOGIN -> PHASE_SELECT -> PHASE_LOADING -> PHASE_GAME
-> MT_SMOKE_PHASE_COMMAND=/logout
-> peer close / MAIN_LOST
-> world_reset -> LoginPhase owner
功能报告为 PASS:角色进入游戏、执行 /logout、释放 GameScene、回到唯一 LoginPhase
owner 均成功。开启 MT_NET_TRACE=1 后,实时 phase 记录为
1,2,3,1,2,3,4,5,随后只有 DISCONNECTED peer closed;没有观察到
[classic:game] phase -> 0,即没有观察到客户端收到 GC_PHASE(PHASE_CLOSE)。
对照服务端 desc.cpp 可见,DESC::SetPhase 先把 m_iPhase 设为 PHASE_CLOSE,而
DESC::Packet 在 m_iPhase == PHASE_CLOSE 时直接返回;因此当前服务端 build 的 /logout
路径实际提供的是 peer-close 证据,而不是可区分的客户端 PHASE_CLOSE 包 fixture。这个
服务端实现边界不应通过修改客户端来“补齐”或伪造。
外层 smoke wrapper 仍因已有的 4 个 ParticlesShaderRD/RID 泄漏门禁返回失败,但报告中的
网络功能状态为 PASS;该渲染资源泄漏与 PHASE_CLOSE 包是否发送是两个独立问题。
结论与未闭合边界
- 当前客户端对“服务端关闭连接后回到 LoginPhase owner”的可观察行为已取得真实证据,且与
已关闭的 peer-close owner 分支一致;本轮没有发现需要再次修改
ClassicSession或AppFlow的新差异。 - 不能把本轮结果记为真实服务器主动
PHASE_CLOSE包序通过,因为 trace 中没有 phase 0。 - 该子分支保持
PARTIAL。要继续关闭它,必须提供一个真正发送客户端可观察GC_PHASE(PHASE_CLOSE)的 40250 server build/fixture,并在同一 fixture 上验证失败后重新 输入成功、完整跨阶段 retry 以及 DirectEnter/GC_WARP 失败回退包序;在 fixture 改变前不重复/logout运行或修改已关闭的 close/peer-close owner。
Implementation fix round 2026-09-21 — auth-success transport handoff order
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-success-game-before-auth-clear。
40250 参考调用链
CAccountConnector::__AuthState_RecvAuthSuccess
-> CPythonNetworkStream::SetLoginKey
-> CPythonNetworkStream::Connect(game address)
-> CAccountConnector::Disconnect
-> CAccountConnector::__OfflineState_Set
AccountConnector.cpp 的成功认证分支先把 login key 交给主网络流并启动游戏连接,随后才
清理 AccountConnector。这里是两个独立 transport owner 的交接边界;不能先清空认证 owner
再建立主连接,否则失败/重试期间的可观察顺序与 40250 不同。
当前客户端调用链(修复前)
ClassicSession::pump
-> m_auth_stream.disconnect()
-> m_stream.connect(game_host, game_port)
修复前认证 transport 的 Offline 状态先于游戏 transport 的 Connecting 状态发生,和
40250 的主连接先启动、认证连接后清理相反。
修复前失败证据
在生产修改前,net_classic_session_test 增加两个 transport 的状态观察 fixture,关键断言
失败:
FAIL: auth success starts game transport before clearing auth transport like 40250
1 check(s) failed
修改内容
ClassicSession::pump()在认证成功交接中先调用m_stream.connect(),再调用m_auth_stream.disconnect();共享的 transition guard 仍覆盖两个清理/连接回调,未改变 游戏连接失败、peer close 或登录重试 owner。extension/tests/net_classic_session_test.cpp增加真实 loopback 双 transport fixture, 验证game:connecting -> auth:offline的顺序,并继续接受替换后的游戏连接,覆盖连接成功、 认证清理和旧 transport 边界。
修复后验证与边界
- 修复后
net_classic_session_test:PASS;认证成功交接严格观察到先建立 game transport, 再清理 auth transport。 - 本轮只改认证成功的 transport handoff 顺序,没有修改飞行物、技能、战斗或 UI,也没有重开 已关闭的 peer-close/PHASE_CLOSE owner 分支。
- 真实 Windows/真实服务端失败后重新输入成功、完整跨阶段 retry、全空角色 live、服务器主动
PHASE_CLOSE包序和 DirectEnter/GC_WARP 失败回退仍未闭合;合同保持PARTIAL。下一步应 继续取新的失败重试或服务端 fixture 证据,不重复本轮 handoff 顺序。
Implementation fix round 2026-09-21 — game-login retry process boundary
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/game-login-retry-process-boundary。
40250 参考调用链
CPythonApplication::Process
-> CPythonNetworkStream::Process
-> CAccountConnector::Process
-> CAccountConnector::__AuthState_RecvAuthSuccess
-> CPythonNetworkStream::Connect(game address)
-> return from the current application frame
-> next frame: CPythonNetworkStream::Process(new game transport)
40250 的登录表单重试只替换 AccountConnector transport;游戏主流仍是同一个
CPythonNetworkStream。因此在“游戏 LoginPhase 失败、等待重新输入”期间,主流先按
CPythonApplication::Process 的顺序处理,随后才处理 AccountConnector。认证成功时主流的
Connect() 发生在主流本帧的处理机会之后,新主流不能在同一帧继续消费 PHASE_LOGIN。
当前客户端调用链(修复前)
ClassicSession::pump
-> auth_stream.process()
-> m_stream.connect(replacement game transport)
-> m_stream.process() // 同一帧再次处理新主流
-> consume replacement PHASE_LOGIN
这会把替换连接上的阶段包提前到认证成功的同一帧处理,改变 40250 的阶段/事件边界;同时 游戏登录失败后的旧主流没有明确保留“在 AccountConnector 之前处理”的语义。
修复前失败证据
在生产代码修改前,net_classic_session_test 增加 loopback 重试 fixture:旧游戏流收到
GC_LOGIN_FAILURE 后保留,认证成功回调期间替换服务器同步发送 PHASE_LOGIN。修复前输出:
FAIL: auth success starts the replacement game transport without processing it in the same frame
1 check(s) failed
修改内容
ClassicSession增加m_auth_retry_with_existing_game_stream,只在重试确实仍有旧主流 transport 时保留该处理语义;认证失败/初次认证/已清理的连接不会误处理旧 socket。ClassicSession::pump()调整为先处理当前有效游戏流,再处理认证流,符合CPythonApplication::Process的主流→AccountConnector 顺序。- 认证成功建立替换游戏 transport 后,本次 pump 设置 handoff 边界并跳过新主流处理;下一次
pump 才消费替换连接的
PHASE_LOGIN,并保留旧 transport 在认证成功前可处理、成功后由ClassicStream::connect()清理的 40250 语义。 - 新增
net_classic_session_test,覆盖游戏 LoginPhase 失败、同 session auth retry、旧流保留、 auth success、替换 game transport 和下一帧才处理PHASE_LOGIN。
修复后验证与边界
net_classic_session_test:PASS;第一帧保持Connecting,第二帧才进入LoggingIn。net_classic_stream_test、net_loopback_test、net_entity_test:PASS;CTest23/23PASS。- ASAN
net_classic_session_test:PASS;Godotapp_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_CLOSElive 和 DirectEnter/GC_WARP 失败回退仍未闭合;合同继续保持PARTIAL。
Implementation fix round 2026-09-21 — GB2312 UI input boundary
问题/分支 ID:network.login.phase_flow/legacy-text-wire-encoding/ui-gb2312-input-capacity。
差异
40250 的角色名字段容量是 24 个 GB2312 wire 字节;“龙驹简体中文免费版”虽然占 27 个
UTF-8 字节,但只占 18 个 GB2312 字节,应该可以进入创建/改名流程。native
ClassicSession 已在发包前转换,但选人页 CharSelectScreen 仍按 UTF-8 字节数把输入框
限制为 24 字节,导致合法名称在到达 native 转码前被错误拒绝。
修改
project/ui/char_select_screen.gd改为只限制 Unicode 字符数量和控制字符;输入框不再按 UTF-8 字节截断,最终 24-byte GB2312 可表示性由M2Client/ClassicSession统一校验。- 创建和改名输入框使用 24 个 Unicode 字符作为编辑上限,提示文本明确中文角色名实际按 GB2312 wire 字节校验;这不会把 13 个中文字符误判为合法,native 仍会拒绝超过 24 GB2312 字节的名称。
net_text_codec_test增加“龙驹简体中文免费版”的 27 UTF-8 / 18 GB2312 往返断言;char_create_delete_test.gd增加选人页不提前拒绝该名称的回归。
验证
build-debug/extension/net_text_codec_test:PASS。build-debug/extension/net_classic_session_test:PASS;固定角色名创建/改名仍按 GB2312 字节写入 24-byte 字段并保留 NUL。MT_PROTOCOL=classic godot --headless --path project --script char_create_delete_test.gd: PASS;UI 保留“龙驹简体中文免费版”并交给创建接口。
本轮只修复 UI 输入层与 GB2312 wire 边界的重复校验差异,没有把地图名、任务名或系统提示
错误套用 24-byte 角色名限制;合同仍保持 PARTIAL,其它 legacy/可选安全和真实服务端
中文建号包序证据仍未闭合。
Implementation fix round 2026-09-21 — Loading EnterGame event boundary
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/loading-enter-game-event-boundary。
40250 参考调用链
CPythonNetworkStream::SetLoadingPhase
-> LoadingPhase
-> RecvMainCharacter / RecvMainCharacter3_BGM / RecvMainCharacter4_BGM_VOL
-> networkmodule.SetLoadingPhase 的 LoadingWindow/GameWindow.LoadData
-> net.SendEnterGamePacket
-> CPythonNetworkStream::SendEnterGame
introloading.py::LoadingWindow.Open() 在 DirectEnter 的 PHASE_SELECT 阶段发送
CG_PLAYER_SELECT;真正收到 GC_MAIN_CHARACTER 后,game.py::GameWindow.LoadData() 的最后
一步才发送 CG_ENTERGAME。参考实现没有在 PHASE_LOADING 进入后按墙钟延迟发送该包。
当前客户端差异(修复前)
ClassicSession::on_phase(PHASE_LOADING)
-> m_loading_since = m_now
-> pump() 等待 m_entergame_delay(M2Client 默认 1500ms)
-> enter_game()
这会让 CG_ENTERGAME 脱离 GC_MAIN_CHARACTER/LoadingWindow 的事件边界,在主角包尚未到达时
可能过早发送,或在主角包已经完成加载后仍额外等待;也无法证明与 40250 的 GameWindow 初始化
顺序一致。
修复前失败证据
先加入专项测试并在生产修复前运行:
build-debug/extension/net_classic_session_test
FAIL: GC_MAIN_CHARACTER triggers version then CG_ENTERGAME in the same loading event
1 check(s) failed
测试关闭兼容定时器后注入 PHASE_LOADING + GC_MAIN_CHARACTER,要求版本包后紧接一次
CG_ENTERGAME;修复前当前实现只发版本包,不会在主角事件中发送 EnterGame。
修改内容
ClassicSession::on_packet()将四个可用主角包中的 40250 主角/BGM 变体作为 Loading 事件边界; 解析成功并发送客户端版本包后立即调用enter_game()。- 删除 ClassicSession 的
m_loading_since、m_entergame_delay和MT_CLASSIC_ENTER_DELAY注入;保留m_entergame_sent作为同一 Loading 周期的幂等保护, 重复GC_MAIN_CHARACTER不会重复发送。 net_classic_session_test改为验证事件顺序和 sequence,世界插值测试改用显式服务器时间, 不再借用 EnterGame 定时器推进测试时钟;net_classic_e2e不再暴露定时器覆盖项。
修复后验证与边界
build-debug/extension/net_classic_session_test:PASS;版本包后同一主角事件发送一次CG_ENTERGAME,重复主角包保持幂等。net_classic_stream_test、net_loopback_test:PASS。- 本分支只修复 Loading/EnterGame 的事件源和包顺序,没有修改飞行物、技能、战斗或 UI;
真实 Windows GameWindow 资源加载耗时、真实服务端完整 Loading 包序、可选安全分支和其它
跨阶段 retry/PHASE_CLOSE live 证据仍属于合同的
remaining,合同继续保持PARTIAL。
Implementation fix round 2026-09-21 — DirectEnter failure resets LoginPhase owner
问题/分支 ID:network.login.phase_flow/ticket-slot-warp-order/direct-enter-failure-login-owner-reset。
40250 参考调用链
CPythonNetworkStream::ConnectGameServer / RecvWarpPacket
-> __DirectEnterMode_Set
-> CNetworkStream::Connect
-> CPythonNetworkStream::OnConnectFailure
-> ClosePhase
-> networkmodule.SetLoginPhase
-> net.Disconnect
-> CPythonNetworkStream::SetOffLinePhase
-> __DirectEnterMode_Initialize
-> 后续 PHASE_LOGIN 的普通 SetLoginPhase 分支
-> __ClearSelectCharacterData
40250 的 DirectEnter 连接失败不是凭据失败。ClosePhase 交给 Python
networkmodule.SetLoginPhase 后,net.Disconnect() 会先回 Offline 并清除 DirectEnter
模式;之后真正的新连接收到 PHASE_LOGIN 时才重新进入普通 LoginPhase。普通分支发送
CG_LOGIN/CG_LOGIN2 后清理旧角色列表 owner。
当前差异与修复前失败证据
当前 ClassicSession::handle_game_connect_failure() 通过
enter_login_phase(true, false) 保留了 m_direct_enter,只把 stage 改成了
LoggingIn。因此替换连接的 PHASE_LOGIN 仍可能沿用 DirectEnter 状态,跳过普通
LoginPhase 的角色列表清理。新增回归在生产修复前运行:
build-debug/extension/net_classic_session_test
FAIL: DirectEnter connect failure clears the stale slot before replacement LoginPhase
1 check(s) failed
修改内容
ClassicSession::enter_login_phase(bool, bool)在“不在旧 transport 上发送登录包”的 ClosePhase/remote-disconnect owner 切换中清除m_direct_enter和m_direct_enter_slot;这对应 40250net.Disconnect -> SetOffLinePhase -> __DirectEnterMode_Initialize,不影响真正PHASE_LOGIN的CG_LOGIN2ticket 路径。- 增加 native 回归:DirectEnter 目标连接失败后注入替换
PHASE_LOGIN,验证旧角色槽位 被普通 LoginPhase 清除;既有 DirectEnter/GC_WARP 成功、半包、重复 phase、peer close 和 PHASE_CLOSE 测试继续保留。
修复后验证与边界
- 修复后
build-debug/extension/net_classic_session_test:PASS。 - 本轮只修复 DirectEnter/GC_WARP 失败后的 phase-owner 状态,不修改飞行物、技能、战斗
或 UI;真实 Windows LoginWindow 表现、真实服务端失败回退包序、空角色 live、服务器
主动
PHASE_CLOSE和完整跨阶段 retry 仍未取证,合同继续保持PARTIAL。
Implementation fix round 2026-09-21 — PHASE_CLOSE LoginWindow retry owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/phase-close-login-retry-owner。
40250 参考调用链
CPythonNetworkStream::RecvPhasePacket(PHASE_CLOSE)
-> ClosePhase
-> networkmodule.SetLoginPhase
-> net.Disconnect
-> LoginWindow.Connect
-> networkmodule.Connect
-> net.ConnectToAccountServer
-> CAccountConnector::Connect
PHASE_CLOSE 不是凭据错误,也不是终态 Failed。40250 先关闭旧主连接并创建 LoginWindow;
用户再次提交登录时,KEEP_ACCOUNT_CONNETION_ENABLE=1 使 ConnectToAccountServer 只替换
认证 transport,旧主连接/session owner 不由 UI 再次全局销毁。
当前差异与修复前失败证据
当前 ClassicSession::on_phase(PHASE_CLOSE) 能回到 LoggingIn 并清理旧游戏流,但没有设置
m_login_retry_pending。因此 AppFlow::_do_connect -> M2Client::retry_login 会无法复用
当前 LoginPhase owner,退回全量 connect_to_server 路径。新增 loopback 回归在生产修复前运行:
build-debug/extension/net_classic_session_test
FAIL: PHASE_CLOSE keeps the LoginWindow retry owner for a replacement auth transport
1 check(s) failed
修改内容
ClassicSession::on_phase(PHASE_CLOSE)设置m_login_retry_pending = true,明确把ClosePhase -> LoginWindow交给下一次显式 Connect;不在已关闭的 game socket 上发送CG_LOGIN,也不发送disconnected。- 增加 loopback 回归:显式
PHASE_CLOSE后在同一 session 上建立替换认证 transport,收到PHASE_AUTH时只发送一份新的CG_LOGIN3;既有 peer-close、DirectEnter failure、半包、 重复 phase 和旧 world 清理测试继续保留。
修复后验证与边界
- 修复后
build-debug/extension/net_classic_session_test:PASS。 - 本轮只补齐显式 PHASE_CLOSE 后的 retry owner,不修改飞行物、技能、战斗或 UI;真实
Windows LoginWindow 视觉时序、真实服务端主动
PHASE_CLOSE包序、空角色 live、完整 跨阶段 retry 和可选安全 runtime 仍未取证,合同继续保持PARTIAL。
Implementation fix round 2026-09-21 — PHASE_HANDSHAKE re-entry owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/phase-handshake-reentry-owner。
40250 参考调用链
CPythonNetworkStream::RecvPhasePacket(PHASE_HANDSHAKE)
-> SetHandShakePhase
-> previous phase-leave callback
-> HandShakePhase
CAccountConnector::__AuthState_RecvPhase(PHASE_HANDSHAKE)
-> __HandshakeState_Set
-> __HandshakeState_Process
40250 收到重新进入的 PHASE_HANDSHAKE 后,不会继续沿用之前的 Login/Select/Loading/Game
处理器。若上一阶段是 Loading/Game,phase-leave owner 先清理地图对象;认证连接则回到
Handshake 状态,只有下一次 PHASE_AUTH 才能发送新的认证登录包。
当前差异与修复前失败证据
修复前 ClassicSession::on_phase 对 PHASE_HANDSHAKE 走默认分支,游戏仍停留在
InGame 且旧 world 未清理;on_auth_phase 也忽略该阶段,认证仍停留在 LoggingIn。
新增专项回归在生产修复前运行:
build-debug/extension/net_classic_session_test
FAIL: PHASE_HANDSHAKE re-enters the 40250 HandShake owner
FAIL: PHASE_HANDSHAKE runs the previous phase leave cleanup before ownership changes
FAIL: auth PHASE_HANDSHAKE re-enters the 40250 HandShake owner
3 check(s) failed
修改内容
ClassicSession::on_phase(PHASE_HANDSHAKE)现在在 Loading/Game owner 上先执行reset_for_map_change()和on_phase_leave,再关闭 time-sync 模式、清除本次CG_ENTERGAME事件状态并进入Connecting/HandShakeowner。ClassicSession::on_auth_phase(PHASE_HANDSHAKE)现在回到认证 Handshake owner,阻止 旧 LoginPhase 应用包在新的PHASE_AUTH之前触发登录副作用。- 没有修改飞行物、技能、战斗、角色名编码或 UI;真正的登录发送仍只由后续
PHASE_LOGIN/PHASE_AUTH触发。
修复后验证与边界
build-debug/extension/net_classic_session_test:PASS。ctest --test-dir build-debug --output-on-failure -j2:23/23 PASS。- ASAN
net_classic_session_test:PASS。 - Godot
app_flow_lifecycle_test.gd、p10_test.gd、netplay_test.gd、disconnect_world_reset_test.gd:PASS。 audit_phase_dispatch.py、audit_sequence_table.py、audit_source_coverage.py:PASS;audit_packet_registry.py仍仅保留已批准的HDR_GC_SYMBOL_DATA平台适配差异。- 真实 Windows/服务端主动
PHASE_CLOSE、空角色 live、完整跨阶段 retry 和可选安全 runtime 仍未取证,合同继续保持PARTIAL。
Implementation fix round 2026-09-21 — GC_AUTH_SUCCESS result authority
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-success-result-authority。
40250 参考调用链
CAccountConnector::__AuthState_Process
-> __AuthState_RecvAuthSuccess
-> if (!kAuthSuccessPacket.bResult) OnLoginFailure("BESAMEKEY")
-> else SetLoginKey(dwLoginKey)
-> CPythonNetworkStream::Connect(game_addr, game_port)
-> CAccountConnector::Disconnect
40250 的认证成功/失败分支只由 bResult 决定。dwLoginKey 是认证成功后交给游戏连接的
票据值,不是第二个凭据失败条件;认证成功分支完成后仍会清理 AccountConnector。
当前客户端差异与修复前失败证据
修复前 ClassicSession::on_auth_packet 使用 p.result == 0 || m_auth_login_key == 0,
导致 result=1、login_key=0 被错误转换为 BESAMEKEY。专项测试在恢复修复前条件后失败:
build-debug/extension/net_classic_session_test
FAIL: GC_AUTH_SUCCESS result=1 remains success even when the ticket value is zero
1 check(s) failed
当前客户端修复后的调用链
ClassicStream::dispatch
-> ClassicSession::on_auth_packet(GC_AUTH_SUCCESS)
-> if (result == 0) on_login_failure("BESAMEKEY")
-> else m_auth_succeeded = true / retain login_key
-> ClassicSession::pump
-> connect game transport first
-> disconnect auth transport
修改内容与验证
- 移除
login_key == 0的伪造失败条件,只保留 40250 的result == 0拒绝分支。 - 保留失败回调、auth stream 清理、成功后的 game transport handoff 和重复包清理行为。
build-debug/extension/net_classic_session_test:PASS;result=1/login_key=0不再发出BESAMEKEY,认证 transport 仍完成成功分支清理。- 本轮未修改飞行物、技能、战斗或 UI。
- 真实服务端不会主动构造“成功但零票据”的异常包,完整 Windows/服务端重试、主动
PHASE_CLOSE、空角色 live 和可选安全 runtime 仍未闭合,合同保持PARTIAL。
Implementation fix round 2026-09-21 — auth PHASE_CLOSE silent owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/auth-phase-close-silent-owner。
40250 参考调用链
CAccountConnector::__AuthState_Process
-> __AuthState_RecvPhase
-> if (phase == PHASE_HANDSHAKE) __HandshakeState_Set()
-> else if (phase == PHASE_AUTH) send CG_LOGIN3 / __AuthState_Set()
-> PHASE_CLOSE falls through and returns true
认证连接的 PHASE_CLOSE 不走游戏侧 ClosePhase,也不调用
OnLoginFailure、OnConnectFailure 或 __OfflineState_Set;AUTH owner 继续由认证
socket 的真实 disconnect/failure 事件决定。
当前客户端差异与修复前失败证据
修复前 ClassicSession::on_auth_phase 对认证 PHASE_CLOSE 写入
m_last_error = "auth server closed phase" 并设置全局 m_phase_closed。这把认证阶段
的静默 phase 值错误地解释成了游戏 transport 的显式关闭,污染后续 owner/retry 判断。
专项回归在恢复旧实现后失败:
build-debug/extension/net_classic_session_test
FAIL: auth PHASE_CLOSE is silent and does not mark game phase closure
1 check(s) failed
修改内容与验证
on_auth_phase每次先清除游戏侧m_phase_closed;只保留PHASE_HANDSHAKE和PHASE_AUTH的认证 owner 分支,移除认证PHASE_CLOSE的错误标记和错误文本。- 游戏 transport 的
on_phase(PHASE_CLOSE)保持原有显式关闭、旧 world 清理和 LoginPhase retry owner 语义,未把两条 phase handler 合并。 - 修复后
net_classic_session_test通过;完整 CTest、ASAN、Godot 生命周期/网络回归 和结构审计随后执行并登记在本轮 history,合同仍保持PARTIAL。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。
Implementation fix round 2026-09-21 — PHASE_DEAD no-op time-sync owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/phase-dead-noop-time-sync。
40250 参考调用链
CPythonNetworkStream::RecvPhasePacket(PHASE_DEAD)
-> switch(PHASE_DEAD)
-> break
-> return true
40250 的 PHASE_DEAD 分支没有调用 phase setter,也没有修改 handshake/time-sync owner。
因此在尚未进入 Login/Select/Loading/Game 的阶段收到该值时,后续握手仍按当前 owner 的普通
HANDSHAKE 回包处理。
当前客户端差异与修复前失败证据
修复前 ClassicSession::on_phase() 在分派到 PHASE_DEAD 之前把
ClassicStream::m_time_sync_mode 设置为 true。这会让随后重复握手错误地发出
CG_TIME_SYNC,而不是保持 40250 的普通 HANDSHAKE 回包;同时通用的
phase != PHASE_CLOSE 清理条件还会错误清除显式关闭 owner 标记。
专项测试在恢复修复前条件后失败:
build-debug/extension/net_classic_session_test
FAIL: PHASE_DEAD is a no-op and keeps the plain handshake owner
1 check(s) failed
补充的修复前回归还失败于:
FAIL: PHASE_DEAD does not clear the explicit close owner marker
1 check(s) failed
修改内容与验证
- 从阶段到 time-sync owner 的条件中移除
PHASE_DEAD,并只允许真正安装新阶段 owner 的PHASE_HANDSHAKE/LOGIN/SELECT/LOADING/GAME清除m_phase_closed;该阶段继续由switch的默认空分支保持无状态变化。 - 新增
PHASE_DEAD → 重复 HANDSHAKE回归,验证回包头仍为HDR_HANDSHAKE,避免将PHASE_DEAD与真正的 Login/Select/Loading/Game phase setter 混用;同时验证PHASE_CLOSE → PHASE_DEAD保留显式关闭 owner 标记。 - 修复后
net_classic_session_test通过;本轮完整 CTest、ASAN、Godot 回归和结构审计完成后 登记到 history,合同仍保持PARTIAL。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。
Implementation fix round 2026-09-21 — PHASE_SELECT re-entry owner
问题/分支 ID:network.login.phase_flow/auth-game-phase-matrix/phase-select-reentry-owner。
40250 参考调用链
CPythonNetworkStream::RecvPhasePacket(PHASE_SELECT)
-> SetSelectPhase
-> if (old phase != Select) m_phaseLeaveFunc.Run()
-> __LeaveGamePhase
-> clear old map-local actors/items/effects
-> install SelectPhase owner
PythonNetworkStreamPhaseSelect.cpp:9-12 明确要求先运行旧阶段的离开函数;当旧 owner
是 Game 时,__LeaveGamePhase 负责清理旧地图状态,然后才进入选人阶段。
当前客户端差异与修复前失败证据
修复前 ClassicSession::on_phase(PHASE_SELECT) 直接设置 CharSelect/DirectEnter 的下一阶段,
没有运行 Game owner 的 phase-leave 清理,因此旧 EntityStore 和宿主的 phase-leave 通知会残留。
专项测试在恢复修复前逻辑后失败:
build-debug/extension/net_classic_session_test
FAIL: PHASE_SELECT runs the previous GamePhase leave cleanup
FAIL: PHASE_SELECT notifies the host after GamePhase leave cleanup
2 check(s) failed
修改内容与验证
ClassicSession::on_phase(PHASE_SELECT)在当前 owner 为InGame时先调用reset_for_map_change(),再调用on_phase_leave,随后才安装CharSelect或 DirectEnter 的 Select owner。- 新增 Game → Select 的旧 world、阶段 owner 和宿主通知回归,避免把 Loading 的空离开函数误套成 Game 清理逻辑。
- 修复后
net_classic_session_test通过;完整 CTest、ASAN、Godot 生命周期/网络回归和结构审计 随后执行并登记在本轮 history,合同仍保持PARTIAL。 - 本轮未修改飞行物、技能、战斗、角色名编码或 UI。