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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue