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

@ -251,7 +251,7 @@ internal sealed class ProjectileController
entity.MeshRefs.Count > 0);
_liveEntities.SetProjectileRuntime(record.ServerGuid, runtime);
if (HasVisibleCell(record))
if (HasVisibleCell(record) && !IsHidden(record))
{
ShadowPositionSynchronizer.Sync(
_shadows,
@ -262,6 +262,15 @@ internal sealed class ProjectileController
liveCenterX,
liveCenterY);
}
else if (HasVisibleCell(record))
{
// Hidden is a retained in-world CPhysicsObj, not leave_world.
// Keep Active/identity but remove collision rows and consume the
// hidden clock so UnHide cannot replay a time backlog.
body.InWorld = true;
body.LastUpdateTime = currentTime;
_shadows.Suspend(entity.Id);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
@ -300,7 +309,8 @@ internal sealed class ProjectileController
SetVelocity(runtime.Body, velocity, currentTime);
runtime.Body.Omega = angularVelocity;
if (!_liveEntities.ShouldAdvanceRootRuntime(serverGuid))
if (!_liveEntities.ShouldAdvanceRootRuntime(serverGuid)
&& !IsHidden(record))
Deactivate(runtime.Body);
return true;
}
@ -425,7 +435,7 @@ internal sealed class ProjectileController
entity.Rotation = orientation;
entity.ParentCellId = canonicalCellId;
_liveEntities.RebucketLiveEntity(serverGuid, canonicalCellId);
if (HasVisibleCell(record))
if (HasVisibleCell(record) && !IsHidden(record))
{
if (!wasInWorld)
Activate(body, currentTime);
@ -439,6 +449,12 @@ internal sealed class ProjectileController
liveCenterX,
liveCenterY);
}
else if (HasVisibleCell(record))
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_shadows.Suspend(entity.Id);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
@ -496,6 +512,27 @@ internal sealed class ProjectileController
|| record.WorldEntity is not { } entity)
continue;
runtime.Body.State = record.FinalPhysicsState;
if (IsHidden(record))
{
if (!HasVisibleCell(record))
{
SuspendOutsideWorld(runtime, entity.Id);
}
else
{
// UpdatePositionInternal skips root physics/PartArray
// while Hidden, but update_object still advances its
// clock and does not clear Active.
runtime.Body.InWorld = true;
runtime.Body.LastUpdateTime = currentTime;
if ((record.FinalPhysicsState & PhysicsStateFlags.Frozen) != 0)
Deactivate(runtime.Body);
_shadows.Suspend(entity.Id);
}
continue;
}
bool isMissile =
(record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0;
if (!isMissile)
@ -530,7 +567,6 @@ internal sealed class ProjectileController
continue;
}
runtime.Body.State = record.FinalPhysicsState;
if (!_liveEntities.ShouldAdvanceRootRuntime(record.ServerGuid))
{
if (!HasVisibleCell(record))
@ -637,6 +673,9 @@ internal sealed class ProjectileController
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
private static bool IsHidden(LiveEntityRecord record) =>
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
private void SuspendOutsideWorld(Runtime runtime, uint localEntityId)
{
runtime.Body.InWorld = false;