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:
parent
a51ebc66e9
commit
1e98d81448
46 changed files with 4883 additions and 127 deletions
|
|
@ -11,6 +11,8 @@ namespace AcDream.App.Physics;
|
|||
/// frame, from inside the same guard the body used to live under
|
||||
/// (<c>ae.Sequencer != null && serverGuid != 0 && serverGuid != _playerServerGuid
|
||||
/// && rm.LastServerPosTime > 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue