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

@ -42,6 +42,39 @@ public readonly record struct ProjectileAdvanceResult(
Vector3 CollisionNormal,
bool TransitionOk);
/// <summary>
/// Candidate frame produced by UpdatePhysicsInternal and held across retail's
/// process_hooks slot before the outer transition/collision commit.
/// </summary>
public readonly record struct ProjectileQuantumPreparation(
uint CellId,
float Quantum,
bool Simulated,
bool RequiresTransition,
Vector3 BeginPosition,
Quaternion BeginOrientation,
Position BeginCellPosition,
bool BeginInWorld,
ProjectileQuantumDynamics BeginDynamics,
Vector3 CandidatePosition,
Quaternion CandidateOrientation,
ProjectileQuantumDynamics CandidateDynamics,
bool PreviousContact,
bool PreviousOnWalkable);
/// <summary>
/// Mutable kinematic fields produced by the candidate integration. The split
/// App scheduler holds these transactionally across <c>process_hooks</c> so a
/// re-entrant authoritative correction never inherits an abandoned impulse.
/// </summary>
public readonly record struct ProjectileQuantumDynamics(
Vector3 Velocity,
Vector3 CachedVelocity,
Vector3 Acceleration,
Vector3 Omega,
TransientStateFlags TransientState,
double LastUpdateTime);
/// <summary>
/// Core-only port of the retail live-projectile physics driver. It owns no
/// renderer, network, or world-lifetime state: the App controller supplies a
@ -152,32 +185,84 @@ public sealed class ProjectilePhysicsStepper
transitionOk);
}
private void StepQuantum(
/// <summary>
/// Advances exactly one quantum already admitted by the owning
/// <see cref="RetailObjectQuantumClock"/>. This is the live-object path:
/// one CPhysicsObj clock admits PartArray, projectile physics, hooks, and
/// the manager tail together. The absolute-time overload remains for the
/// pure stepper conformance surface.
/// </summary>
public ProjectileAdvanceResult AdvanceQuantum(
PhysicsBody body,
float dt,
ref uint cellId,
float quantum,
uint cellId,
ProjectileCollisionSphere sphere,
uint movingEntityId,
uint designatedTargetId,
ref bool collisionNormalValid,
ref Vector3 collisionNormal,
ref bool transitionOk)
uint designatedTargetId = 0,
bool isParented = false)
{
ProjectileQuantumPreparation preparation = BeginQuantum(
body,
quantum,
cellId,
sphere,
isParented);
return CompleteQuantum(
body,
preparation,
sphere,
movingEntityId,
designatedTargetId);
}
public ProjectileQuantumPreparation BeginQuantum(
PhysicsBody body,
float quantum,
uint cellId,
ProjectileCollisionSphere sphere,
bool isParented = false)
{
ArgumentNullException.ThrowIfNull(body);
if (!float.IsFinite(quantum)
|| quantum <= PhysicsGlobals.EPSILON
|| quantum > PhysicsBody.MaxQuantum)
{
throw new ArgumentOutOfRangeException(
nameof(quantum),
quantum,
"An admitted object quantum must be finite and no larger than MaxQuantum.");
}
if (!sphere.IsValid)
return NotSimulated(cellId, quantum);
if (isParented || !body.InWorld
|| body.State.HasFlag(PhysicsStateFlags.Frozen)
|| body.State.HasFlag(PhysicsStateFlags.Static)
|| cellId == 0)
{
body.TransientState &= ~TransientStateFlags.Active;
return NotSimulated(cellId, quantum);
}
if (!body.IsActive)
return NotSimulated(cellId, quantum);
Vector3 beginPosition = body.Position;
Quaternion beginOrientation = body.Orientation;
Position beginCellPosition = body.CellPosition;
bool beginInWorld = body.InWorld;
ProjectileQuantumDynamics beginDynamics = CaptureDynamics(body);
bool previousContact = body.InContact;
bool previousOnWalkable = body.OnWalkable;
// Final PhysicsState drives acceleration on every quantum. Network
// acceleration is retained by the protocol parser but not installed.
body.calc_acceleration();
body.UpdatePhysicsInternal(dt);
body.UpdatePhysicsInternal(quantum);
Vector3 candidatePosition = body.Position;
Quaternion candidateOrientation = body.Orientation;
Vector3 displacement = candidatePosition - beginPosition;
bool candidateMoved = displacement != Vector3.Zero;
if (candidateMoved && body.State.HasFlag(PhysicsStateFlags.AlignPath))
{
candidateOrientation = RetailFrameMath.SetVectorHeading(
@ -189,76 +274,218 @@ public sealed class ProjectilePhysicsStepper
{
body.CachedVelocity = Vector3.Zero;
body.Orientation = candidateOrientation;
return;
body.LastUpdateTime += quantum;
}
else
{
// process_hooks runs against the post-physics candidate while the
// authoritative root remains at the begin frame until transition.
body.Position = beginPosition;
body.Orientation = beginOrientation;
}
// The integrator produces a candidate without committing the root.
// Restore the authoritative start frame before transition setup so
// carried cell-relative position and self-shadow identity remain exact.
body.Position = beginPosition;
body.Orientation = beginOrientation;
ProjectileQuantumDynamics candidateDynamics = CaptureDynamics(body);
RestoreBeginFrame(
body,
beginPosition,
beginOrientation,
beginCellPosition,
beginInWorld);
ApplyDynamics(body, beginDynamics);
return new ProjectileQuantumPreparation(
cellId,
quantum,
Simulated: true,
RequiresTransition: candidateMoved,
beginPosition,
beginOrientation,
beginCellPosition,
beginInWorld,
beginDynamics,
candidatePosition,
candidateOrientation,
candidateDynamics,
previousContact,
previousOnWalkable);
}
public ProjectileAdvanceResult CompleteQuantum(
PhysicsBody body,
in ProjectileQuantumPreparation preparation,
ProjectileCollisionSphere sphere,
uint movingEntityId,
uint designatedTargetId = 0)
{
ArgumentNullException.ThrowIfNull(body);
if (!preparation.Simulated)
return new ProjectileAdvanceResult(preparation.CellId, 0, false, false, default, true);
ApplyDynamics(body, preparation.CandidateDynamics);
if (!preparation.RequiresTransition)
{
body.SetFrameInCurrentCell(
preparation.CandidatePosition,
preparation.CandidateOrientation);
return new ProjectileAdvanceResult(preparation.CellId, 1, true, false, default, true);
}
uint cellId = preparation.CellId;
ObjectInfoState moverFlags = body.State.HasFlag(PhysicsStateFlags.PathClipped)
? ObjectInfoState.PathClipped
: ObjectInfoState.None;
var resolved = _physics.ResolveWithTransition(
beginPosition,
candidatePosition,
preparation.BeginPosition,
preparation.CandidatePosition,
cellId,
sphereRadius: sphere.Radius,
sphereHeight: 0f,
stepUpHeight: PhysicsGlobals.DefaultStepHeight,
stepDownHeight: 0f,
isOnGround: previousOnWalkable,
isOnGround: preparation.PreviousOnWalkable,
body: body,
moverFlags: moverFlags,
movingEntityId: movingEntityId,
localSphereOrigin: sphere.LocalOrigin,
beginOrientation: beginOrientation,
endOrientation: candidateOrientation,
beginOrientation: preparation.BeginOrientation,
endOrientation: preparation.CandidateOrientation,
designatedTargetId: designatedTargetId);
bool collisionNormalValid = false;
Vector3 collisionNormal = default;
bool transitionOk = resolved.Ok;
if (!resolved.Ok)
{
// UpdateObjectInternal 0x005156B0: when transition() returns null,
// retail calls set_frame(candidate), zeros cached_velocity, and
// deliberately skips SetPositionInternal/handle_all_collisions.
// set_frame retains Position.objcell_id even when the candidate
// frame lies beyond the current outdoor cell's canonical range.
body.SetFrameInCurrentCell(candidatePosition, candidateOrientation);
body.SetFrameInCurrentCell(
preparation.CandidatePosition,
preparation.CandidateOrientation);
body.CachedVelocity = Vector3.Zero;
transitionOk = false;
return;
}
body.CachedVelocity = (resolved.Position - beginPosition) / dt;
body.Position = resolved.Position;
body.Orientation = resolved.Orientation == default
? candidateOrientation
: resolved.Orientation;
if (resolved.CellId != 0)
cellId = resolved.CellId;
// SetPositionInternal 0x00515330 commits Contact and OnWalkable as
// distinct facts before handle_all_collisions. A steep surface can be
// contact without becoming walkable ground.
PhysicsObjUpdate.ApplySetPositionContact(
body,
resolved.InContact,
resolved.OnWalkable);
PhysicsObjUpdate.HandleAllCollisions(
body,
resolved.CollisionNormalValid,
resolved.CollisionNormal,
previousContact,
previousOnWalkable,
nowOnWalkable: body.OnWalkable);
if (resolved.CollisionNormalValid)
else
{
collisionNormalValid = true;
collisionNormal = resolved.CollisionNormal;
body.CachedVelocity = (resolved.Position - preparation.BeginPosition)
/ preparation.Quantum;
body.Position = resolved.Position;
body.Orientation = resolved.Orientation == default
? preparation.CandidateOrientation
: resolved.Orientation;
if (resolved.CellId != 0)
cellId = resolved.CellId;
PhysicsObjUpdate.ApplySetPositionContact(
body,
resolved.InContact,
resolved.OnWalkable);
PhysicsObjUpdate.HandleAllCollisions(
body,
resolved.CollisionNormalValid,
resolved.CollisionNormal,
preparation.PreviousContact,
preparation.PreviousOnWalkable,
nowOnWalkable: body.OnWalkable);
if (resolved.CollisionNormalValid)
{
collisionNormalValid = true;
collisionNormal = resolved.CollisionNormal;
}
}
body.LastUpdateTime += preparation.Quantum;
return new ProjectileAdvanceResult(
cellId,
1,
true,
collisionNormalValid,
collisionNormal,
transitionOk);
}
private static ProjectileQuantumPreparation NotSimulated(
uint cellId,
float quantum) => new(
CellId: cellId,
Quantum: quantum,
Simulated: false,
RequiresTransition: false,
BeginPosition: default,
BeginOrientation: default,
BeginCellPosition: default,
BeginInWorld: false,
BeginDynamics: default,
CandidatePosition: default,
CandidateOrientation: default,
CandidateDynamics: default,
PreviousContact: false,
PreviousOnWalkable: false);
private static ProjectileQuantumDynamics CaptureDynamics(PhysicsBody body) => new(
body.Velocity,
body.CachedVelocity,
body.Acceleration,
body.Omega,
body.TransientState,
body.LastUpdateTime);
private static void ApplyDynamics(
PhysicsBody body,
in ProjectileQuantumDynamics dynamics)
{
body.Velocity = dynamics.Velocity;
body.CachedVelocity = dynamics.CachedVelocity;
body.Acceleration = dynamics.Acceleration;
body.Omega = dynamics.Omega;
body.TransientState = dynamics.TransientState;
body.LastUpdateTime = dynamics.LastUpdateTime;
}
private static void RestoreBeginFrame(
PhysicsBody body,
Vector3 beginPosition,
Quaternion beginOrientation,
in Position beginCellPosition,
bool beginInWorld)
{
body.Orientation = beginOrientation;
if (beginCellPosition.ObjCellId != 0)
{
body.SnapToCell(
beginCellPosition.ObjCellId,
beginPosition,
beginCellPosition.Frame.Origin);
}
else
{
body.SetFrameInCurrentCell(beginPosition, beginOrientation);
}
body.InWorld = beginInWorld;
}
private void StepQuantum(
PhysicsBody body,
float dt,
ref uint cellId,
ProjectileCollisionSphere sphere,
uint movingEntityId,
uint designatedTargetId,
ref bool collisionNormalValid,
ref Vector3 collisionNormal,
ref bool transitionOk)
{
ProjectileQuantumPreparation preparation = BeginQuantum(
body,
dt,
cellId,
sphere);
ProjectileAdvanceResult result = CompleteQuantum(
body,
preparation,
sphere,
movingEntityId,
designatedTargetId);
if (result.CellId != 0)
cellId = result.CellId;
collisionNormalValid |= result.CollisionNormalValid;
if (result.CollisionNormalValid)
collisionNormal = result.CollisionNormal;
transitionOk &= result.TransitionOk;
}
}