refactor(runtime): own per-session physics simulation

Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order.

Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-26 13:39:57 +02:00
parent 0dc3bfdeff
commit 7e6033d0ad
39 changed files with 3685 additions and 1722 deletions

View file

@ -1,30 +1,29 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
using AcDream.Runtime.Physics;
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.
/// Projects the presentation-free Runtime result of retail
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) into App's
/// <see cref="WorldEntity"/> and spatial buckets.
/// </summary>
internal sealed class LiveEntityOrdinaryPhysicsUpdater
{
private readonly PhysicsEngine _physics;
private readonly RuntimeOrdinaryPhysicsUpdater _runtime;
private readonly Func<uint, WorldEntity, (float Radius, float Height)>
_getSetupCylinder;
public LiveEntityOrdinaryPhysicsUpdater(
PhysicsEngine physics,
RuntimePhysicsState physics,
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_runtime = new RuntimeOrdinaryPhysicsUpdater(
physics ?? throw new ArgumentNullException(nameof(physics)));
_getSetupCylinder = getSetupCylinder
?? throw new ArgumentNullException(nameof(getSetupCylinder));
}
@ -47,165 +46,47 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
ArgumentNullException.ThrowIfNull(entity);
ArgumentNullException.ThrowIfNull(rootFrame);
ArgumentNullException.ThrowIfNull(captureAnimationHooks);
if (record.PhysicsBody is not { } body
|| !IsCurrent(
runtime,
record,
entity,
body,
objectClockEpoch))
{
if (record.PhysicsBody is not { } body)
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);
bool ExternalOwnerValid() =>
IsCurrent(
runtime,
record,
entity,
body,
objectClockEpoch);
if (integratedPosition != priorPosition
&& sourceCellId != 0
&& radius >= 0.05f
&& _physics.LandblockCount > 0)
{
ResolveResult resolved = _physics.ResolveWithTransition(
priorPosition,
integratedPosition,
sourceCellId,
if (!_runtime.TryBegin(
record.Canonical,
rootFrame,
objectScale,
quantum,
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);
objectClockEpoch,
sequencer,
captureAnimationHooks,
ExternalOwnerValid,
out RuntimeOrdinaryPhysicsCommit commit))
{
return false;
}
if (resolved.Ok)
return _runtime.Complete(
commit,
liveCenterX,
liveCenterY,
snapshot =>
{
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 (!ExternalOwnerValid())
return false;
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);
entity.SetPosition(snapshot.Position);
entity.Rotation = snapshot.Orientation;
entity.ParentCellId = snapshot.FullCellId;
return ExternalOwnerValid();
});
}
private static bool IsCurrent(
@ -220,7 +101,4 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
&& ReferenceEquals(record.PhysicsBody, body)
&& record.RemoteMotionRuntime is null
&& record.ProjectileRuntime is null;
private static bool IsPlayerGuid(uint guid) =>
(guid & 0xFF000000u) == 0x50000000u;
}