using System.Diagnostics; using AcDream.Core.Chat; using AcDream.Core.Combat; using AcDream.Core.Physics; namespace AcDream.Runtime.Gameplay; public interface IRuntimeLocalPlayerControllerSource { PlayerMovementController? Controller { get; } } public interface IRuntimeLocalPlayerMotionSource { MotionInterpreter? Motion { get; } } /// /// C3c-F1 (2026-08-02): typed outcome of routing a server movement-stat /// recompute through the Runtime movement owner. The dropped outcomes are /// the J3.6 displaced-callback-rejection pattern — never an exception and /// never a silent void: the caller logs them under its existing /// diagnostics. A skill write against a dead session is meaningless by /// design; the next login re-derives everything from PlayerDescription. /// public enum RuntimeMovementStatsApplication { /// Applied to the live (published/standalone) controller — /// byte-identical to the pre-F1 direct application path. AppliedLive, /// Applied to the Runtime-owned dormant controller during the /// committed-but-not-yet-activated first-entry window. The same /// instance goes live at activation, so the values are already current /// when movement starts. AppliedDormant, /// No controller is installed (pre-first-entry, mid-candidate /// construction, or after session teardown cleared the owner). DroppedNoController, /// The skill snapshot has no authoritative run/jump values yet /// (PlayerDescription not processed) — same silent skip as the pre-F1 /// path. DroppedIncompleteSnapshot, /// The installed controller is terminal (sealed, retired, or /// discarded): a displaced post-teardown write, reported instead of /// faulting the session. DroppedDisplacedController, } /// /// C3c-F1 (2026-08-02): typed outcome of routing an inbound server /// PhysicsState push through the local movement controller's publication /// lifecycle. Same displaced-callback-rejection family as /// , with one deliberate /// difference: the dormant window DROPS the push rather than applying it, /// because the activation transaction owns the dormant body's physics /// state exclusively (it re-reads the canonical record's FinalPhysicsState /// through RefreshDormantRuntimePhysicsState at both activation /// phases), and the App-side push carries that exact same unchanged record /// value while the accepted SetState itself is queued behind the initial /// residence — dropping it is value-preserving by construction. /// public enum RuntimeServerPhysicsStateApplication { /// Applied to the live (published/standalone) controller — /// byte-identical to the direct ApplyPhysicsState path. AppliedLive, /// The controller is Runtime-owned dormant: the activation /// pipeline is the sole authority for the dormant body's physics state /// and re-reads the canonical value itself. DroppedDormantActivationOwned, /// The installed controller is terminal — a displaced /// post-teardown push. DroppedDisplacedController, } /// /// 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 RuntimeLocalPlayerPhysicsPublicationState? _physicsPublication; private bool _autoRunActive; private bool _hasCommandInput; private MovementInput _commandInput; private bool _disposed; private long _revision; private Action? _onInterfaceText; /// /// Campaign CH slice CH2: the retail-faithful text/type router /// (RuntimeCommunicationState.AddText) — applied to every /// controller this owner installs, past and future, so a client-local /// jump refusal (PlayerMovementController's local /// ChargeJump/jump calls, research doc §4.2/§6.4) reaches /// the SpewBox through the same chokepoint server-sent WeenieErrors use. /// public Action? OnInterfaceText { get => _onInterfaceText; set { _onInterfaceText = value; if (_controller is not null) _controller.OnInterfaceText = value; } } public PlayerMovementController? Controller { get => _controller; // C3c seal: the public write escape hatch is closed. Production // controller installation flows only through the publication // lifecycle (CommitRuntimeOwnedController via // RuntimeLocalPlayerPhysicsPublicationState.Commit) and teardown // through ResetSession/Dispose/DiscardActivation. The setter stays // reachable for tests via InternalsVisibleTo only. internal set { ObjectDisposedException.ThrowIf(_disposed, this); if (ReferenceEquals(_controller, value)) return; _controller?.RetireRuntimePublication(); _controller = value; if (_controller is not null) _controller.OnInterfaceText = _onInterfaceText; ControllerOwnershipEpoch++; Interlocked.Increment(ref _revision); } } public bool AutoRunActive => _autoRunActive; public bool HasCommandInput => _hasCommandInput; public MovementInput CommandInput => _commandInput; public long Revision => Interlocked.Read(ref _revision); public ulong ControllerOwnershipEpoch { get; private set; } public IRuntimeMovementView View => this; internal RuntimeLocalPlayerPhysicsPublicationState PhysicsPublication => _physicsPublication ?? throw new InvalidOperationException( "The Runtime local-player physics publication owner is not bound."); 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; } /// /// C3c-F1 (2026-08-02): the ONLY route by which server-authoritative /// movement stats (run/jump skill, burden, stamina, PK status — the /// exact field set of the deleted /// RuntimeMovementSkillProjection.ApplyTo) reach the local /// movement controller. App holds no controller reference for stat /// application and performs no direct configuration mutation; the /// owner's publication lifecycle decides whether the write lands /// (live/dormant) or is reported as a typed displaced drop (terminal) — /// the fix for the connected-gate post-logout ingest crash at /// PlayerMovementController.EnsureConfigurationMutable. /// Deliberately tolerant of a disposed owner: a recompute displaced /// past teardown observes /// instead of faulting the session. /// public RuntimeMovementStatsApplication ApplyCharacterMovementStats( RuntimeMovementSkillState skills) { ArgumentNullException.ThrowIfNull(skills); if (_controller is not { } controller) return RuntimeMovementStatsApplication.DroppedNoController; RuntimeMovementSkillSnapshot snapshot = skills.Snapshot; if (!snapshot.IsComplete) return RuntimeMovementStatsApplication.DroppedIncompleteSnapshot; return controller.ApplyCharacterMovementStats(snapshot); } /// /// C3c-F1: routes the stamina-exhaustion EVENT (retail /// CommandInterpreter::HandleExhaustion) through the owner so the /// App edge-tracker never touches the gated controller motion surface. /// Returns false when no live controller can dispatch it (absent, /// dormant, terminal, or disposed owner) — displaced-callback-tolerant /// for the same reason as . /// public bool ReportExhaustion() => _controller?.ReportExhaustionAtMovementBoundary() == 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); _physicsPublication?.ResetSession(); bool changed = _autoRunActive || _hasCommandInput || _controller is not null || _preparingMotionOwner is not null; _autoRunActive = false; _hasCommandInput = false; _commandInput = default; if (_controller is not null) { _controller.RetireRuntimePublication(); _controller = null; ControllerOwnershipEpoch++; } _preparingMotionOwner = null; if (changed) Interlocked.Increment(ref _revision); } public RuntimeLocalMovementOwnershipSnapshot CaptureOwnership() => new( _disposed, _controller is not null, _preparingMotionOwner is not null, _autoRunActive, _hasCommandInput, Revision, ControllerOwnershipEpoch, _physicsPublication?.CaptureOwnership() ?? default); public void Dispose() { if (_disposed) return; _autoRunActive = false; _hasCommandInput = false; _commandInput = default; _physicsPublication?.Dispose(); if (_controller is not null) { _controller.RetireRuntimePublication(); _controller = null; ControllerOwnershipEpoch++; } _preparingMotionOwner = null; Interlocked.Increment(ref _revision); _disposed = true; } internal void AttachPhysicsPublication( RuntimeLocalPlayerPhysicsPublicationState publication) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(publication); if (_physicsPublication is not null) { throw new InvalidOperationException( "The Runtime local-player physics publication owner is already bound."); } _physicsPublication = publication; } internal bool CanCommitRuntimeOwnedController( ulong expectedEpoch, PlayerMovementController? expectedController) => !_disposed && ControllerOwnershipEpoch == expectedEpoch && ReferenceEquals(_controller, expectedController); internal void CommitRuntimeOwnedController(PlayerMovementController controller) { _controller?.RetireRuntimePublication(); _controller = controller; // The PRODUCTION install path must carry the same wiring the // internal Controller setter applies: this line was missing, so // every live graphical controller ran with a null OnInterfaceText // and client-local refusals (jump-in-air) silently vanished — // proven by the round-3 probe line // "[jump] ReportJumpRefusal result=NotGrounded hasCallback=False". controller.OnInterfaceText = _onInterfaceText; ControllerOwnershipEpoch++; Interlocked.Increment(ref _revision); } 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, ulong ControllerOwnershipEpoch, RuntimeLocalPlayerPhysicsPublicationOwnershipSnapshot PhysicsPublication) { public bool IsConverged => IsDisposed && !HasController && !HasPreparingMotionOwner && !AutoRunActive && !HasCommandInput && PhysicsPublication.IsConverged; }