feat(headless): complete deterministic bot command parity

This commit is contained in:
Erik 2026-07-27 08:23:36 +02:00
parent 7e8acb74dd
commit 38e83640d9
37 changed files with 2805 additions and 295 deletions

View file

@ -1,4 +1,5 @@
using AcDream.App.Update;
using AcDream.Runtime;
namespace AcDream.App.Input;
@ -14,17 +15,7 @@ namespace AcDream.App.Input;
/// </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);
private readonly RuntimeLocalPlayerFrameController _runtime;
public readonly record struct PresentationFrame(
MovementResult Movement,
@ -35,19 +26,29 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
/// 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,
};
internal bool HiddenPartPoseDirty =>
_runtime.HiddenPartPoseDirty;
public RetailLocalPlayerFrameController(
ILocalPlayerFrameRuntime runtime,
IMovementInputSource movementInput)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_movementInput = movementInput
?? throw new ArgumentNullException(nameof(movementInput));
_runtime = new RuntimeLocalPlayerFrameController(
runtime ?? throw new ArgumentNullException(nameof(runtime)),
movementInput
?? throw new ArgumentNullException(nameof(movementInput)));
}
public RetailLocalPlayerFrameController(
GameRuntime gameRuntime,
ILocalPlayerFrameRuntime runtime,
IMovementInputSource movementInput)
{
ArgumentNullException.ThrowIfNull(gameRuntime);
_runtime = gameRuntime.CreateLocalPlayerFrameController(
runtime ?? throw new ArgumentNullException(nameof(runtime)),
movementInput
?? throw new ArgumentNullException(nameof(movementInput)));
}
/// <summary>
@ -55,64 +56,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
/// 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);
}
=> _runtime.AdvanceBeforeNetwork(deltaSeconds);
/// <summary>
/// Runs retail's post-inbound command-interpreter phase. The split
@ -120,32 +64,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
/// 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);
}
=> _runtime.RunPostNetworkCommandPhase();
/// <summary>
/// Produces the post-network presentation sample without advancing
@ -154,39 +73,15 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
/// </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;
bool available = _runtime.TryGetPresentationAfterNetwork(
out AcDream.Runtime.Gameplay.RuntimeLocalPlayerPresentationFrame
shared);
frame = available
? new PresentationFrame(
shared.Movement,
shared.Hidden,
shared.AdvancedBeforeNetwork)
: default;
return available;
}
private static MovementResult RefreshSpatialFields(
MovementResult movement,
PlayerMovementController controller) =>
movement with
{
Position = controller.Position,
RenderPosition = controller.RenderPosition,
CellId = controller.CellId,
IsOnGround = controller.CanSendPositionEvent,
};
}