Two halves of the felt run-hitch (the visible one-frame player lurch): - The presentation lerp normalized the pending object-clock time by the fixed 30 Hz MinQuantum, but retail's object clock simulates VARIABLE-length quanta (CPhysicsObj::update_object 0x00515D10: capped at MaxQuantum, everything above MinQuantum runs as ONE step). After a long frame the view froze for the quantum and then fast-replayed it. ComputeRenderPosition now spans the ACTUAL last quantum (_lastQuantumSeconds), and PresentedDeltaSeconds accounts continuous presented time across quantum boundaries. - The chase camera damped toward the presented player using wall dt while the player presents on the object clock, so a long frame stepped the camera far past the under-advanced player — measured up to ~1 m of camera/player decoherence in a single frame. Retail ties camera update to the physics-update callback (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60), i.e. the same clock as the body; both chase cameras now integrate PresentedDeltaSeconds. Manual zoom/pitch adjustment stays on wall dt (a user-input rate, not target chasing). Owner gate: camera-vs-player boom-length change fell from ~1 m spikes to 0.2-1.2 cm median on long frames; teleports settle clean. Two Runtime tests updated to pin the continuous-rate contract. The temporary PlayerPresentationProbe apparatus that measured this is retired with the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
5.2 KiB
C#
126 lines
5.2 KiB
C#
using AcDream.App.Combat;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Update;
|
|
using AcDream.Core.Rendering;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Owns the per-update fly/chase camera publication tail.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The local object phase normally publishes the player before inbound
|
|
/// traffic. When the player is first created by that inbound pass, the local
|
|
/// frame publishes its initial root here and the spatial reconciler refreshes
|
|
/// child/effect anchors before either chase camera samples it.
|
|
/// </remarks>
|
|
internal sealed class CameraFrameController : ICameraFramePhase
|
|
{
|
|
private readonly CameraController _camera;
|
|
private readonly IInputCaptureSource _capture;
|
|
private readonly ICameraFrameInputSource _input;
|
|
private readonly ILocalPlayerPresentationRuntime _player;
|
|
private readonly IChaseCameraSource _chase;
|
|
private readonly RetailLocalPlayerFrameController _localFrame;
|
|
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
|
private readonly ICombatCameraTargetSource _combatTarget;
|
|
|
|
public CameraFrameController(
|
|
CameraController camera,
|
|
IInputCaptureSource capture,
|
|
ICameraFrameInputSource input,
|
|
ILocalPlayerPresentationRuntime player,
|
|
IChaseCameraSource chase,
|
|
RetailLocalPlayerFrameController localFrame,
|
|
ILiveSpatialReconcilePhase spatialReconciler,
|
|
ICombatCameraTargetSource combatTarget)
|
|
{
|
|
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
|
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
|
|
_input = input ?? throw new ArgumentNullException(nameof(input));
|
|
_player = player ?? throw new ArgumentNullException(nameof(player));
|
|
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
|
|
_localFrame = localFrame ?? throw new ArgumentNullException(nameof(localFrame));
|
|
_spatialReconciler = spatialReconciler
|
|
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
|
_combatTarget = combatTarget ?? throw new ArgumentNullException(nameof(combatTarget));
|
|
}
|
|
|
|
public void Tick(UpdateFrameTiming timing)
|
|
{
|
|
if (_capture.DevToolsWantCaptureKeyboard || !_input.IsAvailable)
|
|
return;
|
|
|
|
if (_camera.IsFlyMode)
|
|
{
|
|
FlyCameraInput input = _input.CaptureFly();
|
|
_camera.Fly.Update(
|
|
timing.SimulationDeltaSeconds,
|
|
input.Forward,
|
|
input.Left,
|
|
input.Backward,
|
|
input.Right,
|
|
input.Up,
|
|
input.Down,
|
|
input.Boost);
|
|
return;
|
|
}
|
|
|
|
PlayerMovementController? controller = _player.Controller;
|
|
ChaseCamera? legacy = _chase.Legacy;
|
|
RetailChaseCamera? retail = _chase.Retail;
|
|
if (!_player.CanPresentPlayer || controller is null || legacy is null)
|
|
return;
|
|
|
|
if (CameraDiagnostics.UseRetailChaseCamera && retail is not null)
|
|
{
|
|
ChaseCameraAdjustmentInput input = _input.CaptureChaseAdjustment();
|
|
float adjustment = CameraDiagnostics.CameraAdjustmentSpeed
|
|
* timing.SimulationDeltaSecondsSingle;
|
|
if (input.ZoomIn)
|
|
retail.AdjustDistance(-adjustment);
|
|
if (input.ZoomOut)
|
|
retail.AdjustDistance(+adjustment);
|
|
if (input.Raise)
|
|
retail.AdjustPitch(+adjustment * 0.02f);
|
|
if (input.Lower)
|
|
retail.AdjustPitch(-adjustment * 0.02f);
|
|
}
|
|
|
|
if (!_localFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
|
return;
|
|
|
|
if (!playerFrame.AdvancedBeforeNetwork)
|
|
_spatialReconciler.Reconcile();
|
|
|
|
MovementResult result = playerFrame.Movement;
|
|
// #429 defect 2: the chase camera smooths toward the PRESENTED player
|
|
// position, which lives on the retail 30 Hz object clock (see
|
|
// PlayerMovementController.PresentedDeltaSeconds). Integrating the
|
|
// damping with wall dt made the camera step full wall time on long
|
|
// frames while the presented player under-advanced against the
|
|
// quantum — measured ~1 m of camera/player decoherence in one frame,
|
|
// the felt run-hitch. Retail ties the camera to the physics-update
|
|
// callback (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60), i.e.
|
|
// the same clock as the body; the presented delta restores that.
|
|
// Manual zoom/pitch adjustment above stays on wall dt — it is a
|
|
// user-input rate, not target chasing.
|
|
float cameraDt = controller.PresentedDeltaSeconds;
|
|
legacy.Update(
|
|
result.RenderPosition,
|
|
controller.Yaw,
|
|
isOnGround: result.IsOnGround,
|
|
dt: cameraDt);
|
|
|
|
retail?.Update(
|
|
result.RenderPosition,
|
|
controller.Yaw,
|
|
playerVelocity: controller.BodyVelocity,
|
|
isOnGround: result.IsOnGround,
|
|
contactPlaneNormal: controller.ContactPlane.Normal,
|
|
dt: cameraDt,
|
|
cellId: controller.CellId,
|
|
selfEntityId: controller.LocalEntityId,
|
|
trackedTargetPoint: _combatTarget.GetTrackedTargetPoint());
|
|
}
|
|
}
|