fix(motion): preserve signed TurnSpeed for remote turn animations

The wire-arrival animCycle picker in OnLiveMotionUpdated was passing
MathF.Abs(turnSpeed) to the sequencer, stripping the sign that ACE uses
to encode TurnLeft. Confirmed via live wire trace 2026-05-03: TurnLeft
input from a retail-driven character arrives as
turnCmd16=0x000D (TurnRight), TurnSpeed=-1.500 — mirroring retail's
adjust_motion convention on the wire. With Abs, both directions
collapsed onto motion=TurnRight + speedMod=+1.5, and the synthesize-
omega path computed -2.25 (CW = right) for both. Visible symptom:
TurnLeft animated as TurnRight then blipped to the correct facing on
the next UpdatePosition.

Pass the signed speed through unchanged. The sequencer's negative-
speed path (EnqueueMotionData multiplies MotionData.Omega by speedMod;
the synthesize-omega fallback uses -(pi/2)*adjustedSpeed) produces the
correct CCW omega for TurnLeft now that the sign survives.

Also adds a TURN_WIRE diagnostic gated on ACDREAM_REMOTE_VEL_DIAG=1
that prints every wire-arrived TurnCommand with reconstructed enum
and signed speed, plus splits the OMEGA_DIAG throttle off
LastVelDiagLogTime onto its own LastOmegaDiagLogTime so the two
diagnostics don't starve each other.

Verified with the same trace: TURN_WIRE speed=-1.500 -> OMEGA_DIAG
Z=+2.250 (CCW = TurnLeft), TURN_WIRE speed=+1.500 -> OMEGA_DIAG
Z=-2.250 (CW = TurnRight). Both directions now have correct sign.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-03 13:01:43 +02:00
parent 0997f96078
commit 9960ce3bce

View file

@ -376,6 +376,7 @@ public sealed class GameWindow : IDisposable
public System.Numerics.Vector3 PrevServerPos; public System.Numerics.Vector3 PrevServerPos;
public double PrevServerPosTime; public double PrevServerPosTime;
public double LastVelDiagLogTime; public double LastVelDiagLogTime;
public double LastOmegaDiagLogTime;
public RemoteMotion() public RemoteMotion()
{ {
@ -2862,7 +2863,17 @@ public sealed class GameWindow : IDisposable
.ReconstructFullCommand(turnForAnim); .ReconstructFullCommand(turnForAnim);
if (turnFullForAnim == 0) turnFullForAnim = 0x65000000u | turnForAnim; if (turnFullForAnim == 0) turnFullForAnim = 0x65000000u | turnForAnim;
animCycle = turnFullForAnim; animCycle = turnFullForAnim;
animSpeed = MathF.Abs(update.MotionState.TurnSpeed ?? 1f); // SIGNED — do NOT MathF.Abs. ACE encodes TurnLeft on the
// wire as (TurnCommand=TurnRight, TurnSpeed=NEGATIVE),
// mirroring retail's adjust_motion convention. The
// sequencer's negative-speed path (EnqueueMotionData
// multiplies MotionData.Omega by speedMod, the
// synthesize-omega fallback flips zomega via
// -(pi/2)*adjustedSpeed) only produces the correct
// CCW rotation when the sign is preserved here.
// Confirmed by live wire trace 2026-05-03: TurnLeft
// input arrives as turnCmd16=0x000D, speed=-1.500.
animSpeed = update.MotionState.TurnSpeed ?? 1f;
} }
} }
// K-fix17 (2026-04-26): preserve the Falling cycle while // K-fix17 (2026-04-26): preserve the Falling cycle while
@ -2993,6 +3004,14 @@ public sealed class GameWindow : IDisposable
.ReconstructFullCommand(turnCmd16); .ReconstructFullCommand(turnCmd16);
if (turnFull == 0) turnFull = 0x65000000u | turnCmd16; if (turnFull == 0) turnFull = 0x65000000u | turnCmd16;
float turnSpd = update.MotionState.TurnSpeed ?? 1f; float turnSpd = update.MotionState.TurnSpeed ?? 1f;
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{
System.Console.WriteLine(
$"[TURN_WIRE] guid={update.Guid:X8} turnCmd16=0x{turnCmd16:X4} "
+ $"turnFull=0x{turnFull:X8} low=0x{turnFull & 0xFFu:X2} "
+ $"({(((turnFull & 0xFFu) == 0x0D) ? "TurnRight" : ((turnFull & 0xFFu) == 0x0E) ? "TurnLeft" : "OTHER")}) "
+ $"speed={turnSpd:F3}");
}
remoteMot.Motion.DoInterpretedMotion( remoteMot.Motion.DoInterpretedMotion(
turnFull, turnSpd, modifyInterpretedState: true); turnFull, turnSpd, modifyInterpretedState: true);
// Seed ObservedOmega with formula so rotation starts // Seed ObservedOmega with formula so rotation starts
@ -5874,6 +5893,23 @@ public sealed class GameWindow : IDisposable
var rot = System.Numerics.Quaternion.CreateFromAxisAngle(axis, angleDelta); var rot = System.Numerics.Quaternion.CreateFromAxisAngle(axis, angleDelta);
rm.Body.Orientation = System.Numerics.Quaternion.Normalize( rm.Body.Orientation = System.Numerics.Quaternion.Normalize(
System.Numerics.Quaternion.Concatenate(rm.Body.Orientation, rot)); System.Numerics.Quaternion.Concatenate(rm.Body.Orientation, rot));
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): print seqOmega direction
// once per remote per ~1 second so we can confirm whether the omega
// sign actually being applied matches the retail-observed turn
// direction. Z>0 = CCW (TurnLeft); Z<0 = CW (TurnRight).
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
if (nowSec - rm.LastOmegaDiagLogTime > 0.5)
{
uint seqMotion = ae.Sequencer?.CurrentMotion ?? 0;
System.Console.WriteLine(
$"[OMEGA_DIAG] guid={serverGuid:X8} motion=0x{seqMotion:X8} "
+ $"seqOmega.Z={seqOmega.Z:F3} (Z>0=CCW=TurnLeft, Z<0=CW=TurnRight)");
rm.LastOmegaDiagLogTime = nowSec;
}
}
} }
// Step 3: calc_acceleration sets body.Acceleration from the Gravity flag // Step 3: calc_acceleration sets body.Acceleration from the Gravity flag