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>
This commit is contained in:
shenlei
2026-09-22 16:31:17 +09:00
co-authored by Claude Sonnet 5
parent a4ac48db24
commit c5c183da37
6 changed files with 120 additions and 12 deletions
+58 -1
View File
@@ -89,7 +89,7 @@
| Branch structure | PARTIAL | W/S、A/D、八方向、释放顺序和 `m_isCmrRot` 相机跟随已覆盖;Paralysis/Faint/Sleep 已调查——Paralysis 死代码、Sleep 已被 `stunned` 门覆盖、Faint 证据不足,均非差异 |
| Algorithms/formulas | PARTIAL | 方向优先级、归一化和相机跟随的 fold-to-±90/rate 公式一致(`movement.keyboard.motion/camera-auto-follow-rotation`);固定 300 像素 Dst 与根运动仍被连续速度替代 |
| State transition order | PARTIAL | 输入状态、锁门/移动技能恢复、PlayerController、NetPlay 入口已定位;资源事件与网络事件的完整顺序未证实 |
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;米/厘米、移动速度和目标距离不同 |
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;`movSpd` 缩放边界已与 `CInstanceBase::SetMoveSpeed` 对齐(`movement.keyboard.motion/server-speed-scale-clamp`);米/厘米和目标距离不同 |
| Timing/event sources | PARTIAL | 当前是 `_process` 预测 + signal;参考是 Actor motion event 驱动 |
| Resource/data sources | PARTIAL | 当前模型速度可从动作资源读取但有 fallback;`.msa` event/AccumulationMovement 尚未一一对照 |
| Protocol side effects | PARTIAL | `FUNC_MOVE/FUNC_WAIT` 发送和节流有测试;阻止/重复/相机副作用仍未闭合 |
@@ -106,6 +106,11 @@
Paralysis 是可达代码中的死代码(唯一 setter 零调用点);Sleep 在实际运行中是
AFFECT_STUN 的别名,已被当前端 `stunned` 移动门覆盖;Faint 的可达性在现有 40250
检出材料下无法证明或证伪,evidence-blocked。三者均非需要修复的移动等价差异。
- ~~本地玩家 `set_server_speed()``moving_speed/100.0` 施加了参考代码没有的
`clampf(..., 0.25, 3.0)` 边界~~ **已修复(2026-09-22
`movement.keyboard.motion/server-speed-scale-clamp`,见下方实现轮次)**:现在
`CInstanceBase::SetMoveSpeed` 和已修复的 `EntityStore::motion_move_speed`
一样,只在 `moving_speed > 1100` 时归零,其余按 `moving_speed/100.0` 直接换算。
### Audit round 2026-09-21
@@ -274,3 +279,55 @@
移动阻断结果无影响,已移交 `combat.affect_status` 既有条目,不重复登记。`Remaining`
中对应条目标记为已调查关闭。合同状态维持 `PARTIAL`(根运动/资源事件/同步受击门等
仍未闭合),Branch structure 行的 Paralysis/Faint/Sleep 备注移除。
### Implementation fix round 2026-09-22 — `movement.keyboard.motion/server-speed-scale-clamp`
**参考链**`InstanceBaseMovement.cpp:CInstanceBase::SetMoveSpeed(UINT uMovSpd)`——
`if (uMovSpd > 1100) uMovSpd = 0; m_GraphicThingInstance.SetMoveSpeed(uMovSpd/100.0f);`
除 1100 冻结门外没有其他上下限。
**当前链(修复前)**`player_controller.gd::set_server_speed(moving_speed)`
`moving_speed/100.0` 塞进 `clampf(..., 0.25, 3.0)`,即 `moving_speed<25` 会被强行拉高到
0.25 倍速、`moving_speed>300` 会被强行压低到 3.0 倍速——这个区间在参考代码、
`InstanceBase.cpp` 或本项目已修复的远端实现 `EntityStore::motion_move_speed`
`extension/src/net/entity_store.cpp`,只有 `moving_speed>1100→0`
`mount_vnum!=0→0` 两个门)中都找不到依据,是无参考支持的本地私有夹钳:叠了减速/
加速状态的本地玩家会和其他人看到的自己不一致。
调查过程中还确认 `advance_walk_by_motion`(远端实体)和本地 `_process()` 的位移积分
在**种类**上是同构的(都是恒速 `speed*dt` 直线插值,都从 `get_move_motion_speeds()`
`.msa` 基准速度),排除了「根运动 vs 连续速度」这个更大的假设——真正的差异只在
速度缩放公式的边界,不涉及积分模型本身。另确认 `CInstanceBase::SHORSE::SetMoveSpeed`
是坐骑上玩家的另一套速度设定(`InstanceBase.cpp:40-135`),只在 `IsMounting()` 时生效;
本地玩家的动作模式已经在 `net_play.gd::motion_mode_for()` 里按骑乘切到
`MOTION_MODE_HORSE`,而远端实体对 `mount_vnum!=0` 直接归零处理——这是坐骑速度的独立
细节,本轮不下定论,留给未来一轮单独核实是否构成差异。
**修复**`project/player_controller.gd::set_server_speed()`
```gdscript
server_speed_scale = clampf(float(moving_speed) / 100.0, 0.25, 3.0)
```
改为
```gdscript
server_speed_scale = 0.0 if moving_speed > 1100 else float(moving_speed) / 100.0
```
`moving_speed <= 0` 提前 return 的既有保护(防止出生前/畸形值冻结本地预测)保留不变。
两处调用方(`net_world.gd:795-796``net_play.gd:921-924`)均不额外夹钳,改动影响面
只限 `player_controller.gd` 本体。
**测试**:新增 `keyboard_motion_timeline_test.gd` 末尾的 `set_server_speed` 断言
`moving_speed=10→0.1``500→5.0``1200→0.0``100→1.0`,覆盖旧 [0.25,3.0] 边界内外和
1100 冻结门),随同下列既有回归一并执行,全部 PASS:
`keyboard_motion_timeline_test.gd``test_wasd_steering_parity.gd`
`test_no_auto_move_regression.gd``game_camera_test.gd``movement_parity_test.gd`
`player_move_test.gd``mouse_controller_test.gd``test_alignment_parity.gd`
`netplay_test.gd``netplay_test.gd` 里的同名 mock `set_server_speed` 是独立测试替身,
不含旧夹钳逻辑,未受影响)。`git diff --check` 无空白错误。
**Equivalence**`Constants/units` 行的 `movSpd` 缩放边界差异关闭;合同状态维持
`PARTIAL`(固定 300 像素 Dst/根运动适配层、资源与网络事件完整顺序、坐骑速度细节等
仍未闭合)。
+1
View File
@@ -482,3 +482,4 @@
{"time":"2026-09-21T17:08:09Z","event":"implementation_fix_round","result":"PARTIAL","ids":["network.login.phase_flow/auth-game-phase-matrix/phase-dead-noop-time-sync"],"files":["extension/src/net/classic/classic_session.cpp","extension/tests/net_classic_session_test.cpp","audit/contracts/network.login.phase-flow.md","audit/remediation-roadmap.md","audit/manifest.json","audit/history.jsonl","audit/reports/coverage.md"],"tests":["40250 reference source inspection (PASS: PythonNetworkStream.cpp RecvPhasePacket(PHASE_DEAD) is an explicit empty branch; only real phase setters clear the close owner)","pre-fix build-debug/extension/net_classic_session_test (FAIL: PHASE_DEAD cleared the explicit PHASE_CLOSE owner marker; 1 check)","post-fix build-debug/extension/net_classic_session_test (PASS: plain HANDSHAKE response and PHASE_CLOSE marker are both preserved)","cmake --build build-debug -j4 (PASS)","ctest --test-dir build-debug --output-on-failure -j2 (PASS; 23/23)","ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 build-asan/extension/net_classic_session_test (PASS)","MT_PROTOCOL=classic godot --headless --path project --script app_flow_lifecycle_test.gd (PASS)","MT_PROTOCOL=classic godot --headless --path project --script p10_test.gd (PASS)","MT_PROTOCOL=classic godot --headless --path project --script netplay_test.gd (PASS)","MT_PROTOCOL=classic godot --headless --path project --script disconnect_world_reset_test.gd (PASS)","audit_phase_dispatch.py (PASS; no payload differences)","audit_packet_registry.py (PARTIAL; only approved HDR_GC_SYMBOL_DATA adaptation)","audit_sequence_table.py (PASS; 32768 entries identical)","audit_source_coverage.py (PASS; 0 unowned behavior candidates)","audit_ledger.py refresh --write/report --write/validate (PASS; 45 contracts)","manifest/history JSON validation (PASS)","git diff --check (PASS)"],"reason":"在同一分支的进一步等价审计中补齐 PHASE_DEAD 的第二个无副作用约束:通用 phase != PHASE_CLOSE 条件会清除显式关闭 owner,修复为仅真实 phase setter 清除 m_phase_closed。此为同一稳定分支的实质性实现补充,不是重复修复。未修改飞行物、技能、战斗、角色名编码或 UI;真实 Windows/服务器主动 PHASE_CLOSE、空角色 live、完整跨阶段 retry、GC_WARP 失败回退和可选安全 runtime仍未闭合,合同保持 PARTIAL。"}
{"time": "2026-09-22T00:00:00Z", "event": "implementation_fix_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/camera-auto-follow-rotation"], "files": ["project/player_controller.gd", "project/keyboard_motion_timeline_test.gd", "audit/contracts/movement.keyboard-motion.md", "audit/remediation-roadmap.md", "audit/manifest.json", "audit/history.jsonl"], "tests": ["40250 reference source inspection (PASS: PythonPlayerInput.cpp:445-491 CPythonPlayer::NEW_MoveToDirection m_isCmrRot branch folds fDirRot to fSigDirRot/fRotRat and applies CCamera::Roll(-m_fCmrRotSpd*fElapsedTime*fRotRat/90) every frame a direction key is held and the movement gates pass)", "pre-fix keyboard_motion_timeline_test.gd (FAIL by construction: camera.yaw never written by _keyboard_wish/_process, new directional assertions would fail)", "post-fix godot --headless --path project --script keyboard_motion_timeline_test.gd (PASS)", "godot --headless --path project --script test_wasd_steering_parity.gd (PASS)", "godot --headless --path project --script test_no_auto_move_regression.gd (PASS)", "godot --headless --path project --script game_camera_test.gd (PASS)", "godot --headless --path project --script movement_parity_test.gd (PASS)", "godot --headless --path project --script player_move_test.gd (PASS)", "godot --headless --path project --script mouse_controller_test.gd (PASS)", "godot --headless --path project --script test_alignment_parity.gd (PASS)", "git diff --check (PASS)", "audit_ledger.py refresh --write/report --write/validate (PASS)", "cmake --build build-debug -j4 / ctest (BLOCKED: pre-existing stale CMakeCache.txt from a different checkout path, unrelated to this GDScript-only change; not run)", "godot --headless --path project --script player_motion_test.gd (BLOCKED: pre-existing parse error, GDScriptNativeClass.clear_texture_cache() not found in this Godot 4.7.1 build; file untouched this round, last changed in a prior commit)"], "reason": "按 40250 CPythonPlayer::NEW_MoveToDirection 的 m_isCmrRot 分支,在 player_controller.gd 新增 _camera_auto_rotate():按住方向键且通过 frozen/locked/private_shop_open/processing_emotion 门后,每帧按方向相对相机的方位角折算 fRotRat(正前/正后 0,正左右 ±90 最大速率,对角线减半),以 m_fCmrRotSpd=20.0 的速率持续把 camera.yaw 转向新朝向。D3D CCamera::Roll 与 Godot yaw 的转向手性相反,增量对 fRotRat 取正号(参考实现取负号),作为显式记录的平台适配,用『按住方向时相机朝该方向收敛、正前后不转、从不越界』的不变式核验,而非逐位对照 D3D 数值。未修改飞行物、技能、战斗、UI 或根运动/资源事件路径;Paralysis/Faint/Sleep 未接入移动门(新发现,记录于 Remaining,未修复)、根运动/AccumulationMovement、同步/受击门恢复顺序、真实模型姿态测试仍未闭合,合同保持 PARTIAL。"}
{"time": "2026-09-22T01:30:00Z", "event": "audit_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/canact-paralysis-faint-sleep-gate"], "files": ["audit/contracts/movement.keyboard-motion.md", "audit/manifest.json", "audit/history.jsonl"], "tests": ["40250 reference source reachability audit (exhaustive grep -arn across UserInterface/ + GameLib/ for SetParalysis/Paralysis, AFFECT_SLEEP/SetSleep, SetFaint/FaintTest, Stun/RecvStunPacket)"], "reason": "Investigated the movement.keyboard-motion.md Remaining item \"CActorInstance::CanAct() Paralysis/Faint/Sleep movement gates not wired into net_play.gd\" (flagged unfixed by the prior round). Findings: (1) IsParalysis()/m_isParalysis has exactly one setter in the whole reachable 40250 source, CInstanceBase::__Shaman_SetParalysis (InstanceBaseEffect.cpp:821-823), which itself has zero callers anywhere in the tree -- Paralysis is dead code, never true in live gameplay, so not wiring it is correct parity, not a gap. (2) IsSleep()/m_isSleep: the AFFECT_SLEEP case in SetAffect()s switch is commented out (dead); the only live setter is the AFFECT_STUN case (m_GraphicThingInstance.SetSleep(isVisible), ~InstanceBaseEffect.cpp:932-933) -- IsSleep() is functionally just an alias for AFFECT_STUN in practice, which the current clients Entity.stunned field already represents (entity_store.h:119) and net_play.gd::_can_process_network_state() already gates movement on (dead/stunned/knock_down) -- already covered, not a gap. (3) IsFaint()/m_isFaint: only setter is chrFaintTest() (PythonCharacterModule.cpp:1087-1105), a Python-bound debug helper (\"FaintTest\", registered alongside chrtestRestoreRenderMode) operating on GetSelectedInstancePtr(); the available 40250 checkout has no root/ Python UI/quest scripts to prove or disprove live-gameplay reachability -- evidence-blocked, left unresolved per SKILL.md do-not-audit-unreachable-code-as-product-behavior rule, no fix attempted. Byproduct finding while tracing IsStun(): CActorInstance::Stun() is only reachable via RecvStunPacket() (PythonNetworkStreamPhaseGame.cpp:1556-1578), which explicitly routes GC_STUN to Die() for the main character and to Stun() only for remote actors; verified this does NOT create a movement.keyboard.motion discrepancy because CanAct() blocks movement identically on IsDead() and IsStun(), and net_play.gd::_can_process_network_state() already blocks movement identically on dead and stunned -- the movement-blocking outcome is already correct parity regardless of which reference path fires. The Die()-vs-Stun() distinction only affects death-system semantics (m_isRealDead, EXP drop, corpse/respawn flow), which is combat.death.exp_drop/combat.affect_status territory and is already tracked there (combat.affect-status.md Remaining #2, equivalence rows 63/67: native GC_STUN only sets stunned=true with no dedicated clear path) -- not re-logged as a new finding to avoid duplicate tracking. No code changed this round; removed the resolved Remaining bullet from manifest.json and movement.keyboard-motion.md, replaced with investigation evidence and citations. Contract stays PARTIAL (root-motion/resource-event-source/sync-hit-death-gate-stacking/real-model-bone-flip items remain open)."}
{"time": "2026-09-22T03:15:00Z", "event": "implementation_fix_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/server-speed-scale-clamp"], "files": ["project/player_controller.gd", "project/keyboard_motion_timeline_test.gd", "audit/contracts/movement.keyboard-motion.md", "audit/manifest.json", "audit/history.jsonl", "audit/remediation-roadmap.md", "audit/reports/coverage.md"], "tests": ["40250 reference source inspection (PASS: InstanceBaseMovement.cpp CInstanceBase::SetMoveSpeed(UINT uMovSpd) only guards uMovSpd>1100 -> 0; otherwise applies uMovSpd/100.0f unclamped)", "extension/src/net/entity_store.cpp EntityStore::motion_move_speed re-read (PASS: already-fixed remote-entity path mirrors the same >1100 -> 0 / mount_vnum!=0 -> 0 guards, no [0.25,3.0]-style band)", "pre-fix player_controller.gd::set_server_speed (FAIL by construction: clampf(moving_speed/100.0, 0.25, 3.0) silently floors/ceils haste or slow stacks outside the reference's 25..300 movSpd band)", "post-fix godot --headless --path project --script keyboard_motion_timeline_test.gd (PASS, incl. new set_server_speed(10/500/1200/100) assertions)", "godot --headless --path project --script test_wasd_steering_parity.gd (PASS)", "godot --headless --path project --script test_no_auto_move_regression.gd (PASS)", "godot --headless --path project --script game_camera_test.gd (PASS)", "godot --headless --path project --script movement_parity_test.gd (PASS)", "godot --headless --path project --script player_move_test.gd (PASS)", "godot --headless --path project --script mouse_controller_test.gd (PASS)", "godot --headless --path project --script test_alignment_parity.gd (PASS)", "godot --headless --path project --script netplay_test.gd (PASS; unrelated mock set_server_speed in netplay_test.gd has no clamp, unaffected)", "git diff --check (PASS)", "audit_ledger.py refresh --write/report --write/validate (PASS)"], "reason": "在同一合同下追查『根运动 vs 连续速度』假设时发现这是假线索——entity_store.cpp::advance_walk_by_motion 与本地 _process() 的位移积分在种类上同构(都是恒速直线插值);真正的差异是 set_server_speed() 把 moving_speed/100.0 塞进无参考依据的 clampf(0.25, 3.0),而参考实现 CInstanceBase::SetMoveSpeed 和本项目已修复的 EntityStore::motion_move_speed 都只有 moving_speed>1100 -> 0 这一个门。移除该私有夹钳,改为 0.0 if moving_speed > 1100 else moving_speed/100.0,两处调用方(net_world.gd、net_play.gd)均不额外夹钳,改动范围限于 player_controller.gd。新增回归断言覆盖旧边界内外和 1100 冻结门。副产品发现:CInstanceBase::SHORSE::SetMoveSpeed 是坐骑上玩家的独立速度设定,只在 IsMounting() 时生效,与远端 mount_vnum!=0 -> 0 的处理方式不同——本轮未下定论,留作独立候选。未修改根运动/AccumulationMovement 适配层、资源与网络事件完整顺序、同步/受击门叠加或真实模型测试,合同保持 PARTIAL。"}
+10 -10
View File
@@ -283,8 +283,8 @@
],
"fingerprints": {
"reference": "20c74407914aba18dde8f76f7b86917372a56cc7333b2ca8bfeee76c39e357e4",
"implementation": "0ad8d1ee88bce90cd1ccf5b8dc26f8e37541e91056a2c32eb3202b07b1d762d5",
"tests": "57893b1268088b1b393dac6a94142e792ab697e55cb29a22234df4bb93cac034"
"implementation": "ce5cc765134cfa04f7cac39ca268cd7bec773f72ea17b3b529f276fb7226b580",
"tests": "2c79ef6e727717527f54e611629ce15154cb78c785b37ada4970d1156caf1bfd"
}
},
{
@@ -392,7 +392,7 @@
"notes": "本轮已按 40250 SendClickItemPacket/GetCloseItem/__OnPressItem 修复 party/anti-flag 所有权、DISTANCE_APPROX/300cm 选择、最高 VID tie-break、骑马固定边界和 150cm 点击入口距离门;并修复 RecvCreateFlyPacket 在本地主角 EntityStore 行暂缺时通过 GetMainInstancePtr 语义回退 _local_node,同时要求远端起点/终点实例存在后才创建 indexed fly。相关回归通过;真实服务端死亡包序、重复/乱序/重连/拾取竞争、死亡资源生命周期和跨图清理仍保持 PARTIAL。",
"fingerprints": {
"reference": "6a3545f5d1f424e1404c3dd6a66f49dfa28a105b639fce5f8009860fa50928b6",
"implementation": "12eea2672396df3fb36c8ac2b4ab10f6ec1ec9824b6e33e095a7dfb4cc3251c7",
"implementation": "2fd306a9f4b85b78fd75fbffb3512cf1939fed11a3ac11a29f5808d890936565",
"tests": "f066c45415ecd31e61da172c633c1fd7d29f89a8d6a8c5a7a13ca2f47b946af8"
}
},
@@ -1683,7 +1683,7 @@
"notes": "本轮补充核对了 Smart press/hold/release、click miss 的停止语义、运行时距离门、真实 MSM 碰撞球、第二碰撞 BlockMovement 和对象 picking 的差异;四组移动/点选回归均通过,但 click/target/fly 测试有 3 个 ObjectDB 泄漏和 1 个资源退出警告。当前仍不能用固定半径替代真实 MSM sphere,合同保持 PARTIAL。",
"fingerprints": {
"reference": "15850029efadc3a50e52511fb45f51da5ad77e6ce0841a41a53d66fd6306d03d",
"implementation": "7996dad026befa17ce75b4146ddf06774dabb899382f5a55810cba532feb6ad3",
"implementation": "d2e8fc11d36e5f898a4aaea356ce4bee03d1d43b7dea7101c5c610a74adefd76",
"tests": "cdee05a904f96b260c201553915af522bd3623eed78ffb5b23d16ae8e0b2a88a"
}
},
@@ -1902,7 +1902,7 @@
"fingerprints": {
"reference": "f59276d75e5cdf0fc54c95fdce5ca85c794080b94ba7fdee540870273004a471",
"implementation": "01ca0b13b2db5489c1fa37dc48d555d68da096dc2d2370d9d64b2cf78c8f9fc7",
"tests": "7126549b43695f0569d9ed8aaff9080404cf4cd3cbc9aecb8aa0ea7a825ae2af"
"tests": "5815d9275c50109a98b6f21175c4ab274a0eb7cd4b4849dcf4b973a0b4c395d6"
}
},
{
@@ -1961,7 +1961,7 @@
"notes": "已按 40250 isValidAttacking/HitDataContainer 删除缺失动作攻击数据、空命中窗和缺少 .msm 防御球时的本地合成命中;状态包、命中窗、去重、受击、弓箭 FLY、本地主角伤害飘字和 WeaponTrace 多部件生命周期有回归证据。WeaponTrace 目前通过 weapon_trace.gd 适配层接入,包含武器类型过滤、每个 Weapon/Shield 部件独立 trace、双轨迹时间队列、cubic spline triangle strip、alpha/texture 材质、攻击动作开关、纹理控制入口、bound-box 最大长度/局部 Z/reach scale 和换装清理;AFFECT_GEOMGYEONG 的 1.5/1.0 reach scale 已同时接入刀光与普通攻击扫掠。native 骨骼矩阵坐标语义、远端/怪物覆盖、真实视觉资源及服务端伤害闭环仍不等价。强化配置中的 has_trail 仍是独立未消费字段。",
"fingerprints": {
"reference": "3b79deef2e50cb16c6b7226e8900e4d96c82f5dbe4e409f40609b6a656586608",
"implementation": "c2226db3856b7499f505d6e26b09f4402c36bd82848f6893ce8426a283ea2a66",
"implementation": "fa633ddfb631df0d1b472ad674d558869399079d9789b54b6c6775e45b8310ac",
"tests": "228e1bb5ebb0ef52822956e3d33d9c51d55cc2090c323300b466f937249e5d3c"
}
},
@@ -2063,7 +2063,7 @@
"notes": "已完成目标攻击资格、目标可选中、目标框和安全区的逐分支静态审计;目标入口已统一接入 CanChangeTarget/CanPickInstance、重复目标窗口和 CG_TARGET(0) 清理,合同保持 PARTIAL。",
"fingerprints": {
"reference": "818e201fffe6335de4b2c79b505d8749b7ef00d61267865861a8fefc21c5bdf5",
"implementation": "073975693379576d0b2f3e2418a1b942c1af4796484b0380a680b39b0a65245e",
"implementation": "07fcaf36da0b7322ac63c5d2b3ec1001b6fff2c1810dd637c54b3fecfba430fb",
"tests": "9ad10ac749fbc12f4537fe489f592ac8e2159446c6bb1ab01855076c3b6c39ec"
}
},
@@ -2754,7 +2754,7 @@
"notes": "本轮完成掉落/拾取/所有权/名条/协议逐分支审计;基础回归通过但资源、权限和反馈链仍不等价,合同保持 PARTIAL。",
"fingerprints": {
"reference": "18e94abecd2cbd63407ec94fe4f2b692505b0ea156e3149fcfa426920a10856b",
"implementation": "d67db273d2baea25c2b7e21ec6f930d08aae35821370b13b98dd849b9b7541f4",
"implementation": "664112e5403d65b499075c104656efec9e2920da2f5dca0edec66339e9005b66",
"tests": "0713f0aeef0e53fdf447537c2a8e785559269b45ca63825c4b507382fbc95019"
}
},
@@ -4015,7 +4015,7 @@
"notes": "已完成地形/水面/碰撞/拾取首轮逐分支审计;高度、桥面和水面 happy path 已验证,但对象 ray picking、水层高度、高半字节属性和 streaming/清理边界保持 PARTIAL。",
"fingerprints": {
"reference": "46da799d71b3ad4ec459ebaa5364dac82a1cd503a30c55692ccadaf183ade1cc",
"implementation": "70a4c4f810b3fd2e3f01b42ceeb73036a0f17f31e9ba3b210e4f84d12d15c7f1",
"implementation": "0492040f92b685f42e6d553cb50bdf6e67ff22bf757a886f22b9ee230df6912b",
"tests": "a2a38be2cfb31518ed7f30aab7a2aeb324b71dce353271fa4ea1f330d21195f3"
}
},
@@ -4195,7 +4195,7 @@
"notes": "已完成首轮逐分支审计;移动端基础适配测试通过,但相机默认/恢复、鼠标统一分派、光标状态和 IME 仍未达到 40250 等价。",
"fingerprints": {
"reference": "11181965574e3b0c3e235b1403e17494f8fc1b8777a2b18b955f53a84e0bae54",
"implementation": "086901e59cc5768fd74062b39ad4de534827cd0195f6c92b3d8a0885219e203d",
"implementation": "d67243d6e4c881a79c37af554eeaeca5374c3a73caf7d6e96f8e8abdcabce9bf",
"tests": "eb158440ac1fcfce57259aad60a090493d2cebaeab7e380c3513cb34563a90fd"
}
},
+27
View File
@@ -306,3 +306,30 @@ GC_STUN 主角分流。**
`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`
本地结构和 owner 分支均已覆盖,只等新的真实服务端/Windows fixture;在没有新 fixture 前不要
重复 `/logout` 探针或重复修改已闭合的 owner 分支。
### 2026-09-22T03:15Z`movement.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_parity`
`test_no_auto_move_regression``game_camera_test``movement_parity_test``player_move_test`
`mouse_controller_test``test_alignment_parity``netplay_test`)通过。详见
`audit/contracts/movement.keyboard-motion.md``Implementation 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`)——五者均仍未闭合。
+17
View File
@@ -196,6 +196,23 @@ func _init() -> void:
pc._input(_key(KEY_A, false))
pc.stop()
# CInstanceBase::SetMoveSpeed(UINT uMovSpd) applies uMovSpd/100.0f with no
# lower/upper clamp besides the >1100 -> 0 (frozen) guard; a haste stack or a
# heavy slow debuff must scale local prediction exactly like it already
# scales remote actors in EntityStore::motion_move_speed.
pc.set_server_speed(10)
_ck(is_equal_approx(pc.server_speed_scale, 0.1),
"set_server_speed is unclamped below the old 0.25 floor")
pc.set_server_speed(500)
_ck(is_equal_approx(pc.server_speed_scale, 5.0),
"set_server_speed is unclamped above the old 3.0 ceiling")
pc.set_server_speed(1200)
_ck(is_equal_approx(pc.server_speed_scale, 0.0),
"set_server_speed freezes to 0 past the 1100 SetMoveSpeed guard")
pc.set_server_speed(100)
_ck(is_equal_approx(pc.server_speed_scale, 1.0),
"set_server_speed keeps the 100 baseline at scale 1.0")
player.free()
pc.free()
cam.free()
+7 -1
View File
@@ -274,7 +274,13 @@ func set_server_speed(moving_speed: int) -> void:
# pre-spawn zero values from freezing local prediction.
if moving_speed <= 0:
return
server_speed_scale = clampf(float(moving_speed) / 100.0, 0.25, 3.0)
# CInstanceBase::SetMoveSpeed(UINT uMovSpd): only guard is uMovSpd > 1100 ->
# 0 (frozen walk pose, no root-motion advance); otherwise uMovSpd/100.0f is
# applied unclamped. EntityStore::motion_move_speed already mirrors this for
# remote actors; there is no reference basis for an arbitrary [0.25, 3.0]
# band on the local player, so a big haste stack or heavy slow debuff must
# scale local prediction the same way it scales everyone else's remote view.
server_speed_scale = 0.0 if moving_speed > 1100 else float(moving_speed) / 100.0
func set_input_surfaces(cursor: Node, ui: Node = null, ground: Node = null,
cancel_fishing_cb := Callable(), ground_cancel_fishing_cb := Callable()) -> void: