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:
Erik 2026-07-20 09:10:31 +02:00
parent 31a0889f08
commit f961d70023
77 changed files with 12513 additions and 1871 deletions

View file

@ -1,4 +1,5 @@
using System.Numerics;
using AcDream.Core.Physics.Motion;
namespace AcDream.Core.Physics;
@ -7,10 +8,10 @@ namespace AcDream.Core.Physics;
/// by <c>CSequence::update</c> + InterpolationManager catch-up correction.
/// Pure function — no side effects or hidden state.
///
/// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730):
/// rootOffset = CPartArray::Update(dt) // animation
/// PositionManager::adjust_offset(rootOffset) // adds correction
/// frame.origin += rootOffset
/// Mirrors retail <c>CPhysicsObj::UpdatePositionInternal</c> (0x00512C30):
/// CPartArray writes one complete local Frame, then PositionManager mutates
/// that same Frame. Active interpolation replaces it with
/// <c>Position::subtract2</c>; later managers receive the result.
///
/// The animation root motion is the complete body-local <c>Frame.Origin</c>
/// accumulated by <c>CPartArray::Update</c>: authored PosFrames plus the
@ -30,6 +31,50 @@ namespace AcDream.Core.Physics;
/// </summary>
public sealed class RemoteMotionCombiner
{
/// <summary>
/// Compose retail's complete per-object delta frame. Interpolation, when
/// active, replaces the PartArray frame via
/// <c>Position::subtract2</c>; otherwise the authored root frame remains.
/// </summary>
/// <returns><c>true</c> when interpolation replaced the root frame.</returns>
public bool ComposeOffset(
double dt,
Vector3 currentBodyPosition,
Quaternion ori,
MotionDeltaFrame rootMotionLocalFrame,
InterpolationManager interp,
float maxSpeed,
MotionDeltaFrame output,
Vector3? terrainNormal = null,
bool inContact = true)
{
ArgumentNullException.ThrowIfNull(rootMotionLocalFrame);
ArgumentNullException.ThrowIfNull(interp);
ArgumentNullException.ThrowIfNull(output);
output.Origin = rootMotionLocalFrame.Origin;
output.Orientation = rootMotionLocalFrame.Orientation;
bool interpolationOverwrote = interp.AdjustOffset(
dt,
currentBodyPosition,
ori,
maxSpeed,
output,
inContact);
if (!interpolationOverwrote
&& terrainNormal.HasValue
&& terrainNormal.Value.Z > 0.01f)
{
Vector3 rootMotionWorld = Vector3.Transform(output.Origin, ori);
Vector3 normal = terrainNormal.Value;
rootMotionWorld -= normal * Vector3.Dot(rootMotionWorld, normal);
output.Origin = MoveToMath.GlobalToLocalVec(ori, rootMotionWorld);
}
return interpolationOverwrote;
}
/// <summary>
/// Compute the per-frame world-space delta to add to body.Position.
/// </summary>
@ -90,11 +135,21 @@ public sealed class RemoteMotionCombiner
// AdjustOffset returns Vector3.Zero in two cases mapped to retail's
// early-return: empty queue OR distance < DesiredDistance (0.05m).
// In both, body falls back to animation root motion.
Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
if (correction.LengthSquared() > 0f)
return correction;
Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocalDelta, ori);
var root = new MotionDeltaFrame
{
Origin = rootMotionLocalDelta,
};
var output = new MotionDeltaFrame();
ComposeOffset(
dt,
currentBodyPosition,
ori,
root,
interp,
maxSpeed,
output,
terrainNormal: null);
Vector3 rootMotionWorld = Vector3.Transform(output.Origin, ori);
// Slope projection (queue-empty fallback only). Locomotion cycles
// bake Z=0 in body-local, so without projection the body's Z stays