feat(vfx): port retail hidden and teleport presentation

Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -11,6 +11,8 @@ namespace AcDream.App.Physics;
/// frame, from inside the same guard the body used to live under
/// (<c>ae.Sequencer != null &amp;&amp; serverGuid != 0 &amp;&amp; serverGuid != _playerServerGuid
/// &amp;&amp; rm.LastServerPosTime &gt; 0</c>).
/// Hidden remotes use a separate live-entity pass because retail keeps their
/// PositionManager alive even when the object has no render-animation owner.
///
/// <para>Slice 2a extracted this verbatim (fork intact). Slice 2b then COLLAPSED
/// the player/NPC fork: the former Path A (grounded PLAYER remotes advanced by the
@ -55,6 +57,35 @@ internal sealed class RemotePhysicsUpdater
// Duplicated one-liner (GameWindow keeps its own copy — many callers there).
private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u;
/// <summary>
/// Advances the retained PositionManager path for every hidden remote,
/// including objects without an <c>AnimatedEntity</c> (missiles commonly
/// have no PartArray). Retail gates this path on the live CPhysicsObj, not
/// on whether a render-animation owner exists.
/// </summary>
public void TickHiddenEntities(
AcDream.App.World.LiveEntityRuntime liveEntities,
uint localPlayerServerGuid,
float dt,
System.Action<AcDream.Core.World.WorldEntity> publishRootPose)
{
ArgumentNullException.ThrowIfNull(liveEntities);
ArgumentNullException.ThrowIfNull(publishRootPose);
foreach (AcDream.App.World.LiveEntityRecord record in liveEntities.Records.ToArray())
{
if (record.ServerGuid == localPlayerServerGuid
|| (record.FinalPhysicsState & AcDream.Core.Physics.PhysicsStateFlags.Hidden) == 0
|| !liveEntities.ShouldAdvanceRootRuntime(record.ServerGuid)
|| record.RemoteMotionRuntime is not RemoteMotion remote
|| record.WorldEntity is not { } entity)
continue;
TickHidden(remote, entity, dt);
publishRootPose(entity);
}
}
/// <summary>
/// #184 Slice 2a — the per-remote DR tick (retail <c>UpdateObjectInternal</c>
/// shape), verbatim from the former <c>GameWindow.TickAnimations</c> guard
@ -556,6 +587,99 @@ internal sealed class RemotePhysicsUpdater
rm.Host?.PositionManager.UseTime();
}
/// <summary>
/// Retail hidden-object slice of <c>CPhysicsObj::UpdatePositionInternal</c>
/// (<c>0x00512C30</c>) plus the manager tail of
/// <c>UpdateObjectInternal</c> (<c>0x005156B0</c>). Hidden skips
/// <c>CPartArray::Update</c> and <c>UpdatePhysicsInternal</c>, but the
/// PositionManager offset is still composed and the target, movement, and
/// position managers still consume time. Physics-script and particle owners
/// tick later in the shared frame pipeline.
/// </summary>
public void TickHidden(
RemoteMotion rm,
AcDream.Core.World.WorldEntity entity,
float dt)
{
ArgumentNullException.ThrowIfNull(rm);
ArgumentNullException.ThrowIfNull(entity);
System.Numerics.Vector3 preComposePosition = rm.Body.Position;
// The part-array contribution is the identity frame while Hidden.
// Interpolation is the first PositionManager stage in retail; acdream
// retains it in RemoteMotionCombiner, ahead of Sticky/Constraint.
System.Numerics.Vector3 interpolationOffset = rm.Position.ComputeOffset(
dt,
rm.Body.Position,
System.Numerics.Vector3.Zero,
rm.Body.Orientation,
rm.Interp,
rm.Motion.GetMaxSpeed());
var positionDelta = new AcDream.Core.Physics.Motion.MotionDeltaFrame
{
Origin = AcDream.Core.Physics.Motion.MoveToMath.GlobalToLocalVec(
rm.Body.Orientation,
interpolationOffset),
};
rm.Host?.PositionManager.AdjustOffset(positionDelta, dt);
ApplyPositionManagerDelta(rm.Body, positionDelta);
System.Numerics.Vector3 composedPosition = rm.Body.Position;
if (rm.CellId != 0
&& composedPosition != preComposePosition
&& _physicsEngine.LandblockCount > 0)
{
var (radius, height) = _getSetupCylinder(entity.ServerGuid, entity);
if (radius < 0.05f)
{
radius = 0.48f;
height = 1.835f;
}
bool previousContact = rm.Body.InContact;
bool previousOnWalkable = rm.Body.OnWalkable;
var resolved = _physicsEngine.ResolveWithTransition(
preComposePosition,
composedPosition,
rm.CellId,
radius,
height,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
isOnGround: previousOnWalkable,
body: rm.Body,
moverFlags: IsPlayerGuid(entity.ServerGuid)
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
movingEntityId: entity.Id);
rm.Body.Position = resolved.Position;
if (resolved.CellId != 0)
rm.CellId = resolved.CellId;
AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
rm.Body,
resolved.InContact,
resolved.OnWalkable,
resolved.CollisionNormalValid,
resolved.CollisionNormal,
previousContact,
previousOnWalkable,
rm.Movement.HitGround,
rm.Motion.LeaveGround);
rm.Airborne = !rm.Body.OnWalkable;
}
entity.SetPosition(rm.Body.Position);
if (rm.CellId != 0)
entity.ParentCellId = rm.CellId;
entity.Rotation = rm.Body.Orientation;
rm.Host?.HandleTargetting();
TickRemoteMoveTo(rm);
rm.Host?.PositionManager.UseTime();
}
/// <summary>
/// R4-V5 / R5-V2: the per-tick <see cref="AcDream.Core.Physics.Motion.MovementManager"/>
/// drive (retail <c>MovementManager::UseTime</c> 0x005242f0 — the moveto
@ -605,16 +729,36 @@ internal sealed class RemotePhysicsUpdater
/// Moved from GameWindow (#184 Slice 2a); called by the DR tick AND the NPC
/// UP-branch tail.
/// </summary>
public void SyncRemoteShadowToBody(uint entityId, RemoteMotion rm, int liveCenterX, int liveCenterY)
public void SyncRemoteShadowToBody(
uint entityId,
AcDream.App.World.ILiveEntityRemotePlacementRuntime rm,
int liveCenterX,
int liveCenterY,
uint? authoritativeCellId = null)
{
SyncRemoteShadowToBody(
entityId,
rm.Body,
liveCenterX,
liveCenterY,
authoritativeCellId ?? rm.CellId);
rm.LastShadowSyncPosition = rm.Body.Position;
}
public void SyncRemoteShadowToBody(
uint entityId,
AcDream.Core.Physics.PhysicsBody body,
int liveCenterX,
int liveCenterY,
uint authoritativeCellId)
{
ShadowPositionSynchronizer.Sync(
_physicsEngine.ShadowObjects,
entityId,
rm.Body.Position,
rm.Body.Orientation,
rm.CellId,
body.Position,
body.Orientation,
authoritativeCellId,
liveCenterX,
liveCenterY);
rm.LastShadowSyncPos = rm.Body.Position;
}
}