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
226
src/AcDream.App/Physics/LiveEntityOrdinaryPhysicsUpdater.cs
Normal file
226
src/AcDream.App/Physics/LiveEntityOrdinaryPhysicsUpdater.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the body-backed, manager-less branch of retail
|
||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0). A retained canonical
|
||||
/// body can temporarily outlive its RemoteMotion or projectile component; it
|
||||
/// must still compose the complete PartArray Frame, integrate physics, sweep
|
||||
/// through Transition, commit its cell, and move its collision shadow.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
||||
{
|
||||
private readonly PhysicsEngine _physics;
|
||||
private readonly Func<uint, WorldEntity, (float Radius, float Height)>
|
||||
_getSetupCylinder;
|
||||
|
||||
public LiveEntityOrdinaryPhysicsUpdater(
|
||||
PhysicsEngine physics,
|
||||
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder)
|
||||
{
|
||||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_getSetupCylinder = getSetupCylinder
|
||||
?? throw new ArgumentNullException(nameof(getSetupCylinder));
|
||||
}
|
||||
|
||||
public bool Tick(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
Frame rootFrame,
|
||||
float objectScale,
|
||||
float quantum,
|
||||
int liveCenterX,
|
||||
int liveCenterY,
|
||||
ulong objectClockEpoch,
|
||||
AnimationSequencer? sequencer,
|
||||
Action<uint, AnimationSequencer> captureAnimationHooks)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(rootFrame);
|
||||
ArgumentNullException.ThrowIfNull(captureAnimationHooks);
|
||||
if (record.PhysicsBody is not { } body
|
||||
|| !IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
body.State = record.FinalPhysicsState;
|
||||
Vector3 priorPosition = body.Position;
|
||||
Quaternion priorOrientation = body.Orientation;
|
||||
bool previousContact = body.InContact;
|
||||
bool previousOnWalkable = body.OnWalkable;
|
||||
|
||||
// UpdatePositionInternal 0x00512CA1: PartArray root translation is
|
||||
// scaled only while transient OnWalkable is set. Its orientation stays
|
||||
// in the complete Frame and composes regardless of that translation
|
||||
// gate.
|
||||
Vector3 candidatePosition = priorPosition;
|
||||
if (body.OnWalkable && rootFrame.Origin != Vector3.Zero)
|
||||
{
|
||||
candidatePosition += Vector3.Transform(
|
||||
rootFrame.Origin * objectScale,
|
||||
priorOrientation);
|
||||
}
|
||||
|
||||
Quaternion candidateOrientation = priorOrientation;
|
||||
if (!rootFrame.Orientation.IsIdentity)
|
||||
{
|
||||
candidateOrientation = FrameOps.SetRotate(
|
||||
candidatePosition,
|
||||
priorOrientation,
|
||||
priorOrientation * rootFrame.Orientation);
|
||||
}
|
||||
|
||||
body.SetFrameInCurrentCell(candidatePosition, candidateOrientation);
|
||||
body.calc_acceleration();
|
||||
body.UpdatePhysicsInternal(quantum);
|
||||
// Omega integration writes Orientation directly; mirror it into the
|
||||
// retained Position frame before Transition consumes the candidate.
|
||||
body.SetFrameInCurrentCell(body.Position, body.Orientation);
|
||||
|
||||
// UpdatePositionInternal processes PartArray hooks after physics but
|
||||
// before UpdateObjectInternal performs its transition sweep.
|
||||
if (sequencer is not null)
|
||||
captureAnimationHooks(entity.Id, sequencer);
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
Vector3 integratedPosition = body.Position;
|
||||
uint sourceCellId = record.FullCellId;
|
||||
uint resolvedCellId = sourceCellId;
|
||||
bool frameChanged = integratedPosition != priorPosition
|
||||
|| body.Orientation != priorOrientation;
|
||||
var (radius, height) = _getSetupCylinder(record.ServerGuid, entity);
|
||||
|
||||
if (integratedPosition != priorPosition
|
||||
&& sourceCellId != 0
|
||||
&& radius >= 0.05f
|
||||
&& _physics.LandblockCount > 0)
|
||||
{
|
||||
ResolveResult resolved = _physics.ResolveWithTransition(
|
||||
priorPosition,
|
||||
integratedPosition,
|
||||
sourceCellId,
|
||||
radius,
|
||||
height,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: previousOnWalkable,
|
||||
body: body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
? ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide
|
||||
: ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: entity.Id);
|
||||
|
||||
if (resolved.Ok)
|
||||
{
|
||||
resolvedCellId = resolved.CellId != 0
|
||||
? resolved.CellId
|
||||
: sourceCellId;
|
||||
body.CommitTransitionPosition(resolvedCellId, resolved.Position);
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
body,
|
||||
resolved.InContact,
|
||||
resolved.OnWalkable,
|
||||
resolved.CollisionNormalValid,
|
||||
resolved.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable);
|
||||
body.CachedVelocity = quantum > 0f
|
||||
? (body.Position - priorPosition) / quantum
|
||||
: Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// transition() returned null: UpdateObjectInternal keeps the
|
||||
// integrated frame in the current cell and reports no realized
|
||||
// transition velocity.
|
||||
body.CachedVelocity = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The no-PartArray-sphere arm calls set_frame and writes zero
|
||||
// cached_velocity rather than fabricating a collision shape.
|
||||
body.CachedVelocity = Vector3.Zero;
|
||||
}
|
||||
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
entity.SetPosition(body.Position);
|
||||
entity.Rotation = body.Orientation;
|
||||
entity.ParentCellId = resolvedCellId;
|
||||
if (resolvedCellId != sourceCellId
|
||||
&& !runtime.RebucketLiveEntity(record.ServerGuid, resolvedCellId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
if (frameChanged && record.IsSpatiallyVisible && record.FullCellId != 0)
|
||||
{
|
||||
ShadowPositionSynchronizer.Sync(
|
||||
_physics.ShadowObjects,
|
||||
entity.Id,
|
||||
body.Position,
|
||||
body.Orientation,
|
||||
record.FullCellId,
|
||||
liveCenterX,
|
||||
liveCenterY);
|
||||
}
|
||||
|
||||
return IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch);
|
||||
}
|
||||
|
||||
private static bool IsCurrent(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
PhysicsBody body,
|
||||
ulong objectClockEpoch) =>
|
||||
runtime.IsCurrentSpatialRootObject(record)
|
||||
&& record.ObjectClockEpoch == objectClockEpoch
|
||||
&& ReferenceEquals(record.WorldEntity, entity)
|
||||
&& ReferenceEquals(record.PhysicsBody, body)
|
||||
&& record.RemoteMotionRuntime is null
|
||||
&& record.ProjectileRuntime is null;
|
||||
|
||||
private static bool IsPlayerGuid(uint guid) =>
|
||||
(guid & 0xFF000000u) == 0x50000000u;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue