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
|
|
@ -29,6 +29,11 @@ public sealed class LiveEntityRuntimeTests
|
|||
|
||||
private sealed class EffectProfile : ILiveEntityEffectProfile { }
|
||||
|
||||
private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime
|
||||
{
|
||||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; private set; }
|
||||
|
|
@ -73,6 +78,8 @@ public sealed class LiveEntityRuntimeTests
|
|||
spawn.Position!.Value.LandblockId,
|
||||
id => Entity(id, spawn.Guid));
|
||||
runtime.SetAnimationRuntime(spawn.Guid, new AnimationRuntime(entity!));
|
||||
var visibilityEdges = new List<bool>();
|
||||
runtime.ProjectionVisibilityChanged += (_, visible) => visibilityEdges.Add(visible);
|
||||
|
||||
Assert.True(registered.LogicalRegistrationCreated);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
|
|
@ -90,6 +97,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Single(spatial.Entities);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _));
|
||||
Assert.Equal(new[] { false, true }, visibilityEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -296,6 +304,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.True(runtime.TryGetWorldEntity(spawn.Guid, out WorldEntity resolved));
|
||||
Assert.Same(attached, resolved);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
attached.IsAncestorDrawVisible = false;
|
||||
|
||||
WorldEntity same = runtime.MaterializeLiveEntity(
|
||||
spawn.Guid,
|
||||
|
|
@ -304,6 +313,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
LiveEntityProjectionKind.World)!;
|
||||
|
||||
Assert.Same(attached, same);
|
||||
Assert.True(same.IsAncestorDrawVisible);
|
||||
Assert.Same(attached, Assert.Single(runtime.WorldEntities).Value);
|
||||
Assert.True(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||
|
|
@ -500,6 +510,210 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder()
|
||||
{
|
||||
const uint stateBeforeBindGuid = 0x70000037u;
|
||||
const uint bindBeforeStateGuid = 0x70000038u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u));
|
||||
runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u));
|
||||
|
||||
PhysicsStateFlags firstState = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.Gravity
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
Assert.True(runtime.TryApplyState(
|
||||
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
|
||||
out _));
|
||||
var lateBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
|
||||
|
||||
var earlyBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
|
||||
PhysicsStateFlags secondState = PhysicsStateFlags.Static
|
||||
| PhysicsStateFlags.Ethereal
|
||||
| PhysicsStateFlags.NoDraw;
|
||||
Assert.True(runtime.TryApplyState(
|
||||
new SetState.Parsed(bindBeforeStateGuid, (uint)secondState, 1, 2),
|
||||
out _));
|
||||
|
||||
Assert.Equal((firstState & ~PhysicsStateFlags.ReportCollisions)
|
||||
| PhysicsStateFlags.IgnoreCollisions,
|
||||
lateBody.Body.State);
|
||||
Assert.Equal(secondState, earlyBody.Body.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentDrivenChildNoDrawKeepsBoundBodyInCanonicalState()
|
||||
{
|
||||
const uint guid = 0x70000039u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
var remote = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||
|
||||
Assert.True(runtime.SetAttachedChildNoDraw(guid, true));
|
||||
Assert.True(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw));
|
||||
|
||||
Assert.True(runtime.SetAttachedChildNoDraw(guid, false));
|
||||
Assert.False(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitiallyVisibleCreate_DoesNotManufactureUnHideTransition()
|
||||
{
|
||||
const uint guid = 0x70000040u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
Assert.True(record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition));
|
||||
Assert.Equal(RetailHiddenTransition.None, transition.HiddenTransition);
|
||||
Assert.Equal(PhysicsStateFlags.ReportCollisions, record.FinalPhysicsState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenCreate_SuppressesMeshInteractionButRetainsNarrowObjectTick()
|
||||
{
|
||||
const uint guid = 0x70000041u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(
|
||||
guid,
|
||||
1,
|
||||
1,
|
||||
0x01010001u,
|
||||
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
||||
Assert.Single(spatial.Entities);
|
||||
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
Assert.Equal(PhysicsStateFlags.None,
|
||||
record.FinalPhysicsState & PhysicsStateFlags.ReportCollisions);
|
||||
Assert.NotEqual(PhysicsStateFlags.None,
|
||||
record.FinalPhysicsState & PhysicsStateFlags.IgnoreCollisions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenThenVisibleState_RestoresSameMeshAndInteractionIdentity()
|
||||
{
|
||||
const uint guid = 0x70000042u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||
guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
|
||||
1,
|
||||
2), out _, out RetailPhysicsStateTransition hidden));
|
||||
Assert.Equal(RetailHiddenTransition.BecameHidden, hidden.HiddenTransition);
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.False(runtime.TryGetInteractionEligibleEntity(guid, out _));
|
||||
|
||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||
guid,
|
||||
0u,
|
||||
1,
|
||||
3), out _, out RetailPhysicsStateTransition visible));
|
||||
Assert.Equal(RetailHiddenTransition.BecameVisible, visible.HiddenTransition);
|
||||
Assert.True(entity.IsDrawVisible);
|
||||
Assert.True(runtime.TryGetInteractionEligibleEntity(guid, out var eligible));
|
||||
Assert.Same(entity, eligible);
|
||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
||||
Assert.Same(entity, Assert.Single(spatial.Entities));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp()
|
||||
{
|
||||
const uint guid = 0x70000043u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
Assert.True(runtime.TryApplyPickup(
|
||||
new PickupEvent.Parsed(guid, 1, 2),
|
||||
out _));
|
||||
|
||||
var update = new WorldSession.EntityPositionUpdate(
|
||||
guid,
|
||||
new CreateObject.ServerPosition(
|
||||
0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
0);
|
||||
|
||||
Assert.True(runtime.TryApplyPosition(
|
||||
update,
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out AcceptedPhysicsTimestamps timestamps));
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||
Assert.False(timestamps.TeleportAdvanced);
|
||||
Assert.True(timestamps.TeleportHookRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionFromPendingProjection_RequiresTeleportHookWithEqualTeleportStamp()
|
||||
{
|
||||
const uint guid = 0x70000044u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid));
|
||||
spatial.RemoveLandblock(0x0101FFFFu);
|
||||
|
||||
Assert.True(runtime.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
guid,
|
||||
new CreateObject.ServerPosition(
|
||||
0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0),
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out AcceptedPhysicsTimestamps timestamps));
|
||||
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||
Assert.False(timestamps.TeleportAdvanced);
|
||||
Assert.True(timestamps.TeleportHookRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandblockUnload_HidesTopLevelViewAndReloadRestoresSameIdentity()
|
||||
{
|
||||
|
|
@ -792,14 +1006,15 @@ public sealed class LiveEntityRuntimeTests
|
|||
uint guid,
|
||||
ushort instance,
|
||||
ushort positionSequence,
|
||||
uint cell)
|
||||
uint cell,
|
||||
PhysicsStateFlags state = PhysicsStateFlags.ReportCollisions)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
positionSequence, 1, 1, 1, 0, 1, 0, 1, instance);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
RawState: (uint)state,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
|
|
@ -832,7 +1047,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
PhysicsState: (uint)state,
|
||||
InstanceSequence: instance,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue