fix(motion): #39 — handle backward sign + sidestep in ApplyPlayerLocomotionRefinement

User report from fix #3 visual verify (commit 2653b30):
- Forward Run↔Walk Shift toggle: WORKS now
- Strafe Shift toggle: no transition (was out of scope)
- "When I shift walk backwards, the retail char gets animated walking
  slow forward but blipping backwards" — REGRESSION

Root cause of the backward regression: ACE encodes WalkBackward as
`WalkForward` motion with NEGATIVE speedMod (MovementData.cs:115
`interpState.ForwardSpeed *= -0.65f`). My fix #1's hysteresis branches
treated lowByte 0x05 / 0x07 as "forward" and computed positive speedMod
from horizSpeed, overwriting the negative sign. Result: animation
played forward-walk while body kept moving backward (the rubber-band).

Strafe gap: sidestep (low byte 0x0F / 0x10) wasn't in fix #1's scope,
so ApplyPlayerLocomotionRefinement returned early for sidestep cycles.
Retail does the same wire-silence on Shift toggle for sidestep, so
observer-side cycle refinement must also fire for it.

Fix:
- Probe `currentSign = sign(CurrentSpeedMod)` to detect backward direction
- For sidestep (lowByte 0x0F or 0x10): keep motion ID, refine
  speedMod magnitude = horizSpeed / WalkAnimSpeed, preserve sign
- For backward (forward-class lowByte AND currentSign < 0): keep
  WalkForward motion (per ACE encoding), refine magnitude, preserve
  negative sign — no "RunBackward" motion exists, only |speedMod|
  changes between Walk-back (~0.65) and Run-back (~1.91 = runRate × 0.65)
- Forward (currentSign >= 0): existing Walk↔Run hysteresis unchanged

Build clean. Diagnostics: [UPCYCLE_PLAYER] line still prints; the
new sidestep / backward branches use the same SetCycle call so
their decisions appear in [SCFULL] / [CURRNODE] for inspection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-06 08:36:22 +02:00
parent 2653b307c7
commit cc62e1cfde

View file

@ -3448,14 +3448,24 @@ public sealed class GameWindow : IDisposable
uint currentMotion = ae.Sequencer!.CurrentMotion; uint currentMotion = ae.Sequencer!.CurrentMotion;
uint lowByte = currentMotion & 0xFFu; uint lowByte = currentMotion & 0xFFu;
float currentSign = MathF.Sign(ae.Sequencer.CurrentSpeedMod);
if (currentSign == 0f) currentSign = 1f;
// Forward-only refinement scope. WalkForward = 0x05, RunForward = 0x07. // Recognised locomotion directions:
// Sidestep (0x0F/0x10), WalkBackward (0x06), turns and any other // 0x05 (WalkForward) — also encodes WalkBackward via negative speed
// motion (emote, attack, etc.) are left to UM-driven SetCycle. // (ACE convention: SidestepCommand= cancel, ForwardCommand=
const uint LowWalkForward = 0x05u; // WalkForward, ForwardSpeed *= -0.65)
const uint LowRunForward = 0x07u; // 0x07 (RunForward)
bool isForward = lowByte == LowWalkForward || lowByte == LowRunForward; // 0x0F (SideStepRight)
if (!isForward) return; // 0x10 (SideStepLeft)
// Other motions (Ready, Turn, emotes, attacks) are left to UM-driven SetCycle.
const uint LowWalkForward = 0x05u;
const uint LowRunForward = 0x07u;
const uint LowSideStepRight = 0x0Fu;
const uint LowSideStepLeft = 0x10u;
bool isForwardClass = lowByte == LowWalkForward || lowByte == LowRunForward;
bool isSidestep = lowByte == LowSideStepRight || lowByte == LowSideStepLeft;
if (!isForwardClass && !isSidestep) return;
float horizSpeed = MathF.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y); float horizSpeed = MathF.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y);
@ -3468,7 +3478,54 @@ public sealed class GameWindow : IDisposable
uint targetMotion; uint targetMotion;
float speedMod; float speedMod;
if (lowByte == LowRunForward)
if (isSidestep)
{
// Sidestep: motion ID stays the same (SideStepLeft / SideStepRight).
// Retail's wire encoding for sidestep speed buckets uses the same
// motion ID with different SidestepSpeed (slow ≈ 1.25 multiplier,
// fast ≈ 3.0 clamp per ACE MovementData.cs:124-131). On Shift
// toggle while a strafe key is held, retail does NOT broadcast a
// fresh MoveToState (same wire-silence rule as the forward case),
// so observer-side cycle refinement must come from UP-derived
// velocity here. Preserve the sign — SideStepLeft is sometimes
// emitted with negative speedMod by the adjust_motion path.
//
// Magnitude: horizSpeed / WalkAnimSpeed maps the observed speed
// back to a speedMod the sequencer can apply as a framerate
// multiplier. WalkAnimSpeed is the reasonable base because
// sidestep cycles use the WalkAnim equivalent (no separate
// RunSidestep cycle in the dat).
float sideMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed;
sideMag = MathF.Min(MathF.Max(
sideMag,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
targetMotion = currentMotion;
speedMod = sideMag * currentSign;
}
else if (currentSign < 0f)
{
// BACKWARD walk: ACE encodes WalkBackward as `WalkForward` motion
// with NEGATIVE speedMod (MovementData.cs:115 `interpState.ForwardSpeed *= -0.65f`).
// No "RunBackward" motion exists — Shift toggle on backward
// changes only the magnitude of speedMod (slow back ≈ -0.65,
// fast back ≈ -1.91 = -runRate × 0.65). Keep WalkForward motion,
// refine magnitude, preserve negative sign.
//
// Without this branch (the original fix #1), backward refinement
// computed a positive speedMod from horizSpeed and overwrote the
// negative sign, making the legs play forward-walk while the body
// continued moving backward (the rubber-banding the user reported).
float backMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed;
backMag = MathF.Min(MathF.Max(
backMag,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
targetMotion = AcDream.Core.Physics.MotionCommand.WalkForward;
speedMod = -backMag;
}
else if (lowByte == LowRunForward)
{ {
if (horizSpeed < PlayerRunDemoteSpeed) if (horizSpeed < PlayerRunDemoteSpeed)
{ {
@ -3489,7 +3546,7 @@ public sealed class GameWindow : IDisposable
} }
else else
{ {
// currently WalkForward (0x05) // currently WalkForward (0x05) with positive speedMod = walking forward.
if (horizSpeed > PlayerRunPromoteSpeed) if (horizSpeed > PlayerRunPromoteSpeed)
{ {
targetMotion = AcDream.Core.Physics.MotionCommand.RunForward; targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;