feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -15,15 +16,9 @@ namespace AcDream.Core.Physics;
|
|||
// InterpolationManager::NodeCompleted acclient @ 0x005559A0
|
||||
// InterpolationManager::StopInterpolating acclient @ 0x00555950
|
||||
//
|
||||
// FIFO position-waypoint queue (cap 20). Each physics tick the caller passes
|
||||
// current body position + max-speed from the motion table; we return the
|
||||
// world-space delta vector to apply to the body for this frame.
|
||||
//
|
||||
// Public C# API kept Vector3-based for compatibility with PositionManager and
|
||||
// GameWindow callsites; retail-spec method names are documented inline. The
|
||||
// retail Frame mutation pattern collapses to "return a Vector3 delta" because
|
||||
// adjust_offset's offset Frame is rotation-zero (translation-only) for this
|
||||
// queue's purposes — see audit 04-interp-manager.md § 4.
|
||||
// FIFO Position-waypoint queue (cap 20). The compatibility overload returns
|
||||
// only its world-space origin, while the production overload carries retail's
|
||||
// complete relative Frame from Position::subtract2, including orientation.
|
||||
//
|
||||
// Bug fixes applied vs prior port (audit § 7):
|
||||
// #1: progress_quantum accumulates dt (not step magnitude).
|
||||
|
|
@ -38,8 +33,7 @@ namespace AcDream.Core.Physics;
|
|||
internal sealed class InterpolationNode
|
||||
{
|
||||
public Vector3 TargetPosition;
|
||||
public float Heading;
|
||||
public bool IsHeadingValid;
|
||||
public Quaternion TargetOrientation = Quaternion.Identity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,6 +114,7 @@ public sealed class InterpolationManager
|
|||
private float _progressQuantum = 0f; // progress_quantum (sum of dt)
|
||||
private float _originalDistance = OriginalDistanceSentinel; // original_distance
|
||||
private int _failCount = 0; // node_fail_counter
|
||||
private bool _keepHeading; // keep_heading
|
||||
|
||||
// ── public API ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -167,11 +162,31 @@ public sealed class InterpolationManager
|
|||
/// Pass <c>null</c> if not available — far/near classification falls back
|
||||
/// to "near" (no pre-armed blip).
|
||||
/// </param>
|
||||
public void Enqueue(
|
||||
public Quaternion? Enqueue(
|
||||
Vector3 targetPosition,
|
||||
float heading,
|
||||
bool isMovingTo,
|
||||
Vector3? currentBodyPosition = null)
|
||||
=> Enqueue(
|
||||
targetPosition,
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, heading),
|
||||
isMovingTo,
|
||||
currentBodyPosition,
|
||||
currentBodyOrientation: null);
|
||||
|
||||
/// <summary>
|
||||
/// Complete-frame overload of retail <c>InterpolateTo</c>. The node keeps
|
||||
/// the target quaternion; a near enqueue assigns
|
||||
/// <paramref name="isMovingTo"/> to retail's manager-wide
|
||||
/// <c>keep_heading</c> flag. The far branch deliberately retains the
|
||||
/// manager's prior flag, matching the retail early return.
|
||||
/// </summary>
|
||||
public Quaternion? Enqueue(
|
||||
Vector3 targetPosition,
|
||||
Quaternion targetOrientation,
|
||||
bool isMovingTo,
|
||||
Vector3? currentBodyPosition = null,
|
||||
Quaternion? currentBodyOrientation = null)
|
||||
{
|
||||
// Retail compares dist against either the tail's stored position
|
||||
// (if tail exists AND tail->type == 1) or the body's m_position.
|
||||
|
|
@ -195,10 +210,17 @@ public sealed class InterpolationManager
|
|||
// Far branch (retail line 352918, dist > GetAutonomyBlipDistance):
|
||||
if (dist > AutonomyBlipDistance)
|
||||
{
|
||||
EnqueueRaw(targetPosition, heading, isMovingTo);
|
||||
// The far branch does not assign keep_heading from arg3. It uses
|
||||
// the manager's existing flag when storing this Position.
|
||||
EnqueueRaw(
|
||||
targetPosition,
|
||||
StoreTargetOrientation(
|
||||
targetOrientation,
|
||||
currentBodyOrientation,
|
||||
_keepHeading));
|
||||
// Pre-arm immediate blip on next AdjustOffset (audit § 7 #3).
|
||||
_failCount = StallFailCountThreshold + 1;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Near & already-close branch (retail line 352962):
|
||||
|
|
@ -209,7 +231,14 @@ public sealed class InterpolationManager
|
|||
if (bodyDist <= DesiredDistance)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
// InterpolateTo 0x00555C08 calls CPhysicsObj::set_heading
|
||||
// with the target Frame's heading. It does not install the
|
||||
// target's pitch/roll at this already-close seam.
|
||||
return isMovingTo
|
||||
? null
|
||||
: MoveToMath.SetHeading(
|
||||
targetOrientation,
|
||||
MoveToMath.GetHeading(targetOrientation));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,19 +256,43 @@ public sealed class InterpolationManager
|
|||
_queue.RemoveFirst();
|
||||
|
||||
// 3. Append.
|
||||
EnqueueRaw(targetPosition, heading, isMovingTo);
|
||||
_keepHeading = isMovingTo;
|
||||
EnqueueRaw(
|
||||
targetPosition,
|
||||
StoreTargetOrientation(
|
||||
targetOrientation,
|
||||
currentBodyOrientation,
|
||||
_keepHeading));
|
||||
return null;
|
||||
}
|
||||
|
||||
private void EnqueueRaw(Vector3 target, float heading, bool isMovingTo)
|
||||
private void EnqueueRaw(
|
||||
Vector3 target,
|
||||
Quaternion targetOrientation)
|
||||
{
|
||||
_queue.AddLast(new InterpolationNode
|
||||
{
|
||||
TargetPosition = target,
|
||||
Heading = heading,
|
||||
IsHeadingValid = isMovingTo,
|
||||
TargetOrientation = targetOrientation,
|
||||
});
|
||||
}
|
||||
|
||||
private static Quaternion StoreTargetOrientation(
|
||||
Quaternion targetOrientation,
|
||||
Quaternion? currentBodyOrientation,
|
||||
bool keepHeading)
|
||||
{
|
||||
if (!keepHeading || currentBodyOrientation is not { } current)
|
||||
return targetOrientation;
|
||||
|
||||
// InterpolateTo stores the object's current heading into the node
|
||||
// when keep_heading is active. Frame::set_heading intentionally
|
||||
// discards the target Position's pitch/roll at this seam.
|
||||
return MoveToMath.SetHeading(
|
||||
targetOrientation,
|
||||
MoveToMath.GetHeading(current));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the per-frame world-space correction delta. Combines the retail
|
||||
/// <c>UseTime</c> blip-check (fail_count > 3 → snap to tail, clear queue)
|
||||
|
|
@ -259,14 +312,74 @@ public sealed class InterpolationManager
|
|||
/// Max motion-table speed for this entity's current cycle (m/s).
|
||||
/// Pass 0 to use the <see cref="MaxInterpolatedVelocity"/> fallback.
|
||||
/// </param>
|
||||
public Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)
|
||||
public Vector3 AdjustOffset(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
float maxSpeedFromMinterp,
|
||||
bool inContact = true)
|
||||
{
|
||||
if (!inContact)
|
||||
return Vector3.Zero;
|
||||
|
||||
InterpolationStep step = ComputeStep(
|
||||
dt,
|
||||
currentBodyPosition,
|
||||
maxSpeedFromMinterp);
|
||||
return step.Overwrites ? step.WorldOrigin : Vector3.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>InterpolationManager::adjust_offset</c> complete-Frame path
|
||||
/// (0x00555D30). When interpolation is active it replaces both components
|
||||
/// of <paramref name="offset"/> with <c>Position::subtract2</c>'s relative
|
||||
/// target frame, then scales only Origin to the catch-up step. A MoveTo
|
||||
/// node keeps heading by replacing the relative rotation with identity.
|
||||
/// When the queue is empty or a node completes, the incoming PartArray
|
||||
/// frame remains untouched.
|
||||
/// </summary>
|
||||
public bool AdjustOffset(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
Quaternion currentBodyOrientation,
|
||||
float maxSpeedFromMinterp,
|
||||
MotionDeltaFrame offset,
|
||||
bool inContact = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(offset);
|
||||
if (!inContact)
|
||||
return false;
|
||||
|
||||
InterpolationStep step = ComputeStep(
|
||||
dt,
|
||||
currentBodyPosition,
|
||||
maxSpeedFromMinterp);
|
||||
if (!step.Overwrites)
|
||||
return false;
|
||||
|
||||
offset.Origin = MoveToMath.GlobalToLocalVec(
|
||||
currentBodyOrientation,
|
||||
step.WorldOrigin);
|
||||
offset.Orientation = _keepHeading
|
||||
? Quaternion.Identity
|
||||
: FrameOps.SetRotate(
|
||||
offset.Origin,
|
||||
Quaternion.Identity,
|
||||
Quaternion.Inverse(currentBodyOrientation)
|
||||
* step.TargetOrientation);
|
||||
return true;
|
||||
}
|
||||
|
||||
private InterpolationStep ComputeStep(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
float maxSpeedFromMinterp)
|
||||
{
|
||||
// dt sanity guard — protects PhysicsBody.Position from NaN poisoning.
|
||||
if (dt <= 0 || double.IsNaN(dt))
|
||||
return Vector3.Zero;
|
||||
return default;
|
||||
|
||||
if (_queue.First is null)
|
||||
return Vector3.Zero;
|
||||
return default;
|
||||
|
||||
// Distance to head node (retail line 353083).
|
||||
var head = _queue.First.Value;
|
||||
|
|
@ -278,7 +391,7 @@ public sealed class InterpolationManager
|
|||
if (dist <= DesiredDistance)
|
||||
{
|
||||
NodeCompleted(popHead: true, currentBodyPosition);
|
||||
return Vector3.Zero;
|
||||
return default;
|
||||
}
|
||||
|
||||
// Catch-up speed (retail line 353122 + 353128 fallback).
|
||||
|
|
@ -344,9 +457,13 @@ public sealed class InterpolationManager
|
|||
// Retail splits this into a separate UseTime call; we collapse it.
|
||||
if (_failCount > StallFailCountThreshold)
|
||||
{
|
||||
Vector3 tailPos = _queue.Last!.Value.TargetPosition;
|
||||
InterpolationNode tail = _queue.Last!.Value;
|
||||
Vector3 tailDelta = tail.TargetPosition - currentBodyPosition;
|
||||
Clear();
|
||||
return tailPos - currentBodyPosition;
|
||||
return new InterpolationStep(
|
||||
true,
|
||||
tailDelta,
|
||||
tail.TargetOrientation);
|
||||
}
|
||||
|
||||
// Per-frame step magnitude (retail line 353218).
|
||||
|
|
@ -358,9 +475,17 @@ public sealed class InterpolationManager
|
|||
|
||||
// Direction × step.
|
||||
Vector3 delta = ((head.TargetPosition - currentBodyPosition) / dist) * step;
|
||||
return delta;
|
||||
return new InterpolationStep(
|
||||
true,
|
||||
delta,
|
||||
head.TargetOrientation);
|
||||
}
|
||||
|
||||
private readonly record struct InterpolationStep(
|
||||
bool Overwrites,
|
||||
Vector3 WorldOrigin,
|
||||
Quaternion TargetOrientation);
|
||||
|
||||
/// <summary>
|
||||
/// Retail NodeCompleted (@ 0x005559A0). popHead=true after head reached;
|
||||
/// popHead=false during stall fail (re-baseline only). For our collapsed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue