acdream/src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs
Erik eeb0f6b45c refactor(runtime): extract local teleport and player mode
Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements.

Co-authored-by: Codex <noreply@openai.com>
2026-07-22 02:50:15 +02:00

77 lines
3.1 KiB
C#

using AcDream.App.Input;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
namespace AcDream.App.Interaction;
internal interface IPlayerInteractionMovementSink
{
/// <summary>
/// Cancels any preceding MoveTo, invokes <paramref name="armAfterCancel"/>,
/// 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.
/// </summary>
bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? armAfterCancel = null);
}
/// <summary>
/// Installs retail's client-side TurnToObject/MoveToObject prediction through
/// the same MovementManager used by authoritative movement packets.
/// </summary>
internal sealed class PlayerInteractionMovementSink(
Func<PlayerMovementController?> player,
IPlayerApproachTokenSource approachTokens)
: IPlayerInteractionMovementSink
{
private readonly Func<PlayerMovementController?> _player = player
?? throw new ArgumentNullException(nameof(player));
private readonly IPlayerApproachTokenSource _approachTokens = approachTokens
?? throw new ArgumentNullException(nameof(approachTokens));
public bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? 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);
if (!_approachTokens.TryBeginApproach(out PlayerApproachToken token))
return false;
armAfterCancel?.Invoke(token);
// 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;
}
}