using AcDream.App.Input; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; namespace AcDream.App.Interaction; internal interface IPlayerInteractionMovementSink { /// /// Cancels any preceding MoveTo, invokes , /// then installs the new movement. The callback boundary lets the intent /// owner arm completion state before an already-facing TurnTo can finish /// synchronously, without letting the preceding cancellation clear it. /// bool BeginApproach(InteractionApproach approach, Action? armAfterCancel = null); } /// /// Installs retail's client-side TurnToObject/MoveToObject prediction through /// the same MovementManager used by authoritative movement packets. /// internal sealed class PlayerInteractionMovementSink( Func player) : IPlayerInteractionMovementSink { private readonly Func _player = player ?? throw new ArgumentNullException(nameof(player)); public bool BeginApproach(InteractionApproach approach, Action? armAfterCancel = null) { PlayerMovementController? controller = _player(); if (controller?.MoveTo is null) return false; var parameters = new MovementParameters { DistanceToObject = approach.UseRadius, CanCharge = approach.CanCharge, }; var movement = new MovementStruct { ObjectId = approach.Target.ServerGuid, TopLevelId = approach.Target.ServerGuid, Pos = new Position( approach.Player.CellId, approach.Target.Entity.Position, System.Numerics.Quaternion.Identity), Params = parameters, Type = approach.IsCloseRange ? MovementType.TurnToObject : MovementType.MoveToObject, Radius = approach.TargetRadius, Height = approach.TargetHeight, }; // PerformMovement cancels at its head. Do it explicitly before the // intent is armed so cancellation of the preceding move cannot clear // the new request; the internal second call is then a retail no-op. controller.Movement.CancelMoveTo(WeenieError.ActionCancelled); armAfterCancel?.Invoke(); // P1's wire MoveToObject store marks the command non-autonomous. // The speculative local install must do the same or the next raw-input // pump overwrites it before ACE's authoritative movement arrives. controller.SetLastMoveWasAutonomous(false); return controller.Movement.PerformMovement(movement) == WeenieError.None; } }