acdream/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
Erik 947c61d2d7 refactor(camera): extract the update frame
Move fly/chase publication, combat target tracking, and local player projection behind typed runtime owners. Preserve the inbound-created projection/reconcile barrier while removing GameWindow callbacks and duplicate shadow helpers.
2026-07-22 03:08:58 +02:00

192 lines
7 KiB
C#

using AcDream.App.Update;
namespace AcDream.App.Input;
/// <summary>
/// Owns the local player's once-per-frame retail object phase.
/// </summary>
/// <remarks>
/// Retail advances the player and sends input-originated movement/jump output
/// before <c>SmartBox::UseTime</c> drains inbound events. Its later
/// <c>CommandInterpreter::UseTime</c> 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.
/// </remarks>
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);
/// <summary>
/// Whether the pre-network Hidden player update admitted a complete object
/// quantum whose retained CPartArray pose must be sampled this frame.
/// </summary>
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));
}
/// <summary>
/// Advances the existing local object exactly once on the object side of
/// the inbound-network barrier.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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,
};
}