fix(world): complete recall before teleport hide
Match retail's update ordering so object animation, particles, and scripts advance before inbound teleport state is applied. Separate input-originated movement from post-network autonomous position output, and reconcile presentation without a second physics tick so recall cannot resume after arrival. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
dded9e6b17
commit
75acae02d6
13 changed files with 1068 additions and 332 deletions
155
src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
Normal file
155
src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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>
|
||||
public sealed class RetailLocalPlayerFrameController
|
||||
{
|
||||
private readonly Func<bool> _canPresentPlayer;
|
||||
private readonly Func<PlayerMovementController?> _getController;
|
||||
private readonly Func<MovementInput> _captureInput;
|
||||
private readonly Func<uint> _resolveLocalEntityId;
|
||||
private readonly Action _handleTargeting;
|
||||
private readonly Func<bool> _isHidden;
|
||||
private readonly Action<PlayerMovementController, MovementResult, bool> _project;
|
||||
private readonly Action<PlayerMovementController, MovementResult, bool> _sendPreNetwork;
|
||||
private readonly Action<PlayerMovementController, bool> _sendPostNetwork;
|
||||
|
||||
private AdvancedFrame? _advancedFrame;
|
||||
|
||||
private readonly record struct AdvancedFrame(
|
||||
PlayerMovementController Controller,
|
||||
MovementResult Movement);
|
||||
|
||||
public readonly record struct PresentationFrame(
|
||||
MovementResult Movement,
|
||||
bool Hidden,
|
||||
bool AdvancedBeforeNetwork);
|
||||
|
||||
public RetailLocalPlayerFrameController(
|
||||
Func<bool> canPresentPlayer,
|
||||
Func<PlayerMovementController?> getController,
|
||||
Func<MovementInput> captureInput,
|
||||
Func<uint> resolveLocalEntityId,
|
||||
Action handleTargeting,
|
||||
Func<bool> isHidden,
|
||||
Action<PlayerMovementController, MovementResult, bool> project,
|
||||
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
|
||||
Action<PlayerMovementController, bool> sendPostNetwork)
|
||||
{
|
||||
_canPresentPlayer = canPresentPlayer
|
||||
?? throw new ArgumentNullException(nameof(canPresentPlayer));
|
||||
_getController = getController
|
||||
?? throw new ArgumentNullException(nameof(getController));
|
||||
_captureInput = captureInput
|
||||
?? throw new ArgumentNullException(nameof(captureInput));
|
||||
_resolveLocalEntityId = resolveLocalEntityId
|
||||
?? throw new ArgumentNullException(nameof(resolveLocalEntityId));
|
||||
_handleTargeting = handleTargeting
|
||||
?? throw new ArgumentNullException(nameof(handleTargeting));
|
||||
_isHidden = isHidden
|
||||
?? throw new ArgumentNullException(nameof(isHidden));
|
||||
_project = project
|
||||
?? throw new ArgumentNullException(nameof(project));
|
||||
_sendPreNetwork = sendPreNetwork
|
||||
?? throw new ArgumentNullException(nameof(sendPreNetwork));
|
||||
_sendPostNetwork = sendPostNetwork
|
||||
?? throw new ArgumentNullException(nameof(sendPostNetwork));
|
||||
}
|
||||
|
||||
/// <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 = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
return;
|
||||
|
||||
controller.LocalEntityId = _resolveLocalEntityId();
|
||||
_handleTargeting();
|
||||
|
||||
bool hidden = _isHidden();
|
||||
MovementResult movement = hidden
|
||||
? controller.TickHidden(deltaSeconds)
|
||||
: controller.Update(deltaSeconds, _captureInput());
|
||||
|
||||
_project(controller, movement, hidden);
|
||||
_sendPreNetwork(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(controller, movement);
|
||||
}
|
||||
|
||||
/// <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 = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
return;
|
||||
|
||||
bool hidden = _isHidden();
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
advanced.Movement,
|
||||
controller);
|
||||
_project(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(controller, movement);
|
||||
}
|
||||
|
||||
_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 = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
return false;
|
||||
|
||||
bool hidden = _isHidden();
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
advanced.Movement,
|
||||
controller);
|
||||
frame = new PresentationFrame(movement, hidden, AdvancedBeforeNetwork: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
MovementResult initial = controller.CapturePresentationResult();
|
||||
_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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue