fix(motion): #39 candidate — un-gate UP velocity-cycle for player remotes (forward only)
Adds a player-remote velocity-fallback path to ApplyServerControlledVelocityCycle so that when retail (the actor) toggles Shift while holding W and acdream is the observer, the visible leg cycle switches Run↔Walk within ~200–500 ms even though no fresh UM arrives. Static analysis (ACE GameActionMoveToState + MovementData.cs auto-upgrade + acdream's prior diag traces) suggests retail does NOT broadcast a fresh MoveToState on HoldKey-only changes — acdream's UMs handle direction-key changes and our local +Acdream's transitions, but retail-driven actors leave the cycle stuck. Changes (all in src/AcDream.App/Rendering/GameWindow.cs): - New RemoteMotion.LastUMTime field, stamped in OnLiveMotionUpdated - ApplyServerControlledVelocityCycle: removed inner IsPlayerGuid gate; routes player remotes to new ApplyPlayerLocomotionRefinement - ApplyPlayerLocomotionRefinement (forward-direction only): - 500 ms UM grace window (UMs win when fresh) - Forward-direction-only (low byte 0x05 / 0x07) - Hysteresis: Run → Walk demote at < 4.5 m/s; Walk → Run promote > 5.5 m/s - Skip SetCycle when neither motion ID nor speedMod changed meaningfully - [UPCYCLE_PLAYER] diag gated on ACDREAM_REMOTE_VEL_DIAG=1 - Outer call site in OnLivePositionUpdated un-gated (!IsPlayerGuid removed); per-remote routing now lives inside the function Scope: case #1 (Run↔Walk forward) only. Cases #2–#7 (backward, sidestep speed-buckets, direction-flips) remain deferred — PlanFromVelocity is forward-only and its NPC-tuned thresholds (RunThreshold=1.25) do not separate player Walk (~2.5 m/s) from player Run (~9 m/s); a TTD trace of retail's per-direction algorithm should ground the wider fix. ISSUES.md #39 updated with progress; investigation-prompt.md and a new findings-static.md committed under docs/research/2026-05-06-locomotion-cycle-transitions/ (the prompt was authored on a parallel branch in commit 7a38da3 and is brought into this worktree here so the next session can find it without branch-hopping). Build clean. The 8 pre-existing test failures on this branch (BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope, MotionInterpreter WalkBackward GetMaxSpeed, etc.) are unrelated to this change — verified by running them with the diff stashed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5f2e2e28ff
commit
8fa04af4c7
4 changed files with 679 additions and 8 deletions
|
|
@ -0,0 +1,167 @@
|
|||
# Locomotion-cycle transitions — static-analysis findings
|
||||
|
||||
**Date:** 2026-05-06 (follow-up to `investigation-prompt.md`)
|
||||
**Status:** static analysis complete; candidate fix shipped for case #1; cases #2–#7 deferred until TTD validation.
|
||||
|
||||
This is what code reading + ACE cross-reference + named-retail grep
|
||||
established before any TTD/cdb trace was run. It scopes the candidate
|
||||
fix and identifies which questions still need the trace.
|
||||
|
||||
---
|
||||
|
||||
## What's confirmed by static analysis
|
||||
|
||||
### 1. ACE broadcasts unconditionally on inbound MoveToState
|
||||
|
||||
`references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionMoveToState.cs:36`
|
||||
calls `session.Player.BroadcastMovement(moveToState)` for every received
|
||||
MoveToState, no diff-check. So **whether ACE broadcasts a UM is determined
|
||||
purely by whether the client sent a MoveToState**.
|
||||
|
||||
### 2. ACE auto-upgrades `WalkForward + HoldKey.Run → RunForward` on broadcast
|
||||
|
||||
`references/ACE/Source/ACE.Server/Network/Motion/MovementData.cs:110-113`
|
||||
shows the conversion: when client sends `WalkForward + HoldKey.Run`, the
|
||||
broadcast `interpState.ForwardCommand` is upgraded to `RunForward`. So
|
||||
the broadcast UM byte differs between Run and Walk even though the
|
||||
client's wire byte is the same `WalkForward`.
|
||||
|
||||
### 3. Therefore the bug is one of these two
|
||||
|
||||
Either:
|
||||
- **A.** Retail (the actor) does NOT send a fresh MoveToState when only
|
||||
HoldKey changes (Shift toggle while a direction key is held). ACE has
|
||||
nothing to broadcast → no UM arrives at the observer. UPs continue
|
||||
with new velocity (run-pace ↔ walk-pace), but observer's UM-driven
|
||||
cycle never updates.
|
||||
- **B.** Retail DOES send a fresh MoveToState. ACE broadcasts a UM. Our
|
||||
parser receives it but fails to apply the cycle change for some
|
||||
reason.
|
||||
|
||||
The diagnostic data captured in 2026-05-03's investigation
|
||||
(`[FWD_WIRE]`, `[UM_RAW]`, `[SETCYCLE]`) is more consistent with **A**:
|
||||
no `[FWD_WIRE]` line was logged for Shift toggles, suggesting no UM
|
||||
arrived. But that data was for a different scenario, so a fresh trace
|
||||
is needed.
|
||||
|
||||
**This question is the primary gap the TTD/cdb trace must close.**
|
||||
A 2-minute cdb session with a breakpoint counter on
|
||||
`acclient!CommandInterpreter::SendMovementEvent` answers it
|
||||
definitively.
|
||||
|
||||
### 4. `ApplyServerControlledVelocityCycle` was gated against player remotes at three sites
|
||||
|
||||
In `src/AcDream.App/Rendering/GameWindow.cs` before this change:
|
||||
|
||||
- **Inner gate**, function entry (~line 3326):
|
||||
`if (IsPlayerGuid(serverGuid)) return;`
|
||||
- **Outer gate**, call site in `OnLivePositionUpdated` (~line 3683):
|
||||
`if (!IsPlayerGuid(update.Guid) && rmState.HasServerVelocity ...)`
|
||||
- **TickAnimations gate** (~line 6325):
|
||||
`if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity)` —
|
||||
this one is for the "stale velocity → reset to zero" path, used by
|
||||
NPCs whose MoveTo has expired. Leave as-is for now; player remotes
|
||||
don't go through this branch (no MoveTo path).
|
||||
|
||||
### 5. `ServerControlledLocomotion.PlanFromVelocity` thresholds are wrong for player speeds
|
||||
|
||||
In `src/AcDream.Core/Physics/ServerControlledLocomotion.cs:30-33`:
|
||||
|
||||
```csharp
|
||||
public const float StopSpeed = 0.20f;
|
||||
public const float RunThreshold = 1.25f;
|
||||
```
|
||||
|
||||
For player speeds (Walk ≈ 2.5 m/s, Run ≈ 9 m/s — both observed in
|
||||
prior `[VEL_DIAG]` traces), `RunThreshold = 1.25` is below both
|
||||
buckets. Both speeds resolve to `RunForward`. So even if we just
|
||||
un-gated the existing function for player remotes, the function would
|
||||
*always* return `RunForward` and the bug would persist.
|
||||
|
||||
The function works correctly for NPCs (typical NPC speed range
|
||||
1–6 m/s falls on the right side of the 1.25 threshold for the
|
||||
Idle/Walk/Run distinction).
|
||||
|
||||
**Implication:** the player path must use different thresholds. The
|
||||
candidate fix introduces hysteresis tuned for player speeds (4.5 demote,
|
||||
5.5 promote) and routes player remotes through a dedicated
|
||||
`ApplyPlayerLocomotionRefinement` helper instead of `PlanFromVelocity`.
|
||||
|
||||
### 6. `PlanFromVelocity` returns Forward-only motions
|
||||
|
||||
It returns `Ready`, `WalkForward`, or `RunForward` — never sidestep,
|
||||
never backward. Even with thresholds fixed, calling it for a sidestepping
|
||||
player remote would clobber `SideStepLeft`/`Right` with `RunForward`.
|
||||
|
||||
**Implication:** the player path must scope itself to forward direction
|
||||
only; sidestep/backward HoldKey toggles are deferred until TTD confirms
|
||||
retail's per-direction algorithm.
|
||||
|
||||
---
|
||||
|
||||
## What the candidate fix does (and doesn't)
|
||||
|
||||
**Does:**
|
||||
|
||||
- Adds `RemoteMotion.LastUMTime` (timestamp of most recent UM for a remote).
|
||||
- Stamps `LastUMTime` at the top of `OnLiveMotionUpdated`.
|
||||
- Removes the `IsPlayerGuid` early return inside
|
||||
`ApplyServerControlledVelocityCycle`.
|
||||
- Removes the `!IsPlayerGuid` gate at the outer call site in
|
||||
`OnLivePositionUpdated` (so player remotes get the function called).
|
||||
- Routes player remotes through new `ApplyPlayerLocomotionRefinement`:
|
||||
- 500 ms UM grace window (UMs are authoritative).
|
||||
- Forward-direction-only refinement (low byte 0x05 / 0x07).
|
||||
- Hysteresis: Run → Walk demote at < 4.5 m/s; Walk → Run promote at
|
||||
> 5.5 m/s.
|
||||
- SetCycle skipped if neither motion ID nor speedMod changed
|
||||
meaningfully.
|
||||
- Diagnostic `[UPCYCLE_PLAYER]` line gated on `ACDREAM_REMOTE_VEL_DIAG=1`.
|
||||
|
||||
**Does NOT (deferred to TTD-validated follow-up):**
|
||||
|
||||
- Backward (case #2) HoldKey toggle.
|
||||
- Sidestep speed-bucket refinement (cases #4, #5).
|
||||
- Direction-flip transitions (cases #3, #6, #7) — these are believed
|
||||
to work today via UM-driven SetCycle, but the prompt's acceptance
|
||||
criteria explicitly call for visual confirmation of all 7 cases.
|
||||
- Tuning the grace window or the hysteresis thresholds against retail's
|
||||
exact behavior. 500 ms / 4.5 / 5.5 are defensible defaults but TTD
|
||||
may show retail uses different values.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance for the candidate fix (case #1 only)
|
||||
|
||||
When acdream observes a retail-driven character:
|
||||
|
||||
- Hold W (run) for 2 s, then press Shift, then release Shift, then
|
||||
release W. The visible leg cycle should switch Run → Walk → Run
|
||||
→ Idle within ~200–500 ms of each transition.
|
||||
- All other working cases (acdream-on-acdream, retail-on-acdream,
|
||||
Idle ↔ Run, Idle ↔ Walk) must still work — no regressions.
|
||||
|
||||
If the candidate fix produces visible Run↔Walk transitions on retail
|
||||
actors, ship it (close part of #39, file the remaining 6 cases as a
|
||||
follow-up issue) and run TTD on cases #2–#7 next.
|
||||
|
||||
If it doesn't switch the cycle (or thrashes / regresses something),
|
||||
the next investigation step is the cdb breakpoint count on
|
||||
`CommandInterpreter::SendMovementEvent` to settle question A vs. B
|
||||
above before any further fix attempt.
|
||||
|
||||
---
|
||||
|
||||
## Files touched by the candidate fix
|
||||
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs`
|
||||
- Added `RemoteMotion.LastUMTime` field
|
||||
- Stamped `LastUMTime` at top of `OnLiveMotionUpdated`
|
||||
- Removed inner `IsPlayerGuid` gate in `ApplyServerControlledVelocityCycle`
|
||||
- Routed player remotes to new `ApplyPlayerLocomotionRefinement`
|
||||
- Added constants `UmGraceSeconds`, `PlayerRunPromoteSpeed`,
|
||||
`PlayerRunDemoteSpeed`
|
||||
- Removed `!IsPlayerGuid` gate at outer call site in `OnLivePositionUpdated`
|
||||
|
||||
No changes to `ServerControlledLocomotion.cs`, `MotionInterpreter.cs`,
|
||||
or `AnimationSequencer.cs`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue