using System.Diagnostics; using AcDream.Core.Combat; using AcDream.Core.Physics; namespace AcDream.Runtime.Gameplay; public interface IRuntimeLocalPlayerControllerSource { PlayerMovementController? Controller { get; } } public interface IRuntimeLocalPlayerMotionSource { MotionInterpreter? Motion { get; } } /// /// Canonical local movement lifetime and intent owner. Graphical input, /// presentation, diagnostics, and future no-window hosts borrow this exact /// state; they never mirror the controller or the autorun latch. /// public sealed class RuntimeLocalPlayerMovementState : IRuntimeLocalPlayerControllerSource, IRuntimeLocalPlayerMotionSource, IRuntimeMovementView, IDisposable { private PlayerMovementController? _controller; private PlayerMovementController? _preparingMotionOwner; private bool _autoRunActive; private bool _hasCommandInput; private MovementInput _commandInput; private bool _disposed; private long _revision; public PlayerMovementController? Controller { get => _controller; set { ObjectDisposedException.ThrowIf(_disposed, this); if (ReferenceEquals(_controller, value)) return; _controller = value; Interlocked.Increment(ref _revision); } } public bool AutoRunActive => _autoRunActive; public bool HasCommandInput => _hasCommandInput; public MovementInput CommandInput => _commandInput; public long Revision => Interlocked.Read(ref _revision); public IRuntimeMovementView View => this; MotionInterpreter? IRuntimeLocalPlayerMotionSource.Motion => _preparingMotionOwner?.Motion ?? _controller?.Motion; public RuntimeMovementSnapshot Snapshot { get { PlayerMovementController? controller = _controller; return controller is null ? new RuntimeMovementSnapshot( false, 0u, default, default, false, 0d, Revision, _autoRunActive, _hasCommandInput, _commandInput) : new RuntimeMovementSnapshot( true, controller.LocalEntityId, controller.CellPosition, controller.BodyVelocity, controller.IsAirborne, controller.SimTimeSeconds, Revision, _autoRunActive, _hasCommandInput, _commandInput); } } /// /// Opens the retail construction seam where a PartArray completion can /// reach the candidate MotionInterpreter before the controller is /// published to ordinary borrowers. /// public IDisposable BeginMotionPreparation( PlayerMovementController controller, Action? drainPriorAnimationQueue = null) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(controller); if (_preparingMotionOwner is not null) { throw new InvalidOperationException( "A local player motion owner is already being prepared."); } drainPriorAnimationQueue?.Invoke(); _preparingMotionOwner = controller; return new MotionPreparation(this, controller); } public bool Execute(RuntimeMovementCommand command) { ObjectDisposedException.ThrowIf(_disposed, this); switch (command) { case RuntimeMovementCommand.ToggleRunLock: _autoRunActive = !_autoRunActive; Interlocked.Increment(ref _revision); return true; case RuntimeMovementCommand.Stop: CancelAutoRun(); ClearCommandInput(); return true; case RuntimeMovementCommand.Ready: case RuntimeMovementCommand.Sit: case RuntimeMovementCommand.Crouch: case RuntimeMovementCommand.Sleep: CancelAutoRun(); ClearCommandInput(); uint motion = command switch { RuntimeMovementCommand.Ready => MotionCommand.Ready, RuntimeMovementCommand.Sit => MotionCommand.Sitting, RuntimeMovementCommand.Crouch => MotionCommand.Crouch, RuntimeMovementCommand.Sleep => MotionCommand.Sleeping, _ => throw new UnreachableException(), }; return _controller?.RequestPosture(motion) == true; default: return false; } } public bool CancelAutoRun() { ObjectDisposedException.ThrowIf(_disposed, this); if (!_autoRunActive) return false; _autoRunActive = false; Interlocked.Increment(ref _revision); return true; } /// /// Installs the persistent semantic movement level sampled by either host. /// The graphical input adapter and direct host both consume this exact /// owner; no bot-only movement latch exists. /// public void SetCommandInput(in MovementInput input) { ObjectDisposedException.ThrowIf(_disposed, this); if (_hasCommandInput && _commandInput == input) return; _commandInput = input; _hasCommandInput = true; Interlocked.Increment(ref _revision); } public bool ClearCommandInput() { ObjectDisposedException.ThrowIf(_disposed, this); if (!_hasCommandInput) return false; _commandInput = default; _hasCommandInput = false; Interlocked.Increment(ref _revision); return true; } /// /// Direct-host projection of the same combat readiness query used by the /// graphical attack adapter. A host without a constructed local movement /// owner is not ready; Slice K's content host must hydrate that owner /// before combat packets can leave. /// public bool IsReadyForAttack(CombatMode mode) { ObjectDisposedException.ThrowIf(_disposed, this); if (_controller is not { } controller) return false; var motion = controller.Motion.InterpretedState; return CombatInputPlanner.PlayerInReadyPositionForAttack( mode, motion.CurrentStyle, motion.ForwardCommand); } public bool IsDualWield { get { ObjectDisposedException.ThrowIf(_disposed, this); return _controller?.Motion.InterpretedState.CurrentStyle == CombatInputPlanner.DualWieldCombatStyle; } } public bool PrepareForAttackRequest() { ObjectDisposedException.ThrowIf(_disposed, this); CancelAutoRun(); return _controller?.PrepareForAttackRequest() == true; } public void ResetInputIntent() { ObjectDisposedException.ThrowIf(_disposed, this); if (!_autoRunActive && !_hasCommandInput) return; _autoRunActive = false; _hasCommandInput = false; _commandInput = default; Interlocked.Increment(ref _revision); } /// /// Retires every character-generation movement owner while keeping the /// process-scoped Runtime root reusable for the next session. /// Graphical hosts release their camera/physics projections first; direct /// hosts have no parallel projection to coordinate. /// public void ResetSession() { ObjectDisposedException.ThrowIf(_disposed, this); bool changed = _autoRunActive || _hasCommandInput || _controller is not null || _preparingMotionOwner is not null; _autoRunActive = false; _hasCommandInput = false; _commandInput = default; _controller = null; _preparingMotionOwner = null; if (changed) Interlocked.Increment(ref _revision); } public RuntimeLocalMovementOwnershipSnapshot CaptureOwnership() => new( _disposed, _controller is not null, _preparingMotionOwner is not null, _autoRunActive, _hasCommandInput, Revision); public void Dispose() { if (_disposed) return; _autoRunActive = false; _hasCommandInput = false; _commandInput = default; _controller = null; _preparingMotionOwner = null; Interlocked.Increment(ref _revision); _disposed = true; } private void EndMotionPreparation(PlayerMovementController controller) { // Terminal disposal clears the unpublished construction seam. A // caller may still unwind the already-issued lease afterward; that // unwind is idempotent rather than resurrecting or faulting the // retired runtime owner. if (_disposed && _preparingMotionOwner is null) return; if (!ReferenceEquals(_preparingMotionOwner, controller)) { throw new InvalidOperationException( "The local player motion preparation owner changed unexpectedly."); } _preparingMotionOwner = null; } private sealed class MotionPreparation( RuntimeLocalPlayerMovementState owner, PlayerMovementController controller) : IDisposable { private RuntimeLocalPlayerMovementState? _owner = owner; public void Dispose() { RuntimeLocalPlayerMovementState? current = Interlocked.Exchange(ref _owner, null); current?.EndMotionPreparation(controller); } } } public readonly record struct RuntimeLocalMovementOwnershipSnapshot( bool IsDisposed, bool HasController, bool HasPreparingMotionOwner, bool AutoRunActive, bool HasCommandInput, long Revision) { public bool IsConverged => IsDisposed && !HasController && !HasPreparingMotionOwner && !AutoRunActive && !HasCommandInput; }