diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 12828de..4d7e278 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3745,6 +3745,51 @@ public sealed class GameWindow : IDisposable isMovingTo: false, currentBodyPosition: rmState.Body.Position); } + // #39 fix-3 (2026-05-06): velocity-fallback cycle refinement + // for player remotes. Wire-level evidence (`launch-39-diag2.log`): + // when retail's actor toggles Shift while a direction key + // is held, retail's outbound MoveToState logic does NOT + // emit a fresh packet (only Ready ↔ Run UMs visible in + // `[FWD_WIRE]`, despite a clear walk-pace ↔ run-pace + // velocity transition in `[VEL_DIAG]`). ACE has nothing + // to broadcast → no UM arrives at the observer → cycle + // sticks at whatever the last UM set. Compute the + // synth-velocity here in the player-remote path AND + // call into ApplyServerControlledVelocityCycle, which + // routes through the direction-preserving + UM-grace + // ApplyPlayerLocomotionRefinement helper (added in + // commit 8fa04af). + // + // The legacy non-player block below (3759+) covers NPCs + // and is gated `!IsPlayerGuid`; this block fills the + // matching gap for players. + if (rmState.PrevServerPosTime > 0.0) + { + double nowSecVel = rmState.LastServerPosTime; + double dtPos = nowSecVel - rmState.PrevServerPosTime; + if (dtPos > 0.001) + { + var synthVel = (worldPos - rmState.PrevServerPos) / (float)dtPos; + rmState.ServerVelocity = synthVel; + rmState.HasServerVelocity = true; + + if (_animatedEntities.TryGetValue(entity.Id, out var aeForVel) + && aeForVel.Sequencer is not null) + { + if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1") + { + System.Console.WriteLine( + $"[UPCYCLE_SRC] guid={update.Guid:X8} src=synth-player"); + } + ApplyServerControlledVelocityCycle( + update.Guid, + aeForVel, + rmState, + synthVel); + } + } + } + // Sync the visible entity to the body — overrides the unconditional // entity.Position = worldPos snap at the top of this function. // For the far-snap branch this is a no-op (body == worldPos); for