using AcDream.App.Update; namespace AcDream.App.Input; /// /// Owns the local player's once-per-frame retail object phase. /// /// /// Retail advances the player and sends input-originated movement/jump output /// before SmartBox::UseTime drains inbound events. Its later /// CommandInterpreter::UseTime position check uses current post-inbound /// state. The cached input result is presentation-only after the barrier: its /// one-shot commands are never replayed against a later authoritative cell. /// internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase { private readonly ILocalPlayerFrameRuntime _runtime; private readonly IMovementInputSource _movementInput; private AdvancedFrame? _advancedFrame; private readonly record struct AdvancedFrame( PlayerMovementController Controller, MovementResult Movement, bool ObjectAdvanced, bool Hidden, bool ObjectQuantumAdvanced); public readonly record struct PresentationFrame( MovementResult Movement, bool Hidden, bool AdvancedBeforeNetwork); /// /// Whether the pre-network Hidden player update admitted a complete object /// quantum whose retained CPartArray pose must be sampled this frame. /// internal bool HiddenPartPoseDirty => _advancedFrame is { Hidden: true, ObjectQuantumAdvanced: true, }; public RetailLocalPlayerFrameController( ILocalPlayerFrameRuntime runtime, IMovementInputSource movementInput) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _movementInput = movementInput ?? throw new ArgumentNullException(nameof(movementInput)); } /// /// Advances the existing local object exactly once on the object side of /// the inbound-network barrier. /// public void AdvanceBeforeNetwork(float deltaSeconds) { _advancedFrame = null; PlayerMovementController? controller = _runtime.Controller; if (!_runtime.CanPresentPlayer || controller is null) return; if (!float.IsFinite(deltaSeconds) || deltaSeconds <= 0f) { bool rejectedHidden = _runtime.IsHidden; MovementResult rejected = controller.CapturePresentationResult(); _runtime.Project(controller, rejected, rejectedHidden); _advancedFrame = new AdvancedFrame( controller, rejected, ObjectAdvanced: false, Hidden: rejectedHidden, ObjectQuantumAdvanced: false); return; } controller.LocalEntityId = _runtime.ResolveLocalEntityId(); bool hidden = _runtime.IsHidden; if (_runtime.ObjectClockDisposition is AcDream.Core.Physics.RetailObjectClockDisposition.Suspend) { // CPhysicsObj::update_object returns before advancing time for a // Frozen or cell-less object. Keep the retained projection live, // but emit neither input actions nor AutonomousPosition from a // physics tick that retail did not run. controller.SuspendObjectUpdate(deltaSeconds); MovementResult paused = controller.CapturePresentationResult(); _runtime.Project(controller, paused, hidden); _advancedFrame = new AdvancedFrame( controller, paused, ObjectAdvanced: false, Hidden: hidden, ObjectQuantumAdvanced: false); return; } MovementResult movement = hidden ? controller.TickHidden(deltaSeconds, _runtime.HandleTargeting) : controller.Update( deltaSeconds, _movementInput.Capture(), _runtime.HandleTargeting); _runtime.Project(controller, movement, hidden); _runtime.SendPreNetwork(controller, movement, hidden); _advancedFrame = new AdvancedFrame( controller, movement, ObjectAdvanced: true, Hidden: hidden, ObjectQuantumAdvanced: controller.AdvancedObjectQuantumLastTick); } /// /// Runs retail's post-inbound command-interpreter phase. The split /// controller/render projection is reconciled without advancing physics, /// then AutonomousPosition is evaluated from that current state. /// public void RunPostNetworkCommandPhase() { PlayerMovementController? controller = _runtime.Controller; if (!_runtime.CanPresentPlayer || controller is null) return; bool hidden = _runtime.IsHidden; if (_advancedFrame is { } advanced && ReferenceEquals(advanced.Controller, controller)) { MovementResult movement = RefreshSpatialFields( advanced.Movement, controller); _runtime.Project(controller, movement, hidden); _advancedFrame = new AdvancedFrame( controller, movement, advanced.ObjectAdvanced, advanced.Hidden, advanced.ObjectQuantumAdvanced); if (!advanced.ObjectAdvanced) return; } _runtime.SendPostNetwork(controller, hidden); } /// /// Produces the post-network presentation sample without advancing /// physics. An owner first created by the inbound pass is seeded from its /// current state and receives its first real object tick next frame. /// public bool TryGetPresentationAfterNetwork(out PresentationFrame frame) { frame = default; PlayerMovementController? controller = _runtime.Controller; if (!_runtime.CanPresentPlayer || controller is null) return false; bool hidden = _runtime.IsHidden; if (_advancedFrame is { } advanced && ReferenceEquals(advanced.Controller, controller)) { MovementResult movement = RefreshSpatialFields( advanced.Movement, controller); frame = new PresentationFrame( movement, hidden, AdvancedBeforeNetwork: advanced.ObjectAdvanced); return true; } MovementResult initial = controller.CapturePresentationResult(); _runtime.Project(controller, initial, hidden); frame = new PresentationFrame(initial, hidden, AdvancedBeforeNetwork: false); return true; } private static MovementResult RefreshSpatialFields( MovementResult movement, PlayerMovementController controller) => movement with { Position = controller.Position, RenderPosition = controller.RenderPosition, CellId = controller.CellId, IsOnGround = controller.CanSendPositionEvent, }; }