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;
}