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.
192 lines
7 KiB
C#
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,
|
|
};
|
|
}
|