Files
mtgodot-poc/audit/remediation-roadmap.md
T
shenleiandClaude Sonnet 5 7df97e01f9 fix(movement): camera auto-rotates to face WASD input direction
40250 parity: CPythonPlayer::NEW_MoveToDirection's m_isCmrRot branch
continuously rolls the camera toward the held movement direction every
frame while a direction key is down, independent of whether a new
translation target was just set. Add _camera_auto_rotate() to replicate
this (forward/back = no rotation, strafe = max rate, diagonal = half
rate), called unconditionally after the frozen/locked/shop/emotion gate
in _process(). Godot's yaw-increase handedness is opposite the
reference's D3D Roll(), so the sign is flipped as a platform adaptation;
verified by convergence invariants in keyboard_motion_timeline_test.gd
rather than bit-for-bit angle matching.

Closes movement.keyboard.motion/camera-auto-follow-rotation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:17:00 +09:00

280 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 40250 客户端差异修复路线
本文件记录当前修复顺序和调度规则。它是给人和其他 AI 工具读取的当前路线,不替代机器可读的 `audit/manifest.json`;合同状态、剩余分支和证据以账本为准。
## 当前基线
统计快照:2026-09-21。
| 项目 | 数量 |
| --- | ---: |
| 总合同 | 45 |
| 已排除 | 1 |
| 未完全闭合合同 | 44 |
| 未闭合分支条目 | 224 |
| P0 分支条目 | 93 |
| P1 分支条目 | 118 |
| P2 分支条目 | 13 |
| P3 分支条目 | 0 |
这些数量是快照,不是手工修改的真相。每次修复前后都要运行账本刷新和报告脚本,以 `audit/manifest.json` 的当前内容为准。
## 修复阶段
### 阶段 0:账本和调度卫生
- 读取本文件、`audit/manifest.json`、相关合同和最近的 `audit/history.jsonl`
- 先运行 `audit_ledger.py refresh --write``report --write``validate`
- 将一个合同中的每个剩余分支作为独立的修复目标;修复后必须同步合同、账本和历史。
- 问题/分支 ID 使用稳定格式 `<contract-id>/<branch-slug>`;历史中只有合同 ID 的旧记录不能证明该合同的所有分支都已完成,首次处理某个分支时要补建具体 ID。
- 如果只是缺少真实服务端、Windows 客户端或视觉运行证据,不要伪装成代码已完成,记录为验证阻塞。
### 阶段 1P0 在线核心链路
按完整的用户可玩链路推进:
1. 登录、认证、选人、Loading、进入地图和失败重试:`network.login.phase_flow``network.character_select.create_delete`
2. WASD、停止、转向、远程同步、传送和地图切换:`movement.keyboard.motion``movement.remote_sync_state``world.map_load_transition``world.reset_cleanup`
3. 攻击、受击、死亡、经验、掉落和拾取:`combat.attack_combo_damage``combat.death.exp_drop``item.drop_pickup`
4. 技能释放、动作事件、特效和服务端结果:`skill.cast.effect_timing``skill.player_ui_cooldown`
5. 协议分派、重连、安全分支和资源/协议加载:`network.packet_dispatch_protocol``network.reconnect_warp``network.security_encoding``resource.pack_locale_proto`
阶段 1 未完成前,不主动扩大 P1 的视觉或外围功能修复范围。
### 阶段 2P1 游戏功能
依次处理背包/装备、NPC/任务、社交、目标选择、状态、模型和效果资源。`combat.flying_projectile` 在这一阶段处理;除非它直接阻塞 P0 或引入回归,否则不提前插队。
### 阶段 3P2/P3 平台和表现
处理音频、天气、平台发布、移动端细节和其他不阻塞核心游戏流程的差异。
## 防止重复修复规则
- 每一轮只设置一个主合同,并为本轮选定一个明确的问题/分支 ID;可以同时修复被它直接调用的跨合同根因,但必须在历史中列出关联 ID。
- 同一个问题/分支 ID 不得重复修复或重复审计,除非出现新的回归、实现文件发生实质变化,或新增证据证明原结论不成立。
- 同一合同可以在后续轮次继续处理,但必须选择不同的 `remaining` 分支;合同是功能分组,不是问题去重单位。
- 一个合同仍为 `PARTIAL` 不代表此前修改无效;只处理其 `remaining` 中尚未闭合的分支,不能重新审计已通过的测试路径。
- 每轮开始前必须检查 `audit/history.jsonl`,确认本轮问题/分支 ID 没有已完成记录;如果历史记录不够细,先补充问题 ID,再开始修改。
- 每轮结束必须记录:参考调用链、当前调用链、修改的分支、修复前失败证据、修复后测试、剩余阻塞和下一轮候选。
- 下一轮候选按 `P0 → P1 → P2 → P3` 排序;同优先级按用户可玩链路和阻塞关系排序,不按“最容易修改”排序。
## 完成门槛
单个分支只有同时满足以下条件才可关闭:
1. 40250 完整调用链和当前实现调用链已对齐。
2. 前置条件、分支、算法、状态顺序、单位、时序、资源、协议副作用和清理行为均有等价证据。
3. 有修复前失败、修复后通过的自动测试;必要时覆盖拒绝、打断、重复包、死亡、重连和地图切换。
4. 需要真实服务端、Windows 客户端或视觉证据时,已完成对应验证;否则保持 `PARTIAL` 并记录阻塞原因。
合同只有在全部剩余分支关闭后,才能从 `PARTIAL` 提升为 `STATIC_VERIFIED``TEST_VERIFIED`
## 下一轮
下一轮主合同固定为 `network.login.phase_flow`,具体处理其尚未闭合的问题/分支。除非发现新的 P0 回归或登录链路被其他合同阻塞,否则不切回已处理过的问题。
上一轮已闭合分支:`network.login.phase_flow/auth-game-phase-matrix/peer-close-login-owner`
真实 `/logout` 已证明建立连接后的 peer closeAppFlow 现在按 40250 回到唯一 LoginPhase owner
不得重复修改“保留 GameScene/自动重连遮罩”这一已闭合分支。
上一轮已闭合分支:`network.login.phase_flow/auth-game-phase-matrix/phase-close-server-fixture`
本地等价 fixture 已证明显式 `PHASE_CLOSE` 不再伪造 `disconnected`;真实服务端主动
`PHASE_CLOSE` 包序仍是证据阻塞,后续只在获得新 live fixture 时验证,不重复修改 native owner。
上一轮已处理分支:`network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture`
已补充 `net_classic_e2e``MT_CLASSIC_EXPECT_EMPTY=1` 只读探针;当前账号仅有部分空槽,
不能将其当作全空角色证据,也不要通过删号制造 fixture。全空角色的正向 live 证据仍保持在
`remaining`,后续只在获得安全 fixture 时验证,不重复修改本地 SELECT owner。
上一轮已处理分支:`network.login.phase_flow/login-failure-retry-cleanup/login-form-retry-owner`
已按 40250 `LoginWindow.Connect -> AccountConnector.Connect` 增加 LoginPhase retry owner;登录失败后
只替换认证 transport,不再由 AppFlow 重试触发全局 `disconnect_from_server/world_reset`。本地 UI/source
断言和 native loopback 包序已通过;真实 Windows 可视化重试和服务器“失败->重新输入->成功”仍是证据阻塞,
不得重复修改该 owner。
前一阶段首选分支:`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order`
下一步只处理跨阶段失败/重复进入的真实或等价包序:失败后旧 auth/game stream 的迟到包、重复
`PHASE_LOGIN/PHASE_AUTH` 的替换顺序、以及完整 UI retry 的真实服务端证据;优先补测试和 fixture,
不要重新审计已经由
`direct-enter-loading-select-packet``gc-warp-live-package-order``peer-close-login-owner`
`phase-close-server-fixture` 闭合的成功进入、peer-close owner 和显式 PHASE_CLOSE transport-owner
分离链路。
本轮已处理该分支中的认证成功尾包子路径:`GC_AUTH_SUCCESS` 后的旧 `PHASE_AUTH` 会随旧
AccountConnector 接收缓冲一起丢弃,不重复发送 `CG_LOGIN3`。后续必须使用新的稳定子分支
`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order-game-login-stale-tail`
处理游戏登录失败后的迟到 phase;不得重复修改认证成功的清理边界。
本轮已处理 `cross-phase-retry-order-game-login-stale-tail`:游戏 LoginPhase/SelectPhase/
Loading 自有包在当前应用帧完成后按 40250 phase-local return 停止,迟到 receive tail 延后到
下一次处理周期;Loading 转交 GamePhase 的包仍保留游戏批处理语义。后续应选择新的稳定子分支
处理真实失败后重新输入成功、重复进入包序或服务器主动 PHASE_CLOSE live 证据,不得重复修改
本轮 `return_after_current_packet` 机制。
本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-connect-immediate-failure-owner`
认证地址解析/socket 同步失败与异步 `OnConnectFailure` 统一回到 40250 的 Offline owner,并保留
登录重试资格。不得重复修改本轮 `handle_auth_connect_failure()` 的认证清理边界。
本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/game-connect-immediate-failure-owner`
普通认证成功后的游戏连接失败现在走独立 `OnConnectFailure -> LoginWindow` ownerDirectEnter
和 GC_WARP 失败走 `ClosePhase -> SetLoginPhase`,不再触发凭据 `OnLoginFailure` 或伪造
`disconnected`。修复前 DirectEnter/普通 game-connect fixture 均有失败断言,修复后 native、
Godot bridge、CTest、ASAN 和阶段/包结构审计通过;不得重复修改本轮
`handle_game_connect_failure()``connect_failed` signal 或 GC_WARP owner 分离。
本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-connect-bridge-owner`:按
40250 `AccountConnector::OnConnectFailure -> Offline -> LoginWindow` 修复初始认证同步/异步
连接失败的 M2Client bridge,新增独立 `connect_failed` 回调、保留认证 session 和
`retry_login` owner;修复前专项桥接测试失败,修复后 native/CTest/ASAN/Godot 与阶段/包结构
审计通过。不得重复修改本轮 `on_auth_connect_failure``classic_auth_connect_failure_pending`
或认证 session 保留逻辑。
本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-peer-close-bridge-owner`:按
40250 `CAccountConnector::OnRemoteDisconnect -> __OfflineState_Set` 增加认证 peer-close
独立 callbackM2Client 不再把认证 socket 被动关闭伪造成 `login_failed``connect_failed`
并保留非终态 LoginPhase owner。修复前后均由真实本地 TCP 接受/关闭/再次替换 transport 的
Godot 回归固定;不得重复修改本轮 `on_auth_remote_disconnect`
`classic_auth_remote_disconnect_pending`
本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/phase-close-disconnect-login-owner`
重新拆分 40250 的 `PHASE_LOGIN -> CPythonNetworkStream::SetLoginPhase`
`PHASE_CLOSE/OnRemoteDisconnect -> Python networkmodule.SetLoginPhase -> net.Disconnect` 两种
语义;只有真正的 `PHASE_LOGIN` 会在当前 transport 上发送 `CG_LOGIN/CG_LOGIN2`,显式关闭、peer close
和 DirectEnter 失败会清理旧 transport 并回到 LoginWindow owner。`net_classic_session_test` 的修复前
失败、修复后无旧连接登录包/旧 transport Offline、CTest/ASAN/Godot 回归均已固定;不得重复修改
`enter_login_phase` 的发送分支或本地 PHASE_CLOSE owner。真实服务器主动 PHASE_CLOSE 包序仍需 live 证据。
本轮已处理 `network.login.phase_flow/login-failure-retry-cleanup/game-peer-close-retry-owner`
按 40250 `OnRemoteDisconnect -> SetLoginPhase -> LoginWindow` 与后续
`LoginWindow.Connect -> ConnectToAccountServer` 的 owner 边界,游戏态 peer close 现在保留同一
`ClassicSession` 的显式重试资格;重试只替换认证 transport,新的 `PHASE_AUTH` 只发送一份使用新密码的
`CG_LOGIN3`,不再回退到全局 session/world teardown。修复前失败和 loopback 替换 transport 回归已记录;
不得重复修改 `m_login_retry_pending` 的游戏 peer-close owner。真实 Windows LoginWindow 时序、真实服务端
失败后重试成功包序和完整跨阶段 retry 仍需 live 证据。
下一主候选:`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`
本地结构和 owner 分支已覆盖,下一轮只取真实 Windows/40250 服务端证据:失败后重新输入成功、
服务器主动 `PHASE_CLOSE`、完整跨阶段 retry 和 DirectEnter/GC_WARP 失败回退包序;不要回到已关闭
的认证或游戏连接失败 owner。
本轮已执行该候选的真实 `/logout` 探针并登记为
`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`
登录到 PHASE_GAME、执行命令、peer close、world reset 和 LoginPhase owner 均通过,但
`MT_NET_TRACE=1` 没有出现 `phase -> 0`。服务端 `DESC::SetPhase(PHASE_CLOSE)` 的源码状态链已
确认,当前 build 因 close 后的 `DESC::Packet` guard 没有提供客户端可观察的 PHASE_CLOSE 包,
因此不能用客户端改动伪造这个证据。外层 wrapper 的失败仅来自既有 4 个 ParticlesShaderRD/RID
泄漏门禁。除非更换服务端 build/fixture,否则不要重复 `/logout` 探针;下一步改为获取真实
PHASE_CLOSE fixture,并在同一 fixture 上完成失败重试和跨阶段包序审计。
本轮新增并闭合本地分支:
`network.login.phase_flow/auth-game-phase-matrix/auth-success-game-before-auth-clear`
对照 40250 `__AuthState_RecvAuthSuccess -> SetLoginKey -> MainStream.Connect -> AccountConnector.Disconnect`
发现当前 `ClassicSession::pump()` 曾反向执行 auth disconnect 再 game connect;修复后使用 loopback
双 transport 状态回调固定为 `game:connecting -> auth:offline`。该分支不得重复修改。下一主候选仍是
新的真实失败后重试/跨阶段包序 fixture;如服务端 fixture 没有变化,不重复 `/logout`
本轮新增并闭合本地分支:
`network.login.phase_flow/auth-game-phase-matrix/game-login-retry-process-boundary`
对照 40250 `CPythonApplication::Process -> CPythonNetworkStream::Process -> CAccountConnector::Process`
发现认证成功后当前端在同一 pump 再次处理新游戏流;修复后重试期间保留旧主流的处理机会,
认证成功建立替换流后延迟到下一 pump 才处理新流。该分支不得重复修改;真实服务端失败后重试
成功包序和 Windows LoginWindow 仍是未验证证据,下一候选继续选择新的 live/跨阶段分支。
本轮新增并闭合本地分支:
`network.login.phase_flow/legacy-text-wire-encoding/ui-gb2312-input-capacity`
发现 native 已按 UTF-8 -> GB2312 转换后的 wire 字节校验,但 `CharSelectScreen` 仍按 UTF-8
字节限制角色名输入,导致“龙驹简体中文免费版”在 UI 层被提前拒绝。现改为 UI 只做 Unicode
字符/控制字符校验,实际 24-byte GB2312 容量由 native 发包边界统一判断,并加入 UI/native
回归;不得重复把角色名 UI 改回 UTF-8 字节限制,也不得把地图名、任务名、系统提示套用该
24-byte 字段限制。
本轮新增并处理分支:
`network.login.phase_flow/ticket-slot-warp-order/direct-enter-failure-login-owner-reset`
对照 40250 `OnConnectFailure -> ClosePhase -> net.Disconnect -> SetOffLinePhase`,修复
DirectEnter/GC_WARP 失败回到 LoginPhase 时残留 `m_direct_enter`、导致替换 `PHASE_LOGIN`
跳过普通角色列表清理的问题;修复前失败测试和修复后通过证据已登记。真实 Windows/服务端
失败回退包序仍是 `PARTIAL`,下一轮继续获取新的 live fixture,不重复修改本地 ClosePhase owner。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/phase-close-login-retry-owner`
对照 40250 `PHASE_CLOSE -> networkmodule.SetLoginPhase -> net.Disconnect -> LoginWindow.Connect`
补齐显式关闭后的 retry owner;同一 `ClassicSession` 现在可替换认证 transport,收到新的
`PHASE_AUTH` 时只发送一份 `CG_LOGIN3`。真实服务端 `PHASE_CLOSE` 包序和完整跨阶段 retry
仍是 live 证据阻塞,不能据此关闭整个登录合同。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/phase-handshake-reentry-owner`
对照 40250 `RecvPhasePacket(PHASE_HANDSHAKE) -> SetHandShakePhase`
`CAccountConnector::__AuthState_RecvPhase -> __HandshakeState_Set`,补齐认证/游戏
HandShake owner 重入;从 Loading/Game 重入时先执行旧 phase-leave world 清理,后续只有新的
`PHASE_LOGIN/PHASE_AUTH` 才能继续登录。真实 Windows/服务端重试包序仍未闭合;不得重复修改
本轮的 `PHASE_HANDSHAKE` owner 和 world-leave 边界。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/auth-success-result-authority`
对照 40250 `__AuthState_RecvAuthSuccess``bResult` 分支,修复当前端把 `login_key == 0`
额外解释为凭据失败的问题;成功/失败现在只由 `result` 决定,票据值仍交给后续游戏连接。
真实服务端不会主动构造成功零票据包,不能用该本地边界测试替代完整 live retry 或安全模块证据;
不得重复修改本轮的认证结果判定条件。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/auth-phase-close-silent-owner`
对照 40250 `CAccountConnector::__AuthState_RecvPhase` 只处理 `PHASE_HANDSHAKE/PHASE_AUTH`
的分支表,修复当前端把认证 `PHASE_CLOSE` 错误转成游戏侧 `phase_closed`
`auth server closed phase`。认证 close 现在保持 AUTH owner,直到真实认证 socket
disconnect/failure;不得重复修改本轮的认证 close 静默语义。服务器主动 `PHASE_CLOSE`
的 live 包序仍需外部 fixture,不能用本地 phase 注入替代。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/phase-select-reentry-owner`
对照 40250 `RecvPhasePacket(PHASE_SELECT) -> SetSelectPhase -> m_phaseLeaveFunc.Run()`
修复当前端从 Game 返回 Select 时未清理旧地图 world、未通知宿主 phase-leave 的差异;现在先执行
Game owner 的 `reset_for_map_change()``on_phase_leave`,再安装 Select/DirectEnter owner。
该分支不得重复修改;真实 Windows/服务端跨阶段 retry、服务器主动 PHASE_CLOSE 和 GC_WARP
失败回退包序仍需 live fixture。
本轮新增并处理分支:
`network.login.phase_flow/auth-game-phase-matrix/phase-dead-noop-time-sync`
对照 40250 `RecvPhasePacket(PHASE_DEAD)` 的显式空分支,修复当前端先打开 time-sync mode、
导致后续重复握手错误发出 `CG_TIME_SYNC`,以及清除显式 PHASE_CLOSE owner 标记的差异;现在
`PHASE_DEAD` 不改变阶段 owner 或 time-sync 状态。该分支不得重复修改;完整跨阶段 retry、
服务器主动 PHASE_CLOSE、空角色 live 和 GC_WARP 失败回退包序仍需独立证据。
### 2026-09-22:本轮临时切换主合同到 `movement.keyboard.motion`
`network.login.phase_flow` 的剩余分支目前全部卡在真实 Windows 客户端/服务端主动
`PHASE_CLOSE` 包序证据或第三方闭源安全 SDKMatrix/Passpod/Panama/HybridCrypt/HS/XTrap),
本地代码层面已无可继续推进的动作。按阶段 1 的可玩链路顺序(登录 → 移动 → 战斗 → 技能 →
网络),本轮改为处理 `movement.keyboard.motion`
本轮新增并闭合分支:`movement.keyboard.motion/camera-auto-follow-rotation`
对照 40250 `CPythonPlayer::NEW_MoveToDirection``m_isCmrRot` 相机跟随分支(每帧对按住
的方向键,把相对相机的方位角折算到 `fRotRat∈[-90,90]` 并按 `m_fCmrRotSpd=20.0` 的速率用
`CCamera::Roll` 持续转动相机),当前端此前完全没有实现这个副作用。已在
`player_controller.gd` 新增 `_camera_auto_rotate()`,用同样的折叠/缩放公式驱动
`GameCamera.yaw`D3D `Roll` 与 Godot `yaw` 转向手性相反,增量符号作为显式记录的平台适配
取反,用『按住方向时相机朝该方向收敛、正前后不转、从不越界』的不变式在
`keyboard_motion_timeline_test.gd` 中核验(因为没有真实 Windows 客户端可供逐位对照)。
详见 `audit/contracts/movement.keyboard-motion.md``Implementation fix round
2026-09-22` 小节。不得重复修改本轮的 `_camera_auto_rotate()` 折叠公式或符号约定,除非
出现新的 live 证据推翻当前的收敛不变式假设。
本轮过程中额外发现但未修复(已记录进合同 `Remaining` 与本文件供下一轮参考):
`CActorInstance::CanAct()``Paralysis/Faint/Sleep` 三个移动门当前端完全没有接入
`net_play.gd::_can_process_network_state()` 只挡 `dead/stunned/knock_down`)。这三个
状态的触发源分别在萨满技能、Python 侧角色模块和效果可见性绑定,尚未定位到当前端对应的
服务端 affect 管线,属于比相机分支更大的独立子项,不要在没有先做好触发源映射的情况下
仓促接线。
`movement.keyboard.motion` 合同仍为 `PARTIAL`:根运动/`AccumulationMovement`
`OnMove/OnMoving/OnWaiting/OnStop` 资源事件源、同步/受击/死亡/传送门叠加顺序、上述
Paralysis/Faint/Sleep 移动门和真实模型姿态测试仍未闭合,下一轮可以在这些分支中任选一个
继续(`AccumulationMovement` 根运动差异或 Paralysis/Faint/Sleep 移动门是其中两个具体、
无需真实服务端/客户端证据即可推进的候选)。
`network.login.phase_flow` 的下一主候选仍保持文件前面记录的
`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`
本地结构和 owner 分支均已覆盖,只等新的真实服务端/Windows fixture;在没有新 fixture 前不要
重复 `/logout` 探针或重复修改已闭合的 owner 分支。