feat(physics): integrate live projectile runtime

Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions.

Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped.

Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 12:51:42 +02:00
parent f02b995e4f
commit a51ebc66e9
12 changed files with 2559 additions and 32 deletions

View file

@ -0,0 +1,744 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Physics;
/// <summary>
/// Update-thread presentation owner for live retail missiles. Logical identity,
/// generation, accepted network state, and teardown remain owned by
/// <see cref="LiveEntityRuntime"/>; this controller only advances the
/// <see cref="PhysicsBody"/> attached to that canonical record and commits its
/// frame through the existing <see cref="WorldEntity"/> projection.
/// </summary>
/// <remarks>
/// Retail anchors are <c>CPhysicsObj::update_object</c> <c>0x00515D10</c>,
/// <c>CPhysicsObj::UpdateObjectInternal</c> <c>0x005156B0</c>, and
/// <c>CPhysicsObj::SetPositionInternal</c> <c>0x00515330</c>. The pure
/// integration/sweep is implemented by <see cref="ProjectilePhysicsStepper"/>.
/// ACE remains authoritative for collision outcomes, damage, effects, and
/// deletion; no such events are synthesized here.
/// </remarks>
internal sealed class ProjectileController
{
internal sealed class Runtime : ILiveEntityProjectileRuntime
{
internal Runtime(
ushort generation,
PhysicsBody body,
ProjectileCollisionSphere collisionSphere,
bool hasPartArray)
{
Generation = generation;
Body = body;
CollisionSphere = collisionSphere;
HasPartArray = hasPartArray;
}
internal ushort Generation { get; }
public PhysicsBody Body { get; }
internal ProjectileCollisionSphere CollisionSphere { get; }
internal bool HasPartArray { get; }
}
private readonly LiveEntityRuntime _liveEntities;
private readonly ProjectilePhysicsStepper _stepper;
private readonly ShadowObjectRegistry _shadows;
private readonly Func<uint, Setup?> _setupResolver;
private readonly Action<WorldEntity> _publishRootPose;
private double _lastFiniteGameTime;
internal ProjectileController(
LiveEntityRuntime liveEntities,
PhysicsEngine physicsEngine,
Func<uint, Setup?>? setupResolver = null,
Action<WorldEntity>? publishRootPose = null)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
ArgumentNullException.ThrowIfNull(physicsEngine);
_stepper = new ProjectilePhysicsStepper(physicsEngine);
_shadows = physicsEngine.ShadowObjects;
_setupResolver = setupResolver ?? (_ => null);
_publishRootPose = publishRootPose ?? (_ => { });
}
internal Action<string>? DiagnosticSink { get; set; }
internal int Count => _liveEntities.Records.Count(
static record => record.ProjectileRuntime is Runtime);
internal bool CanAcceptVectorPayload(
uint serverGuid,
Vector3 velocity,
Vector3 angularVelocity)
{
bool valid = IsFinite(velocity) && IsFinite(angularVelocity);
if (!valid)
DiagnosticSink?.Invoke(
$"Rejected invalid VectorUpdate for live object 0x{serverGuid:X8}.");
return valid;
}
internal bool CanAcceptPositionPayload(
uint serverGuid,
CreateObject.ServerPosition position,
Vector3? velocity)
{
var origin = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ);
var orientation = new Quaternion(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW);
bool valid = IsFinite(origin)
&& PositionFrameValidation.IsValid(
position.LandblockId,
origin,
orientation)
&& (velocity is null || IsFinite(velocity.Value));
if (!valid)
DiagnosticSink?.Invoke(
$"Rejected invalid PositionUpdate for live object 0x{serverGuid:X8}.");
return valid;
}
/// <summary>
/// Installs a projectile body once, after the live record and render
/// projection exist. Re-entry returns the same component and never replays
/// logical registration or create-time effects.
/// </summary>
internal bool TryBind(
LiveEntityRecord record,
Setup setup,
double currentTime,
int liveCenterX = 0,
int liveCenterY = 0)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(setup);
if ((record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0)
return false;
if (record.ProjectileRuntime is Runtime retained)
{
retained.Body.State = record.FinalPhysicsState;
return true;
}
if (record.WorldEntity is not { } entity
|| record.Snapshot.Physics is not { } physics
|| record.Snapshot.Position is not { } wirePosition)
return false;
float scale = physics.Scale ?? record.Snapshot.ObjScale ?? 1f;
if (!TryGetCollisionSphere(setup, scale, out ProjectileCollisionSphere sphere))
{
DiagnosticSink?.Invoke(
$"Missile 0x{record.ServerGuid:X8} Setup 0x{record.Snapshot.SetupTableId ?? 0u:X8} " +
$"does not have the supported retail one-sphere collision shape.");
return false;
}
if (!double.IsFinite(currentTime))
{
DiagnosticSink?.Invoke(
$"Missile 0x{record.ServerGuid:X8} has an invalid physics clock.");
return false;
}
_lastFiniteGameTime = currentTime;
PhysicsBody body;
if (record.RemoteMotionRuntime is { } remote)
{
// Retail owns one CPhysicsObj. If a non-missile incarnation
// already created its MovementManager, classification adopts that
// same body instead of replacing it.
body = remote.Body;
uint currentCellId = record.FullCellId;
Vector3 currentCellLocal = CellLocalFromWorld(
body.Position,
currentCellId,
liveCenterX,
liveCenterY);
if (!IsFinite(body.Position)
|| !IsFinite(body.Velocity)
|| !IsFinite(body.Omega)
|| !PositionFrameValidation.IsValid(
currentCellId,
currentCellLocal,
body.Orientation))
{
DiagnosticSink?.Invoke(
$"Missile 0x{record.ServerGuid:X8} has an invalid current shared physics frame or vector.");
return false;
}
body.State = record.FinalPhysicsState;
body.Friction = NormalizeFriction(
physics.Friction ?? record.Snapshot.Friction);
body.Elasticity = NormalizeElasticity(
physics.Elasticity ?? record.Snapshot.Elasticity ?? 0.05f);
// The MovementManager can predate missile classification and may
// already have advanced beyond the retained CreateObject frame.
// Preserve its current velocity, omega, orientation, and Active
// state exactly as retail SetState does. Only normalize the
// carried cell frame and translate the legacy remote clock onto
// the App projectile clock.
body.SnapToCell(
currentCellId,
body.Position,
currentCellLocal);
body.LastUpdateTime = currentTime;
}
else
{
Vector3 velocity = physics.Velocity ?? Vector3.Zero;
Vector3 omega = physics.AngularVelocity ?? Vector3.Zero;
if (!IsFinite(entity.Position)
|| !PositionFrameValidation.IsValid(
wirePosition.LandblockId,
new Vector3(
wirePosition.PositionX,
wirePosition.PositionY,
wirePosition.PositionZ),
entity.Rotation)
|| !IsFinite(velocity)
|| !IsFinite(omega))
{
DiagnosticSink?.Invoke(
$"Missile 0x{record.ServerGuid:X8} has an invalid initial physics frame or vector.");
return false;
}
body = new PhysicsBody
{
State = record.FinalPhysicsState,
Friction = NormalizeFriction(
physics.Friction ?? record.Snapshot.Friction),
Elasticity = NormalizeElasticity(
physics.Elasticity ?? record.Snapshot.Elasticity ?? 0.05f),
Orientation = entity.Rotation,
LastUpdateTime = currentTime,
};
SetVelocity(body, velocity, currentTime);
body.Omega = omega;
body.SnapToCell(
wirePosition.LandblockId,
entity.Position,
new Vector3(
wirePosition.PositionX,
wirePosition.PositionY,
wirePosition.PositionZ));
}
uint canonicalCellId = body.CellPosition.ObjCellId;
entity.SetPosition(body.Position);
entity.Rotation = body.Orientation;
entity.ParentCellId = canonicalCellId;
_liveEntities.RebucketLiveEntity(record.ServerGuid, canonicalCellId);
var runtime = new Runtime(
record.Generation,
body,
sphere,
entity.MeshRefs.Count > 0);
_liveEntities.SetProjectileRuntime(record.ServerGuid, runtime);
if (HasVisibleCell(record))
{
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
entity.Position,
entity.Rotation,
canonicalCellId,
liveCenterX,
liveCenterY);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
}
_publishRootPose(entity);
return true;
}
internal bool ApplyAuthoritativeVector(
uint serverGuid,
Vector3 velocity,
Vector3 angularVelocity,
double currentTime)
{
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime))
return false;
// Once Missile is cleared, an existing MovementManager owns ordinary
// jump/vector side effects (Airborne, gravity, LeaveGround) on this
// same body. Let that established funnel consume the packet. A
// projectile-only object still needs the unconditional retail
// set_velocity/set_omega writes here.
if ((record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
&& record.RemoteMotionRuntime is not null)
return false;
if (!IsFinite(velocity)
|| !IsFinite(angularVelocity)
|| !double.IsFinite(currentTime))
{
DiagnosticSink?.Invoke(
$"Rejected invalid VectorUpdate for missile 0x{serverGuid:X8}.");
return true;
}
_lastFiniteGameTime = currentTime;
SetVelocity(runtime.Body, velocity, currentTime);
runtime.Body.Omega = angularVelocity;
if (!_liveEntities.ShouldAdvanceRootRuntime(serverGuid))
Deactivate(runtime.Body);
return true;
}
internal bool ApplyAuthoritativeState(
uint serverGuid,
PhysicsStateFlags state,
double currentTime,
int liveCenterX,
int liveCenterY)
{
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
return false;
bool validClock = double.IsFinite(currentTime);
double effectiveClock = validClock
? currentTime
: _lastFiniteGameTime;
if (validClock)
_lastFiniteGameTime = currentTime;
if (!validClock)
DiagnosticSink?.Invoke(
$"Ignored invalid State update clock for live object 0x{serverGuid:X8}; state remains authoritative.");
if (record.ProjectileRuntime is Runtime runtime)
{
bool wasMissile =
(runtime.Body.State & PhysicsStateFlags.Missile) != 0;
runtime.Body.State = state;
if ((state & PhysicsStateFlags.Missile) != 0 && !wasMissile)
{
// Ownership transfers back from generic remote motion to the
// absolute projectile clock. State alone must not activate,
// but an already-active body keeps moving from now rather
// than replaying the ordinary-motion interval. A malformed
// local receipt clock is not retail state and cannot clear
// Active; translate to the last finite App clock instead.
runtime.Body.LastUpdateTime = effectiveClock;
if (record.FullCellId != 0)
{
runtime.Body.SnapToCell(
record.FullCellId,
runtime.Body.Position,
CellLocalFromWorld(
runtime.Body.Position,
record.FullCellId,
liveCenterX,
liveCenterY));
}
}
return true;
}
// SetState applies to the canonical CPhysicsObj independently of
// whether this state also makes projectile-component acquisition
// possible. Missing DAT/shape data must never leave the existing body
// on stale collision flags.
if (record.RemoteMotionRuntime is { } remote)
remote.Body.State = state;
if ((state & PhysicsStateFlags.Missile) == 0
|| record.WorldEntity is not { } entity)
return false;
// A newer State packet can make an already materialized object a
// missile. TryBind adopts an existing MovementManager body so the
// live record still represents retail's one CPhysicsObj.
Setup? setup = _setupResolver(entity.SourceGfxObjOrSetupId);
if (setup is null)
{
DiagnosticSink?.Invoke(
$"Cannot activate missile 0x{serverGuid:X8}: Setup 0x{entity.SourceGfxObjOrSetupId:X8} is unavailable.");
return true;
}
TryBind(record, setup, effectiveClock, liveCenterX, liveCenterY);
return true;
}
/// <summary>
/// Applies a timestamp-gated server correction to the same predicted body.
/// The caller supplies both render-world and wire cell-local coordinates so
/// streaming origin changes never leak into the canonical cell frame.
/// </summary>
internal bool ApplyAuthoritativePosition(
uint serverGuid,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY)
{
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime)
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
|| record.WorldEntity is not { } entity)
return false;
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
DiagnosticSink?.Invoke(
$"Rejected invalid PositionUpdate for missile 0x{serverGuid:X8}.");
return true;
}
_lastFiniteGameTime = currentTime;
PhysicsBody body = runtime.Body;
bool wasInWorld = body.InWorld;
body.Orientation = orientation;
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
body.State = record.FinalPhysicsState;
uint canonicalCellId = body.CellPosition.ObjCellId;
entity.SetPosition(worldPosition);
entity.Rotation = orientation;
entity.ParentCellId = canonicalCellId;
_liveEntities.RebucketLiveEntity(serverGuid, canonicalCellId);
if (HasVisibleCell(record))
{
if (!wasInWorld)
Activate(body, currentTime);
body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
worldPosition,
orientation,
canonicalCellId,
liveCenterX,
liveCenterY);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
}
_publishRootPose(entity);
return true;
}
/// <summary>
/// Movement packets use the ordinary MovementManager/animation funnel on
/// this same body. The generic remote translation tick consults this gate
/// so only the projectile integrator commits its root frame.
/// </summary>
internal bool HandlesMovement(uint serverGuid) =>
TryGetCurrent(serverGuid, out LiveEntityRecord record, out _)
&& (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0;
internal bool TryGetBody(uint serverGuid, out PhysicsBody body)
{
if (TryGetCurrent(serverGuid, out _, out Runtime runtime))
{
body = runtime.Body;
return true;
}
body = null!;
return false;
}
internal bool LeaveWorld(uint serverGuid)
{
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime)
|| record.WorldEntity is not { } entity)
return false;
SuspendOutsideWorld(runtime, entity.Id);
return true;
}
internal void Tick(
double currentTime,
int liveCenterX,
int liveCenterY,
Vector3? playerWorldPosition)
{
if (!double.IsFinite(currentTime))
{
DiagnosticSink?.Invoke("Rejected non-finite projectile update clock.");
return;
}
_lastFiniteGameTime = currentTime;
foreach (LiveEntityRecord record in _liveEntities.Records.ToArray())
{
if (record.ProjectileRuntime is not Runtime runtime
|| runtime.Generation != record.Generation
|| record.WorldEntity is not { } entity)
continue;
bool isMissile =
(record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0;
if (!isMissile)
{
// World entry/exit is independent of Missile classification.
// A projectile can enter a pending bucket, lose Missile, then
// hydrate as an ordinary live object. Restore its retained
// shadow/body membership without reactivating projectile
// integration or consuming the out-of-world clock gap.
if (!HasVisibleCell(record))
{
SuspendOutsideWorld(runtime, entity.Id);
}
else if (!runtime.Body.InWorld)
{
runtime.Body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
runtime.Body.Position,
runtime.Body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
_publishRootPose(entity);
}
// A retained body without MovementManager still remains an
// active CPhysicsObj after Missile is cleared. Continue the
// same one-sphere update path; PhysicsEngine reads the final
// state and therefore disables missile_ignore automatically.
if (record.RemoteMotionRuntime is not null)
continue;
}
runtime.Body.State = record.FinalPhysicsState;
if (!_liveEntities.ShouldAdvanceRootRuntime(record.ServerGuid))
{
if (!HasVisibleCell(record))
SuspendOutsideWorld(runtime, entity.Id);
else
Deactivate(runtime.Body); // Frozen retains its cell/shadow.
continue;
}
if (!runtime.Body.InWorld)
{
runtime.Body.InWorld = true;
Activate(runtime.Body, currentTime);
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
runtime.Body.Position,
runtime.Body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
_publishRootPose(entity);
}
// CPhysicsObj::update_object 0x00515D4D-0x00515DB2 keeps
// rendered objects active only within 96 world units of the
// player. Objects with no PartArray are exempt.
if (runtime.HasPartArray && playerWorldPosition is { } playerPosition)
{
float playerDistance = Vector3.Distance(runtime.Body.Position, playerPosition);
if (!float.IsFinite(playerDistance) || playerDistance > 96f)
{
Deactivate(runtime.Body);
continue;
}
Activate(runtime.Body, currentTime);
}
bool isParented = record.Snapshot.ParentGuid is not null
|| record.Snapshot.Physics?.Parent is not null;
ProjectileAdvanceResult result = _stepper.Advance(
runtime.Body,
currentTime,
record.FullCellId,
runtime.CollisionSphere,
entity.Id,
designatedTargetId: 0u,
isParented);
// An owner can be deleted/replaced by a callback reached from a
// future collision sink. Never commit a completed step into a new
// incarnation that reused the same server GUID.
if (!TryGetCurrent(record.ServerGuid, out LiveEntityRecord current, out Runtime currentRuntime)
|| !ReferenceEquals(runtime, currentRuntime)
|| !ReferenceEquals(record, current)
|| !result.Simulated)
continue;
uint priorCellId = record.FullCellId;
uint resolvedCellId = result.CellId != 0
? result.CellId
: priorCellId;
entity.SetPosition(runtime.Body.Position);
entity.Rotation = runtime.Body.Orientation;
entity.ParentCellId = resolvedCellId;
if (resolvedCellId != priorCellId)
_liveEntities.RebucketLiveEntity(record.ServerGuid, resolvedCellId);
runtime.Body.SnapToCell(
resolvedCellId,
runtime.Body.Position,
CellLocalFromWorld(
runtime.Body.Position,
resolvedCellId,
liveCenterX,
liveCenterY));
// A successful transition can cross into an unloaded landblock.
// Rebucket first, then make the physics/shadow decision from the
// resulting projection state in this same update quantum. Retail
// never leaves a CPhysicsObj active in a cell that is outside the
// current object-maintenance world.
if (HasVisibleCell(record))
{
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
runtime.Body.Position,
runtime.Body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
_publishRootPose(entity);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
}
}
}
private static bool HasVisibleCell(LiveEntityRecord record) =>
record.IsSpatiallyProjected
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
private void SuspendOutsideWorld(Runtime runtime, uint localEntityId)
{
runtime.Body.InWorld = false;
Deactivate(runtime.Body);
_shadows.Suspend(localEntityId);
}
private static void Activate(PhysicsBody body, double currentTime)
{
if ((body.State & PhysicsStateFlags.Static) != 0)
return;
if ((body.TransientState & TransientStateFlags.Active) == 0)
body.LastUpdateTime = currentTime;
body.TransientState |= TransientStateFlags.Active;
}
private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active;
private static void SetVelocity(
PhysicsBody body,
Vector3 velocity,
double currentTime)
{
bool wasActive = (body.TransientState & TransientStateFlags.Active) != 0;
body.set_velocity(velocity);
if ((body.State & PhysicsStateFlags.Static) != 0)
{
Deactivate(body);
return;
}
if (!wasActive && (body.State & PhysicsStateFlags.Static) == 0)
body.LastUpdateTime = currentTime;
}
private static float NormalizeFriction(float? value) =>
value is >= 0f and <= 1f && float.IsFinite(value.Value)
? value.Value
: PhysicsBody.DefaultFriction;
private static float NormalizeElasticity(float value)
{
if (float.IsNaN(value) || value <= 0f)
return 0f;
return MathF.Min(value, 0.1f);
}
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
private static Vector3 CellLocalFromWorld(
Vector3 worldPosition,
uint cellId,
int liveCenterX,
int liveCenterY)
{
int landblockX = (int)((cellId >> 24) & 0xFFu);
int landblockY = (int)((cellId >> 16) & 0xFFu);
return worldPosition - new Vector3(
(landblockX - liveCenterX) * 192f,
(landblockY - liveCenterY) * 192f,
0f);
}
private bool TryGetCurrent(
uint serverGuid,
out LiveEntityRecord record,
out Runtime runtime)
{
if (_liveEntities.TryGetRecord(serverGuid, out record!)
&& record.ProjectileRuntime is Runtime found
&& found.Generation == record.Generation)
{
runtime = found;
return true;
}
record = null!;
runtime = null!;
return false;
}
private static bool TryGetCollisionSphere(
Setup setup,
float scale,
out ProjectileCollisionSphere sphere)
{
// Installed DAT audit in 2026-07-13-retail-projectile-vfx-pseudocode.md
// pins every required arrow, bolt, and spell projectile to one ordinary
// Setup sphere (including Force Bolt's non-centered origin). Do not
// fabricate a mesh-derived radius for an unsupported Setup.
if (setup.Spheres.Count == 1)
{
var source = setup.Spheres[0];
sphere = new ProjectileCollisionSphere(source.Origin, source.Radius, scale);
if (sphere.IsValid)
return true;
}
sphere = default;
return false;
}
}

View file

@ -0,0 +1,39 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Physics;
namespace AcDream.App.Physics;
/// <summary>
/// Applies CreateObject physics vectors when the ordinary remote-motion body
/// is first constructed. This keeps initialization at object/body creation;
/// a later SetState classification change never replays retained vectors.
/// </summary>
/// <remarks>
/// Retail <c>CPhysicsObj::set_description</c> <c>0x00514F40</c> installs the
/// unpacked velocity/omega during object initialization. Retail
/// <c>SmartBox::DoSetState</c> <c>0x004520D0</c> only calls
/// <c>CPhysicsObj::set_state</c> and therefore preserves the current vectors.
/// </remarks>
internal static class RemotePhysicsBodyInitializer
{
internal static void Initialize(PhysicsBody body, LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(body);
ArgumentNullException.ThrowIfNull(record);
body.State = record.FinalPhysicsState;
if (record.Snapshot.Physics is not { } physics)
return;
if (physics.Velocity is { } velocity && IsFinite(velocity))
body.set_velocity(velocity);
if (physics.AngularVelocity is { } omega && IsFinite(omega))
body.Omega = omega;
}
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
}