Files
mtgodot-poc/audit/remediation-roadmap.md
T
shenleiandClaude Sonnet 5 c5c183da37 fix(movement): remove unreferenced [0.25,3.0] clamp on local player's movSpd scale
CInstanceBase::SetMoveSpeed only guards moving_speed > 1100 -> 0; the local
player's set_server_speed() was additionally floor/ceil-clamping the scale to
an arbitrary [0.25, 3.0] band with no basis in the reference or in this
project's already-fixed remote-entity equivalent (EntityStore::motion_move_speed).
A heavy haste stack or slow debuff on the local player would silently diverge
from what everyone else sees. Removed the extra clamp; kept the moving_speed<=0
early-return guard against pre-spawn/malformed values.

audit: movement.keyboard.motion/server-speed-scale-clamp closed with reference
citation, regression test, and full related-suite pass; contract stays PARTIAL.

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

25 KiB
Raw Blame History

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 --writereport --writevalidate
  • 将一个合同中的每个剩余分支作为独立的修复目标;修复后必须同步合同、账本和历史。
  • 问题/分支 ID 使用稳定格式 <contract-id>/<branch-slug>;历史中只有合同 ID 的旧记录不能证明该合同的所有分支都已完成,首次处理某个分支时要补建具体 ID。
  • 如果只是缺少真实服务端、Windows 客户端或视觉运行证据,不要伪装成代码已完成,记录为验证阻塞。

阶段 1P0 在线核心链路

按完整的用户可玩链路推进:

  1. 登录、认证、选人、Loading、进入地图和失败重试:network.login.phase_flownetwork.character_select.create_delete
  2. WASD、停止、转向、远程同步、传送和地图切换:movement.keyboard.motionmovement.remote_sync_stateworld.map_load_transitionworld.reset_cleanup
  3. 攻击、受击、死亡、经验、掉落和拾取:combat.attack_combo_damagecombat.death.exp_dropitem.drop_pickup
  4. 技能释放、动作事件、特效和服务端结果:skill.cast.effect_timingskill.player_ui_cooldown
  5. 协议分派、重连、安全分支和资源/协议加载:network.packet_dispatch_protocolnetwork.reconnect_warpnetwork.security_encodingresource.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_VERIFIEDTEST_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_e2eMT_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-packetgc-warp-live-package-orderpeer-close-login-ownerphase-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_failureclassic_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_failedconnect_failed 并保留非终态 LoginPhase owner。修复前后均由真实本地 TCP 接受/关闭/再次替换 transport 的 Godot 回归固定;不得重复修改本轮 on_auth_remote_disconnectclassic_auth_remote_disconnect_pending

本轮已处理 network.login.phase_flow/auth-game-phase-matrix/phase-close-disconnect-login-owner 重新拆分 40250 的 PHASE_LOGIN -> CPythonNetworkStream::SetLoginPhasePHASE_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) -> SetHandShakePhaseCAccountConnector::__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_RecvAuthSuccessbResult 分支,修复当前端把 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_closedauth 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_MoveToDirectionm_isCmrRot 相机跟随分支(每帧对按住 的方向键,把相对相机的方位角折算到 fRotRat∈[-90,90] 并按 m_fCmrRotSpd=20.0 的速率用 CCamera::Roll 持续转动相机),当前端此前完全没有实现这个副作用。已在 player_controller.gd 新增 _camera_auto_rotate(),用同样的折叠/缩放公式驱动 GameCamera.yawD3D Roll 与 Godot yaw 转向手性相反,增量符号作为显式记录的平台适配 取反,用『按住方向时相机朝该方向收敛、正前后不转、从不越界』的不变式在 keyboard_motion_timeline_test.gd 中核验(因为没有真实 Windows 客户端可供逐位对照)。 详见 audit/contracts/movement.keyboard-motion.mdImplementation 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:根运动/AccumulationMovementOnMove/OnMoving/OnWaiting/OnStop 资源事件源、同步/受击/死亡/传送门叠加顺序、上述 Paralysis/Faint/Sleep 移动门和真实模型姿态测试仍未闭合,下一轮可以在这些分支中任选一个 继续(AccumulationMovement 根运动差异或 Paralysis/Faint/Sleep 移动门是其中两个具体、 无需真实服务端/客户端证据即可推进的候选)。

2026-09-22T01:30Zmovement.keyboard.motion/canact-paralysis-faint-sleep-gate 调查关闭(无需修复)

对上一轮标记的两个候选之一(Paralysis/Faint/Sleep 移动门)做完整可达性追踪后确认它是 假阳性,不是代码可执行的差异,本轮未改动 GDScript/C++ 实现:

  • Paralysis:唯一 setter __Shaman_SetParalysisInstanceBaseEffect.cpp:821)在整个可达 40250 源码树中零调用点,是死代码,40250 客户端实际运行时 IsParalysis() 恒为 false。
  • SleepSetAffect()AFFECT_SLEEP 分支已被注释掉,唯一存活路径是 AFFECT_STUN → SetSleep()InstanceBaseEffect.cpp:932),即 IsSleep() 实际上是 AFFECT_STUN 的别名,已被当前端 entity_store.h:119stunned 字段和 net_play.gd::_can_process_network_state()dead/stunned/knock_down 门覆盖。
  • Faint:唯一 setter chrFaintTest()PythonCharacterModule.cpp:1087)是与 chrtestRestoreRenderMode 并列的调试用 Python 绑定,作用于 GetSelectedInstancePtr() 现有 40250 检出没有 root/ Python 脚本可证明/证伪在线可达性,evidence-blocked,未修复。

调查过程中发现的衍生项(IsStun()RecvStunPacket() 对主角 GC_STUNDie() 而非 Stun())核实后确认不是 movement.keyboard.motion 差异——CanAct()IsDead()/IsStun() 的移动阻断效果相同,当前端 _can_process_network_state() 也已对 dead/stunned 同等阻断,移动结果已正确等价;Die()/Stun() 分流只影响死亡系统本身 (EXP 掉落/尸体/复活),已在 combat.affect-status.md Remaining #2、分支矩阵第 63/67 行登记,不在本合同重复记录。详见 audit/contracts/movement.keyboard-motion.md 的「调查轮次 2026-09-22T」小节。 该分支已关闭,不要在没有新 live 证据的情况下重新调查 Paralysis/Faint/Sleep 或 GC_STUN 主角分流。

movement.keyboard.motion 下一轮候选收窄为:AccumulationMovement 固定 300cm 根运动/连续速度替代差异、OnMove/OnMoving/OnWaiting/OnStop 资源事件源、同步/受击/ 死亡/传送门叠加顺序、真实模型骨骼快切不翻转的模型级测试——四者均仍未闭合。

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 分支。

2026-09-22T03:15Zmovement.keyboard.motion/server-speed-scale-clamp 修复

追查上一轮列出的四个候选之一(AccumulationMovement 根运动/连续速度替代差异)时,先确认 远端 entity_store.cpp::advance_walk_by_motion 和本地 _process() 的位移积分在种类上同构 (都是恒速 speed*dt 直线插值),排除了「需要整体迁移到逐帧根运动采样」这个更大方向;转而 逐项核对两条路径各自的速度缩放公式,发现真正的差异:本地 set_server_speed()moving_speed/100.0 塞进 clampf(..., 0.25, 3.0),而参考 CInstanceBase::SetMoveSpeed InstanceBaseMovement.cpp)和本项目已修复的远端 EntityStore::motion_move_speed 都只有 moving_speed>1100 → 0 一个门,没有下限/上限夹钳。已修复为 0.0 if moving_speed > 1100 else moving_speed/100.0,新增 keyboard_motion_timeline_test.gd 断言覆盖旧边界内外和 1100 冻结门,随同全部相关回归套件(test_wasd_steering_paritytest_no_auto_move_regressiongame_camera_testmovement_parity_testplayer_move_testmouse_controller_testtest_alignment_paritynetplay_test)通过。详见 audit/contracts/movement.keyboard-motion.mdImplementation fix round 2026-09-22 — server-speed-scale-clamp 小节。该分支已关闭,不要在没有新证据的情况下重新调整 set_server_speed 的缩放边界。

副产品发现(未下定论,供下一轮参考):CInstanceBase::SHORSE::SetMoveSpeed InstanceBase.cpp:40-135)是坐骑上玩家的独立速度设定,只在 IsMounting() 时生效; 远端实体对 mount_vnum!=0 直接归零,而本地玩家改走 MOTION_MODE_HORSE 动作资源—— 两者是否构成差异本轮未核实,不要不经调查直接假设一致或假设有 bug。

movement.keyboard.motion 下一轮候选收窄为:AccumulationMovement 固定 300cm 根运动/连续速度替代差异(积分种类已确认同构,但逐帧时序/.msa motion event 源仍未对拍)、 OnMove/OnMoving/OnWaiting/OnStop 资源事件源、同步/受击/死亡/传送门叠加顺序、真实模型骨骼 快切不翻转的模型级测试、坐骑速度设定(SHORSE::SetMoveSpeed)——五者均仍未闭合。