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

@ -52,6 +52,96 @@ public sealed class ProjectileControllerTests
Assert.Same(record.ProjectileRuntime!.Body, record.PhysicsBody);
}
[Fact]
public void Hidden_PausesProjectileWithoutClearingActiveOrReplayingClockBacklog()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x01010000u,
state: (uint)MissileState,
seedCellId: CellA);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
PhysicsBody body = record.PhysicsBody!;
Assert.True(body.IsActive);
record.FinalPhysicsState = MissileState
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
Vector3 hiddenPosition = entity.Position;
fixture.Controller.Tick(5.0, 1, 1, playerWorldPosition: null);
Assert.Equal(hiddenPosition, entity.Position);
Assert.True(body.InWorld);
Assert.True(body.IsActive);
Assert.Equal(5.0, body.LastUpdateTime, 8);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 5.0, 1, 1));
fixture.Controller.Tick(5.1, 1, 1, playerWorldPosition: null);
Assert.True(entity.Position.X > hiddenPosition.X);
Assert.True(entity.Position.X < hiddenPosition.X + 2f);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
}
[Fact]
public void HiddenSharedRemoteProjectile_UsesPositionManagerNarrowTickExactlyOnce()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.Null(record.AnimationRuntime);
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
record.FinalPhysicsState = MissileState
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
remote.Interp.Enqueue(
entity.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: remote.Body.Position);
var updater = new RemotePhysicsUpdater(
fixture.Engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
int publishedRoots = 0;
updater.TickHiddenEntities(
fixture.Live,
localPlayerServerGuid: 0x50000001u,
dt: 0.1f,
_ => publishedRoots++);
Vector3 afterNarrowTick = entity.Position;
fixture.Controller.Tick(1.2, 1, 1, playerWorldPosition: null);
Assert.True(afterNarrowTick.X > 10f);
Assert.Equal(afterNarrowTick, entity.Position);
Assert.Equal(1, publishedRoots);
Assert.True(fixture.Controller.HandlesMovement(Guid));
}
[Fact]
public void PendingDestinationAndLaterHydration_RetainIdentityAndProjectileOwner()
{