using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Physics; public sealed class RemoteTeleportControllerTests { private sealed class Resources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } private sealed class BodyOnlyRemote(PhysicsBody body) : ILiveEntityRemoteMotionRuntime { public PhysicsBody Body { get; } = body; } private sealed class MutablePlacementRemote(PhysicsBody body) : ILiveEntityRemotePlacementRuntime { private uint _cellId; private Func? _readCell; private Action? _writeCell; public PhysicsBody Body { get; set; } = body; public uint CellId { get => _readCell?.Invoke() ?? _cellId; set { _cellId = value; _writeCell?.Invoke(value); } } public bool Airborne { get; set; } public Vector3 LastServerPosition { get; set; } public double LastServerPositionTime { get; set; } public Vector3 LastShadowSyncPosition { get; set; } public void BindCanonicalCell(Func read, Action write) { _readCell = read; _writeCell = write; } public void HitGround() { } public void LeaveGround() { } } private sealed class AlternatingBodyRemote( PhysicsBody first, PhysicsBody later) : ILiveEntityRemotePlacementRuntime { private int _reads; public PhysicsBody Body => _reads++ == 0 ? first : later; public uint CellId { get; set; } public bool Airborne { get; set; } public Vector3 LastServerPosition { get; set; } public double LastServerPositionTime { get; set; } public Vector3 LastShadowSyncPosition { get; set; } public void BindCanonicalCell(Func read, Action write) { } public void HitGround() { } public void LeaveGround() { } } private sealed class AlternatingProjectileRuntime( PhysicsBody first, PhysicsBody later) : ILiveEntityProjectileRuntime { private int _reads; public PhysicsBody Body => _reads++ == 0 ? first : later; } [Fact] public void PendingDestination_ParksAuthoritativeFrameThenResolvesContactOnLoad() { const uint guid = 0x70000001u; const uint sourceCell = 0xA9B40039u; const uint destinationCell = 0xAAB40001u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); var live = new LiveEntityRuntime(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, sourceCell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(191f, 10f, 50f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var remote = new GameWindow.RemoteMotion(); remote.Body.SnapToCell( sourceCell, entity.Position, entity.Position); remote.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; live.SetRemoteMotionRuntime(guid, remote); var engine = BuildEngine(includeDestination: false); engine.ShadowObjects.Register( entity.Id, entity.SourceGfxObjOrSetupId, entity.Position, entity.Rotation, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0xA9B4FFFFu, collisionType: ShadowCollisionType.Cylinder, cylHeight: 1.835f, seedCellId: sourceCell, isStatic: false); Assert.True(engine.ShadowObjects.Suspend(entity.Id)); int shadowSyncs = 0; using var controller = new RemoteTeleportController( engine, live, (_, _) => (0.48f, 1.835f), CellLocal, (resolvedEntity, resolvedRemote, resolvedCell) => { shadowSyncs++; engine.ShadowObjects.Register( resolvedEntity.Id, resolvedEntity.SourceGfxObjOrSetupId, resolvedRemote.Position, resolvedRemote.Orientation, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0xAAB4FFFFu, collisionType: ShadowCollisionType.Cylinder, cylHeight: 1.835f, seedCellId: resolvedCell, isStatic: false); }, (_, _, _) => { }, (_, _) => { }); Assert.True(live.RebucketLiveEntity(guid, destinationCell)); var result = controller.TryApply( remote, entity, new Vector3(193f, 10f, 50f), destinationCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 5.0, destinationProjectionVisible: false, generation: 1, positionSequence: 1); Assert.True(result.Applied); Assert.False(result.ContactResolved); Assert.Equal(destinationCell, remote.CellId); Assert.Equal(new Vector3(193f, 10f, 50f), entity.Position); Assert.False(remote.Body.InContact); Assert.Equal(0, engine.ShadowObjects.TotalRegistered); Assert.True(live.TryGetRecord(guid, out LiveEntityRecord pending)); Assert.False(pending.IsSpatiallyVisible); Assert.True(live.TryApplyPosition( new WorldSession.EntityPositionUpdate( guid, new CreateObject.ServerPosition( destinationCell, 2f, 10f, 50f, 1f, 0f, 0f, 0f), null, null, true, 1, 2, 0, 0), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); var latest = controller.TryApply( remote, entity, new Vector3(194f, 10f, 50f), destinationCell, new Vector3(2f, 10f, 50f), Quaternion.Identity, gameTime: 6.0, destinationProjectionVisible: false, generation: 1, positionSequence: 2); Assert.True(latest.Applied); Assert.False(latest.ContactResolved); AddDestination(engine); spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu)); Assert.True(pending.IsSpatiallyVisible); Assert.Equal(destinationCell, pending.FullCellId); Assert.True(remote.Body.InContact); Assert.True(remote.Body.OnWalkable); Assert.False(remote.Airborne); Assert.Equal(194f, entity.Position.X, 3); Assert.Equal(remote.Body.Position, entity.Position); Assert.Equal(1, shadowSyncs); Assert.Equal(1, engine.ShadowObjects.TotalRegistered); ShadowEntry restored = Assert.Single( engine.ShadowObjects.AllEntriesForDebug()); Assert.Equal(entity.Position, restored.Position); } [Fact] public void LoadedPlacementFailure_RestoresSuspendedShadowAtRetainedPose() { const uint guid = 0x70000002u; const uint sourceCell = 0xA9B40039u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); var live = new LiveEntityRuntime(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, sourceCell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 50f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var remote = new GameWindow.RemoteMotion(); remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position); live.SetRemoteMotionRuntime(guid, remote); var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.ShadowObjects.Register( entity.Id, entity.SourceGfxObjOrSetupId, entity.Position, entity.Rotation, 0.48f, 0f, 0f, 0xA9B4FFFFu, ShadowCollisionType.Cylinder, 1.835f, seedCellId: sourceCell, isStatic: false); Assert.True(engine.ShadowObjects.Suspend(entity.Id)); int shadowSyncs = 0; using var controller = new RemoteTeleportController( engine, live, (_, _) => (0.48f, 1.835f), CellLocal, (resolvedEntity, resolvedRemote, resolvedCell) => { shadowSyncs++; engine.ShadowObjects.Register( resolvedEntity.Id, resolvedEntity.SourceGfxObjOrSetupId, resolvedRemote.Position, resolvedRemote.Orientation, 0.48f, 0f, 0f, 0xA9B4FFFFu, ShadowCollisionType.Cylinder, 1.835f, seedCellId: resolvedCell, isStatic: false); }, (_, _, _) => { }, (_, _) => { }, resolvePlacement: (position, cell, _, _, _, _) => new ResolveResult( position, cell, IsOnGround: false, Ok: false)); RemoteTeleportController.Result result = controller.TryApply( remote, entity, new Vector3(20f, 20f, 50f), requestedCellId: 0u, requestedCellLocalPosition: Vector3.Zero, requestedOrientation: Quaternion.Identity, gameTime: 2.0, destinationProjectionVisible: true, generation: 1, positionSequence: 1); Assert.False(result.Applied); Assert.Equal(1, shadowSyncs); Assert.Equal(1, engine.ShadowObjects.TotalRegistered); Assert.Equal(new Vector3(10f, 10f, 50f), entity.Position); } [Fact] public void DeferredPlacementFailure_RollsBackProjectionBodyAndShadow() { const uint guid = 0x70000003u; const uint sourceCell = 0xA9B40039u; const uint destinationCell = 0xAAB40001u; Vector3 sourcePosition = new(191f, 10f, 50f); var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); var live = new LiveEntityRuntime(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, sourceCell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = sourcePosition, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var remote = new GameWindow.RemoteMotion(); remote.Body.SnapToCell(sourceCell, sourcePosition, sourcePosition); remote.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; remote.CellId = sourceCell; live.SetRemoteMotionRuntime(guid, remote); var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.ShadowObjects.Register( entity.Id, entity.SourceGfxObjOrSetupId, sourcePosition, Quaternion.Identity, 0.48f, 0f, 0f, 0xA9B4FFFFu, ShadowCollisionType.Cylinder, 1.835f, seedCellId: sourceCell, isStatic: false); Assert.True(engine.ShadowObjects.Suspend(entity.Id)); int shadowSyncs = 0; using var controller = new RemoteTeleportController( engine, live, (_, _) => (0.48f, 1.835f), CellLocal, (resolvedEntity, resolvedRemote, resolvedCell) => { shadowSyncs++; engine.ShadowObjects.Register( resolvedEntity.Id, resolvedEntity.SourceGfxObjOrSetupId, resolvedRemote.Position, resolvedRemote.Orientation, 0.48f, 0f, 0f, 0xA9B4FFFFu, ShadowCollisionType.Cylinder, 1.835f, seedCellId: resolvedCell, isStatic: false); }, (_, _, _) => { }, (_, _) => { }, resolvePlacement: (position, cell, _, _, _, _) => new ResolveResult( position, cell, IsOnGround: false, Ok: false)); Assert.True(live.RebucketLiveEntity(guid, destinationCell)); RemoteTeleportController.Result parked = controller.TryApply( remote, entity, new Vector3(193f, 10f, 50f), destinationCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 5.0, destinationProjectionVisible: false, generation: 1, positionSequence: 1); Assert.True(parked.Applied); Assert.True(controller.HasPending(guid)); // Rendering becomes resident without matching physics collision data. // The visibility edge is the one resolution attempt; failure must // restore the retained source instead of leaving a visible ghost. spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu)); Assert.False(controller.HasPending(guid)); Assert.Equal(sourcePosition, remote.Body.Position); Assert.Equal(sourceCell, remote.CellId); Assert.Equal(sourcePosition, entity.Position); Assert.Equal(sourceCell, entity.ParentCellId); Assert.True(remote.Body.InContact); Assert.True(remote.Body.OnWalkable); Assert.True(live.TryGetRecord(guid, out LiveEntityRecord restored)); Assert.Equal(sourceCell, restored.FullCellId); Assert.True(restored.IsSpatiallyVisible); Assert.Equal(1, shadowSyncs); Assert.Equal(1, engine.ShadowObjects.TotalRegistered); } [Fact] public void DeferredFailure_WithUnloadedSource_RestoresShadowWhenSourceReloads() { using var fixture = new RollbackFixture(); Assert.True(fixture.ParkPending().Applied); fixture.VisibilityEdges.Clear(); fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord pendingSource)); Assert.Equal(RollbackFixture.SourceCell, pendingSource.FullCellId); Assert.False(pendingSource.IsSpatiallyVisible); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.False(fixture.PresentationVisible); Assert.Equal(new[] { true, false }, fixture.VisibilityEdges); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock)); Assert.True(pendingSource.IsSpatiallyVisible); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.True(fixture.PresentationVisible); Assert.Equal(new[] { true, false, true }, fixture.VisibilityEdges); } [Fact] public void DeferredFailure_WithCelllessSource_WithdrawsPresentationWithoutShadowRestore() { using var fixture = new RollbackFixture(celllessSource: true); Assert.True(fixture.ParkPending().Applied); Assert.False(fixture.PresentationVisible); fixture.VisibilityEdges.Clear(); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord withdrawn)); Assert.Equal(0u, withdrawn.FullCellId); Assert.False(withdrawn.IsSpatiallyProjected); Assert.False(withdrawn.IsSpatiallyVisible); Assert.False(fixture.PresentationVisible); Assert.Equal(0, fixture.ShadowSyncs); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(new[] { true, false }, fixture.VisibilityEdges); } [Fact] public void DeferredFailure_WithLoadedSource_PublishesNoFalseRebucketPulse() { using var fixture = new RollbackFixture(); Assert.True(fixture.ParkPending().Applied); fixture.VisibilityEdges.Clear(); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.Equal(new[] { true }, fixture.VisibilityEdges); Assert.True(fixture.PresentationVisible); Assert.Equal(RollbackFixture.SourcePosition, fixture.Entity.Position); Assert.Equal(RollbackFixture.SourceCell, fixture.Entity.ParentCellId); Assert.Equal(1, fixture.ShadowSyncs); } [Fact] public void NewerLoadedFailure_ReplacesPendingAndRestoresOriginalSource() { const uint newerCell = 0xABB40001u; const uint newerLandblock = 0xABB4FFFFu; using var fixture = new RollbackFixture(); fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); Assert.True(fixture.ParkPending().Applied); Assert.True(fixture.Live.TryApplyPosition( new WorldSession.EntityPositionUpdate( RollbackFixture.Guid, new CreateObject.ServerPosition( newerCell, 1f, 10f, 50f, 1f, 0f, 0f, 0f), null, null, true, 1, 2, 0, 0), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); fixture.VisibilityEdges.Clear(); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.True(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.Equal(new[] { true }, fixture.VisibilityEdges); RemoteTeleportController.Result failed = fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(385f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 6.0, destinationProjectionVisible: true, generation: 1, positionSequence: 2); Assert.False(failed.Applied); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.Equal(RollbackFixture.SourcePosition, fixture.Remote.Body.Position); Assert.Equal(RollbackFixture.SourceCell, fixture.Remote.CellId); Assert.True(fixture.Remote.Body.InContact); Assert.True(fixture.Remote.Body.OnWalkable); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord restored)); Assert.Equal(RollbackFixture.SourceCell, restored.FullCellId); Assert.True(restored.IsSpatiallyVisible); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(new[] { true }, fixture.VisibilityEdges); } [Fact] public void HiddenThenUnhideWhileRollbackSourcePending_RestoresShadowExactlyOnce() { using var fixture = new RollbackFixture(); Assert.True(fixture.ParkPending().Applied); fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.TryApplyState( new SetState.Parsed( RollbackFixture.Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, 2), out _, out _)); Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid)); Assert.True(fixture.Live.TryApplyState( new SetState.Parsed( RollbackFixture.Guid, (uint)PhysicsStateFlags.ReportCollisions, 1, 3), out _, out _)); Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid)); Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); } [Theory] [InlineData(true)] [InlineData(false)] public void DeferredRollback_NewerLoadedPlacementTransfersShadowUntilResolution( bool resolutionSucceeds) { const uint newerCell = 0xABB40001u; const uint newerLandblock = 0xABB4FFFFu; using var fixture = new RollbackFixture(); CreateUnloadedSourceRollback(fixture); fixture.ResolveSucceeds = resolutionSucceeds; fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); AcceptNewerPosition(fixture, newerCell); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); fixture.VisibilityEdges.Clear(); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); RemoteTeleportController.Result result = fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(385f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 7.0, destinationProjectionVisible: true, generation: 1, positionSequence: 2); Assert.Equal(resolutionSucceeds, result.Applied); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(!resolutionSucceeds, fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record)); Assert.Equal( resolutionSucceeds ? newerCell : RollbackFixture.SourceCell, record.FullCellId); Assert.Equal(resolutionSucceeds, record.IsSpatiallyVisible); } [Theory] [InlineData(true)] [InlineData(false)] public void DeferredRollback_NewerPendingHydrationTransfersShadowUntilResolution( bool resolutionSucceeds) { const uint newerCell = 0xACB40001u; const uint newerLandblock = 0xACB4FFFFu; using var fixture = new RollbackFixture(); CreateUnloadedSourceRollback(fixture); fixture.ResolveSucceeds = resolutionSucceeds; AcceptNewerPosition(fixture, newerCell); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.True(fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(577f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 7.0, destinationProjectionVisible: false, generation: 1, positionSequence: 2).Applied); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(!resolutionSucceeds, fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record)); Assert.Equal( resolutionSucceeds ? newerCell : RollbackFixture.SourceCell, record.FullCellId); Assert.Equal(resolutionSucceeds, record.IsSpatiallyVisible); } [Theory] [InlineData(true)] [InlineData(false)] public void HiddenPlacement_UnhideBeforeHydrationWaitsForResolution( bool resolutionSucceeds) { const uint newerCell = 0xACB40001u; const uint newerLandblock = 0xACB4FFFFu; using var fixture = new RollbackFixture(); CreateUnloadedSourceRollback(fixture); fixture.ResolveSucceeds = resolutionSucceeds; ApplyHidden(fixture, stateSequence: 2); AcceptNewerPosition(fixture, newerCell); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.True(fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(577f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 7.0, destinationProjectionVisible: false, generation: 1, positionSequence: 2).Applied); ApplyVisible(fixture, stateSequence: 3); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(!resolutionSucceeds, fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); } [Theory] [InlineData(true)] [InlineData(false)] public void PendingPlacement_HideUnhideOscillationCannotRestoreBeforeResolution( bool resolutionSucceeds) { const uint newerCell = 0xADB40001u; const uint newerLandblock = 0xADB4FFFFu; using var fixture = new RollbackFixture(); CreateUnloadedSourceRollback(fixture); fixture.ResolveSucceeds = resolutionSucceeds; AcceptNewerPosition(fixture, newerCell); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.True(fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(769f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 7.0, destinationProjectionVisible: false, generation: 1, positionSequence: 2).Applied); ApplyHidden(fixture, stateSequence: 2); ApplyVisible(fixture, stateSequence: 3); ApplyHidden(fixture, stateSequence: 4); ApplyVisible(fixture, stateSequence: 5); Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs); Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(!resolutionSucceeds, fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); } [Theory] [InlineData(true)] [InlineData(false)] public void PendingPlacement_SameBodyRuntimeRebindFinishesStableOwnership( bool resolutionSucceeds) { using var fixture = new RollbackFixture(); fixture.ResolveSucceeds = resolutionSucceeds; Assert.True(fixture.ParkPending().Applied); Assert.True(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, new GameWindow.RemoteMotion())); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, new BodyOnlyRemote(fixture.Remote.Body))); var replacement = new GameWindow.RemoteMotion(fixture.Remote.Body); fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, replacement); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable)); Assert.Same(replacement, stable.RemoteMotionRuntime); Assert.Equal( resolutionSucceeds ? RollbackFixture.DestinationCell : RollbackFixture.SourceCell, stable.FullCellId); Assert.Equal(stable.FullCellId, replacement.CellId); } [Fact] public void PendingPlacement_RuntimeClearRollsBackAndCompletesOwnership() { using var fixture = new RollbackFixture(); Assert.True(fixture.ParkPending().Applied); Assert.True(fixture.Live.ClearRemoteMotionRuntime(RollbackFixture.Guid)); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord retained)); Assert.Same(fixture.Remote.Body, retained.PhysicsBody); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, new GameWindow.RemoteMotion())); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, new BodyOnlyRemote(fixture.Remote.Body))); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable)); Assert.Equal(RollbackFixture.SourceCell, stable.FullCellId); Assert.Null(stable.RemoteMotionRuntime); } [Fact] public void PendingPlacement_MutatedWrapperBodyIsDetachedAndCanonicalBodyRollsBack() { using var fixture = new RollbackFixture(); Assert.True(fixture.ParkPending().Applied); PhysicsBody canonicalBody = fixture.Remote.Body; var mutable = new MutablePlacementRemote(canonicalBody); fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, mutable); mutable.Body = new PhysicsBody(); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid)); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable)); Assert.Same(canonicalBody, stable.PhysicsBody); Assert.Null(stable.RemoteMotionRuntime); Assert.Equal(RollbackFixture.SourcePosition, canonicalBody.Position); Assert.Equal(RollbackFixture.SourcePosition, fixture.Entity.Position); Assert.Equal(RollbackFixture.SourceCell, stable.FullCellId); } [Fact] public void RuntimeBinding_SnapshotsDynamicBodyGetterOncePerBoundary() { using var fixture = new RollbackFixture(); PhysicsBody canonicalBody = fixture.Remote.Body; var otherBody = new PhysicsBody(); var remote = new AlternatingBodyRemote(canonicalBody, otherBody); fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, remote); Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record)); Assert.Same(canonicalBody, record.PhysicsBody); Assert.Same(remote, record.RemoteMotionRuntime); var projectile = new AlternatingProjectileRuntime(canonicalBody, otherBody); fixture.Live.SetProjectileRuntime(RollbackFixture.Guid, projectile); Assert.Same(canonicalBody, record.PhysicsBody); Assert.Same(projectile, record.ProjectileRuntime); } [Theory] [InlineData(true)] [InlineData(false)] public void HiddenPlacement_HydrationHandsRestoreBackToUnhide( bool resolutionSucceeds) { const uint newerCell = 0xACB40001u; const uint newerLandblock = 0xACB4FFFFu; using var fixture = new RollbackFixture(); CreateUnloadedSourceRollback(fixture); fixture.ResolveSucceeds = resolutionSucceeds; ApplyHidden(fixture, stateSequence: 2); AcceptNewerPosition(fixture, newerCell); fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1); Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell)); Assert.True(fixture.Controller.TryApply( fixture.Remote, fixture.Entity, new Vector3(577f, 10f, 50f), newerCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 7.0, destinationProjectionVisible: false, generation: 1, positionSequence: 2).Applied); fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock)); Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); ApplyVisible(fixture, stateSequence: 3); if (!resolutionSucceeds) { Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock)); } Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(1, fixture.ShadowSyncs); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); } [Fact] public void TeardownCallbackFailure_StillForgetsPendingBeforeGuidReuse() { const uint guid = 0x70000004u; const uint sourceCell = 0xA9B40039u; const uint destinationCell = 0xAAB40001u; RemoteTeleportController? controller = null; bool cleanupAfterFailureRan = false; var resources = new DelegateLiveEntityResourceLifecycle( _ => { }, _ => LiveEntityTeardown.Run( [ () => throw new InvalidOperationException("effect teardown failed"), () => controller!.Forget(guid), () => cleanupAfterFailureRan = true, ])); var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); var live = new LiveEntityRuntime(spatial, resources); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, sourceCell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(191f, 10f, 50f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var remote = new GameWindow.RemoteMotion(); remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position); live.SetRemoteMotionRuntime(guid, remote); controller = new RemoteTeleportController( BuildEngine(includeDestination: false), live, (_, _) => (0.48f, 1.835f), CellLocal, (_, _, _) => { }, (_, _, _) => { }, (_, _) => { }); Assert.True(live.RebucketLiveEntity(guid, destinationCell)); Assert.True(controller.TryApply( remote, entity, new Vector3(193f, 10f, 50f), destinationCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 5.0, destinationProjectionVisible: false, generation: 1, positionSequence: 1).Applied); Assert.True(controller.HasPending(guid)); Assert.Throws(() => live.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); Assert.True(cleanupAfterFailureRan); Assert.False(controller.HasPending(guid)); Assert.False(live.TryGetRecord(guid, out _)); LiveEntityRegistrationResult replacement = live.RegisterLiveEntity(Spawn(guid, sourceCell)); Assert.True(replacement.LogicalRegistrationCreated); Assert.False(controller.HasPending(guid)); controller.Dispose(); } private static PhysicsEngine BuildEngine(bool includeDestination) { var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( 0xA9B4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 0f, 0f); if (includeDestination) AddDestination(engine); return engine; } private static void AddDestination(PhysicsEngine engine) => engine.AddLandblock( 0xAAB4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 192f, 0f); private static TerrainSurface FlatTerrain() { var heights = new byte[81]; var table = new float[256]; Array.Fill(table, 50f); return new TerrainSurface(heights, table); } private static Vector3 CellLocal(Vector3 world, uint cell) { int lbX = (int)(cell >> 24); return world - new Vector3((lbX - 0xA9) * 192f, 0f, 0f); } private static LoadedLandblock EmptyLandblock(uint id) => new(id, new LandBlock(), Array.Empty()); private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) { var position = new CreateObject.ServerPosition( cell, 191f, 10f, 50f, 1f, 0f, 0f, 0f); var physics = new PhysicsSpawnData( (uint)(PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions), position, null, null, 0x02000001u, 0x09000001u, null, null, null, null, null, null, null, null, null, null, null, null, null, new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1)); return new WorldSession.EntitySpawn( Guid: guid, Position: position, SetupTableId: 0x02000001u, AnimPartChanges: Array.Empty(), TextureChanges: Array.Empty(), SubPalettes: Array.Empty(), BasePaletteId: null, ObjScale: 1f, Name: "remote", ItemType: null, MotionState: null, MotionTableId: 0x09000001u, PhysicsState: physics.RawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private static void CreateUnloadedSourceRollback(RollbackFixture fixture) { Assert.True(fixture.ParkPending().Applied); fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid)); Assert.Equal(0, fixture.ShadowSyncs); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); } private static void AcceptNewerPosition(RollbackFixture fixture, uint cellId) { Assert.True(fixture.Live.TryApplyPosition( new WorldSession.EntityPositionUpdate( RollbackFixture.Guid, new CreateObject.ServerPosition( cellId, 1f, 10f, 50f, 1f, 0f, 0f, 0f), null, null, true, 1, 2, 0, 0), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); } private static void ApplyHidden(RollbackFixture fixture, ushort stateSequence) { Assert.True(fixture.Live.TryApplyState( new SetState.Parsed( RollbackFixture.Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, stateSequence), out _, out _)); Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid)); } private static void ApplyVisible(RollbackFixture fixture, ushort stateSequence) { Assert.True(fixture.Live.TryApplyState( new SetState.Parsed( RollbackFixture.Guid, (uint)PhysicsStateFlags.ReportCollisions, 1, stateSequence), out _, out _)); Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid)); } private sealed class RollbackFixture : IDisposable { internal const uint Guid = 0x70000010u; internal const uint SourceCell = 0xA9B40039u; internal const uint SourceLandblock = 0xA9B4FFFFu; internal const uint DestinationCell = 0xAAB40001u; internal const uint DestinationLandblock = 0xAAB4FFFFu; internal static readonly Vector3 SourcePosition = new(191f, 10f, 50f); internal RollbackFixture(bool celllessSource = false) { Spatial.AddLandblock(EmptyLandblock(SourceLandblock)); Live = new LiveEntityRuntime(Spatial, new Resources()); Live.RegisterLiveEntity(Spawn(Guid, SourceCell)); Entity = Live.MaterializeLiveEntity( Guid, SourceCell, id => new WorldEntity { Id = id, ServerGuid = Guid, SourceGfxObjOrSetupId = 0x02000001u, Position = SourcePosition, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; Remote = new GameWindow.RemoteMotion(); Remote.Body.SnapToCell( celllessSource ? 0u : SourceCell, SourcePosition, SourcePosition); Remote.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; Remote.CellId = celllessSource ? 0u : SourceCell; Live.SetRemoteMotionRuntime(Guid, Remote); Engine.ShadowObjects.Register( Entity.Id, Entity.SourceGfxObjOrSetupId, SourcePosition, Quaternion.Identity, 0.48f, 0f, 0f, SourceLandblock, ShadowCollisionType.Cylinder, 1.835f, seedCellId: SourceCell, isStatic: false); Assert.True(Engine.ShadowObjects.Suspend(Entity.Id)); Live.ProjectionVisibilityChanged += OnVisibilityChanged; Presentation = new LiveEntityPresentationController( Live, Engine.ShadowObjects, (_, _, _) => true, liveCenter: () => (0xA9, 0xB4), onShadowRestored: _ => ShadowSyncs++); Assert.True(Presentation.OnLiveEntityReady(Guid)); Controller = new RemoteTeleportController( Engine, Live, (_, _) => (0.48f, 1.835f), CellLocal, (resolvedEntity, resolvedRemote, resolvedCell) => { ShadowSyncs++; uint landblock = (resolvedCell & 0xFFFF0000u) | 0xFFFFu; Engine.ShadowObjects.Register( resolvedEntity.Id, resolvedEntity.SourceGfxObjOrSetupId, resolvedRemote.Position, resolvedRemote.Orientation, 0.48f, 0f, 0f, landblock, ShadowCollisionType.Cylinder, 1.835f, seedCellId: resolvedCell, isStatic: false); }, (guid, generation, deferShadowRestore) => Presentation.CompleteAuthoritativePlacement( guid, generation, deferShadowRestore), (guid, generation) => Presentation.BeginAuthoritativePlacement(guid, generation), resolvePlacement: (position, cell, _, _, _, _) => ResolveSucceeds ? new ResolveResult( position, cell, IsOnGround: true, InContact: true, OnWalkable: true, ContactPlane: new Plane(Vector3.UnitZ, 0f), ContactPlaneCellId: cell) : new ResolveResult(position, cell, IsOnGround: false, Ok: false)); if (celllessSource) Assert.True(Live.WithdrawLiveEntityProjection(Guid)); } internal GpuWorldState Spatial { get; } = new(); internal LiveEntityRuntime Live { get; } internal WorldEntity Entity { get; } internal GameWindow.RemoteMotion Remote { get; } internal PhysicsEngine Engine { get; } = new() { DataCache = new PhysicsDataCache() }; internal RemoteTeleportController Controller { get; } internal LiveEntityPresentationController Presentation { get; } internal int ShadowSyncs { get; private set; } internal bool PresentationVisible { get; private set; } = true; internal List VisibilityEdges { get; } = []; internal bool ResolveSucceeds { get; set; } internal RemoteTeleportController.Result ParkPending() { Controller.BeginPlacement(Guid, generation: 1); Assert.True(Live.RebucketLiveEntity(Guid, DestinationCell)); return Controller.TryApply( Remote, Entity, new Vector3(193f, 10f, 50f), DestinationCell, new Vector3(1f, 10f, 50f), Quaternion.Identity, gameTime: 5.0, destinationProjectionVisible: false, generation: 1, positionSequence: 1); } public void Dispose() { Live.ProjectionVisibilityChanged -= OnVisibilityChanged; Controller.Dispose(); Presentation.Dispose(); } private void OnVisibilityChanged(LiveEntityRecord _, bool visible) { PresentationVisible = visible; VisibilityEdges.Add(visible); } } }