feat(D6.2b): send retail-faithful raw forward_speed=1.0 on the wire

The outbound MoveToState now sends the RAW forward_speed 1.0 (omitted by
default-difference packing) instead of the pre-computed runRate, matching what
retail's client sends.

Settled the open question with a live echo-test: acdream sends forward_speed=1.0,
ACE broadcasts back RunForward @ runRate (not 1.0), and a retail observer saw
+Acdream run at full pace. So ACE RECOMPUTES the broadcast run speed from the
character's run skill and auto-upgrades WalkForward+HoldKey.Run -> RunForward for
observers. The earlier PlayerMovementController comment citing ACE
MovementData.cs ("ACE relays the speed, so the wire must send run_rate") was
wrong; corrected.

Changes:
- PlayerMovementController section 6: forward wire command is WalkForward @ 1.0
  (+ HoldKey.Run when running); LocalAnimationCommand still carries RunForward for
  the local cycle. Backward already 1.0. The MotionStateChanged detection now
  keys the walk<->run toggle off HoldKey + LocalAnimationCommand (forward_speed is
  constant 1.0); the forward_speed comparison is retained but never fires.
- GameWindow: reverted the D6.2b echo-test one-liner; the wire RawMotionState
  takes forward_speed from result (now 1.0).

The wire and the D6.2a local velocity are now both raw-1.0. Threading them onto
one shared RawMotionState (removing section-6's duplicate build) is a
behavior-neutral cleanup follow-up.

Full suite green (3255). Docs: roadmap + pseudocode doc updated with the
echo-test finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 10:36:46 +02:00
parent 4740750649
commit d34721fa94
4 changed files with 45 additions and 37 deletions

View file

@ -1268,31 +1268,26 @@ public sealed class PlayerMovementController
uint? outTurnCmd = null;
float? outTurnSpeed = null;
// Retail-faithful wire commands. ACE's MovementData constructor only
// computes interpState.ForwardSpeed for WalkForward / WalkBackwards
// (Network/Motion/MovementData.cs:104-119) — for any other command
// the else-branch passes through without setting speed, so observers
// dead-reckon at speed=0. The wire therefore must be:
// Retail-faithful wire commands — the wire carries the RAW motion state:
// - Forward (walk): WalkForward @ 1.0
// - Forward (run): WalkForward @ run_rate + HoldKey.Run
// (ACE auto-upgrades to RunForward for observers)
// - Forward (run): WalkForward @ 1.0 + HoldKey.Run
// - Backward: WalkBackward @ 1.0
// Our own local animation still wants the actual RunForward cycle
// though — that's carried separately in LocalAnimationCommand below.
// D6.2b (echo-test 2026-07-01): ACE RECOMPUTES the broadcast run speed
// from the character's run skill and auto-upgrades WalkForward+HoldKey.Run
// → RunForward for observers. Sending the raw forward_speed=1.0 (omitted by
// default-difference packing) still broadcasts RunForward @ runRate — a
// retail observer saw +Acdream run at full pace. The earlier "wire must
// send run_rate because ACE relays it" comment (citing MovementData.cs)
// was WRONG. Our own local animation still wants the actual RunForward
// cycle — carried separately in LocalAnimationCommand below.
uint? localAnimCmd = null;
if (input.Forward)
{
outForwardCmd = MotionCommand.WalkForward;
if (input.Run && _weenie.InqRunRate(out float runRate))
{
outForwardSpeed = runRate;
localAnimCmd = MotionCommand.RunForward; // local cycle is RunForward
}
else
{
outForwardSpeed = 1.0f;
localAnimCmd = MotionCommand.WalkForward;
}
outForwardCmd = MotionCommand.WalkForward;
outForwardSpeed = 1.0f; // RAW — ACE recomputes the broadcast speed
localAnimCmd = (input.Run && _weenie.InqRunRate(out _))
? MotionCommand.RunForward // local cycle is RunForward
: MotionCommand.WalkForward;
}
else if (input.Backward)
{
@ -1333,20 +1328,15 @@ public sealed class PlayerMovementController
}
// ── 7. Detect motion state change ─────────────────────────────────────
// Bug fix: ForwardCommand can stay the same (WalkForward) while ONLY
// ForwardSpeed or the run-hold bit changes. If the user is already
// walking (W held), then presses Shift, the outbound wire still has
// ForwardCommand=WalkForward but outForwardSpeed jumps from 1.0 to
// runRate. Without also tracking speed + hold-key here, no new
// MoveToState is sent — the server keeps thinking the player walks,
// and retail observers render walking animation despite the local
// player's RunForward cycle.
//
// Similarly LocalAnimationCommand change (Walk→Run on local cycle)
// must force a fresh outbound so ACE's BroadcastMovement re-runs
// MovementData(this, moveToState) which only reads ForwardCommand +
// ForwardSpeed + HoldKey to pick between WalkForward vs RunForward
// for remote observers.
// ForwardCommand can stay WalkForward while only the run-hold bit changes
// (walk W held, then Shift → run). Since D6.2b the wire forward_speed is
// always the RAW 1.0 (ACE recomputes the broadcast run speed), so the
// walk↔run toggle is detected via the HoldKey (runHold) and the
// LocalAnimationCommand change (Walk↔Run cycle), NOT via forward_speed. A
// fresh MoveToState on that toggle lets ACE's BroadcastMovement re-pick
// WalkForward vs RunForward (via HoldKey) and recompute the run speed for
// observers. The forward_speed comparison below is retained (harmless — it
// never fires now that forward_speed is constant) for defensiveness.
bool runHold = input.Run;
bool changed = outForwardCmd != _prevForwardCmd
|| outSidestepCmd != _prevSidestepCmd