using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using AcDream.Core.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Physics; public sealed class RemotePhysicsUpdaterTests { [Fact] public void ShadowPoseGate_TracksTranslationAndSignInvariantOrientation() { Quaternion turn = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, MathF.PI / 2f); Assert.False(RemotePhysicsUpdater.ShouldSynchronizeShadowPose( Vector3.Zero, Quaternion.Identity, Vector3.Zero, Quaternion.Identity)); Assert.False(RemotePhysicsUpdater.ShouldSynchronizeShadowPose( Vector3.Zero, turn, Vector3.Zero, new Quaternion(-turn.X, -turn.Y, -turn.Z, -turn.W))); Assert.True(RemotePhysicsUpdater.ShouldSynchronizeShadowPose( new Vector3(0.02f, 0f, 0f), Quaternion.Identity, Vector3.Zero, Quaternion.Identity)); Assert.True(RemotePhysicsUpdater.ShouldSynchronizeShadowPose( Vector3.Zero, turn, Vector3.Zero, Quaternion.Identity)); Assert.True(RemotePhysicsUpdater.ShouldSynchronizeShadow( cellChanged: true, Vector3.Zero, Quaternion.Identity, Vector3.Zero, Quaternion.Identity)); } [Fact] public void Tick_InPlaceCompleteRootTurn_UpdatesOffsetCollisionShadow() { const uint cellId = 0x01010001u; Quaternion turn = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, MathF.PI / 2f); var engine = new PhysicsEngine(); var entity = new WorldEntity { Id = 13u, ServerGuid = 0x80000013u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = cellId, }; var motion = new GameWindow.RemoteMotion { LastShadowSyncPos = Vector3.Zero, LastShadowSyncOrientation = Quaternion.Identity, }; motion.Body.Position = Vector3.Zero; motion.Body.Orientation = Quaternion.Identity; motion.CellId = cellId; engine.ShadowObjects.RegisterMultiPart( entity.Id, entity.Position, entity.Rotation, [ new ShadowShape( 0x01000001u, new Vector3(1f, 0f, 0f), Quaternion.Identity, Scale: 1f, CollisionType: ShadowCollisionType.Cylinder, Radius: 0.25f, CylHeight: 1f), ], state: (uint)PhysicsStateFlags.ReportCollisions, flags: EntityCollisionFlags.None, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0x01010000u, seedCellId: cellId); var updater = new RemotePhysicsUpdater( engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); updater.Tick( motion, entity, objectScale: 1f, sequencer: null, animationForVelocityCycle: null, dt: 0.1f, new MotionDeltaFrame { Orientation = turn }, liveCenterX: 1, liveCenterY: 1); ShadowEntry entry = Assert.Single( engine.ShadowObjects.AllEntriesForDebug(), candidate => candidate.EntityId == entity.Id); Assert.Equal(0f, entry.Position.X, 3); Assert.Equal(1f, entry.Position.Y, 3); Assert.InRange( MathF.Abs(Quaternion.Dot( turn, Quaternion.Normalize(motion.LastShadowSyncOrientation))), 0.99999f, 1.00001f); } [Fact] public void Tick_ComposesCompleteSequenceFrameWithoutOmegaSideChannel() { var entity = new WorldEntity { Id = 10u, ServerGuid = 0x80000010u, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 20f, 30f), Rotation = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, -MathF.PI / 2f), MeshRefs = Array.Empty(), }; var animated = new GameWindow.AnimatedEntity { Entity = entity, Setup = new Setup(), Animation = new Animation(), LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = 1f, PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; var motion = new GameWindow.RemoteMotion(); motion.Body.Position = entity.Position; motion.Body.Orientation = entity.Rotation; motion.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable | TransientStateFlags.Active; var sequenceFrame = new MotionDeltaFrame { Origin = new Vector3(0f, 0.1f, 0f), Orientation = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, MathF.PI / 2f), }; var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); updater.Tick( motion, animated, dt: 0.1f, sequenceFrame, liveCenterX: 0, liveCenterY: 0); // Current body faces east, so the sequence's local +Y origin moves // east before its quarter-turn is composed. No ObservedOmega/manual // integrator participates. Assert.Equal(10.1f, motion.Body.Position.X, 3); Assert.Equal(20f, motion.Body.Position.Y, 3); Assert.InRange( MathF.Abs(Quaternion.Dot( Quaternion.Identity, Quaternion.Normalize(motion.Body.Orientation))), 0.99999f, 1.00001f); Assert.Equal(motion.Body.Orientation, entity.Rotation); } [Fact] public void Tick_HostlessAirborneRemote_SuppressesOriginButPreservesRootOrientation() { Quaternion initial = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1.1f); Quaternion rootTurn = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f); var entity = new WorldEntity { Id = 11u, ServerGuid = 0x80000011u, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 20f, 30f), Rotation = initial, MeshRefs = Array.Empty(), }; var animated = new GameWindow.AnimatedEntity { Entity = entity, Setup = new Setup(), Animation = new Animation(), LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = 1f, PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; var motion = new GameWindow.RemoteMotion { Airborne = true, }; motion.Body.Position = entity.Position; motion.Body.Orientation = initial; motion.Body.TransientState = TransientStateFlags.Active; var root = new MotionDeltaFrame { Origin = new Vector3(0f, 10f, 0f), Orientation = rootTurn, }; var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); updater.Tick(motion, animated, 0.1f, root, 0, 0); Assert.Equal(new Vector3(10f, 20f, 30f), motion.Body.Position); Quaternion expected = Quaternion.Normalize(initial * rootTurn); Assert.InRange( MathF.Abs(Quaternion.Dot( expected, Quaternion.Normalize(motion.Body.Orientation))), 0.99999f, 1.00001f); } [Fact] public void Tick_ActiveInterpolation_ReplacesRootWithCompleteTargetFrame() { Quaternion initial = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1.1f); Quaternion target = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f); var entity = new WorldEntity { Id = 12u, ServerGuid = 0x80000012u, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 20f, 30f), Rotation = initial, MeshRefs = Array.Empty(), }; var animated = new GameWindow.AnimatedEntity { Entity = entity, Setup = new Setup(), Animation = new Animation(), LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = 1f, PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; var motion = new GameWindow.RemoteMotion(); motion.Body.Position = entity.Position; motion.Body.Orientation = initial; motion.Interp.Enqueue( entity.Position + Vector3.UnitX, target, isMovingTo: false, currentBodyPosition: entity.Position); var root = new MotionDeltaFrame { Origin = new Vector3(0f, 10f, 0f), Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.7f), }; var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); updater.Tick(motion, animated, 0.1f, root, 0, 0); // InterpolationManager::adjust_offset clamps the 1 m correction to // maxSpeed (8 m/s) * 0.1 s = 0.8 m and replaces the authored 10 m // root origin rather than adding to it. Assert.InRange(motion.Body.Position.X, 10.79f, 10.81f); Assert.InRange( MathF.Abs(Quaternion.Dot( Quaternion.Normalize(target), Quaternion.Normalize(motion.Body.Orientation))), 0.99999f, 1.00001f); Assert.Equal(motion.Body.Orientation, entity.Rotation); } [Fact] public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics() { var motion = new GameWindow.RemoteMotion(); motion.Body.Position = Vector3.Zero; motion.Body.Orientation = Quaternion.Identity; motion.Body.Velocity = new Vector3(4f, 0f, 5f); motion.Body.Acceleration = new Vector3(0f, 0f, -9.8f); motion.CellId = 0x01010001u; motion.Interp.Enqueue( new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false, currentBodyPosition: Vector3.Zero); var entity = new WorldEntity { Id = 1u, ServerGuid = 0x70000001u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var poses = new EntityEffectPoseRegistry(); poses.Publish(entity, Array.Empty()); updater.TickHidden(motion, entity, 0.1f); Assert.True(poses.UpdateRoot(entity)); Assert.InRange(motion.Body.Position.X, 0.01f, 1f); Assert.Equal(0f, motion.Body.Position.Z); Assert.Equal(new Vector3(4f, 0f, 5f), motion.Body.Velocity); Assert.Equal(motion.Body.Position, entity.Position); Assert.Equal(motion.CellId, entity.ParentCellId); Assert.True(poses.TryGetRootPose(entity.Id, out Matrix4x4 root)); Assert.Equal(entity.Position, root.Translation); } [Fact] public void TickHidden_RunsPartArrayManagerTailWithoutAdvancingSequence() { var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion(); motion.Body.Orientation = Quaternion.Identity; var entity = new WorldEntity { Id = 2u, ServerGuid = 0x70000002u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; AnimationSequencer sequencer = CreateSequencer(); sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0); updater.TickHidden( motion, entity, 0.1f, sequencer.Manager); Assert.Empty(sequencer.Manager.PendingAnimations); } [Fact] public void TickHidden_ProcessesPendingHooksBeforeManagerTail() { var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion(); motion.Body.Orientation = Quaternion.Identity; var entity = new WorldEntity { Id = 3u, ServerGuid = 0x70000003u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; AnimationSequencer sequencer = CreateSequencer(); sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0); var order = new List(); updater.TickHidden( motion, entity, 0.1f, partArrayHandleMovement: sequencer.Manager, processAnimationHooks: (_, observed) => { Assert.Same(sequencer, observed); Assert.NotEmpty(sequencer.Manager.PendingAnimations); order.Add("hooks"); }, sequencer); Assert.Equal(["hooks"], order); Assert.Empty(sequencer.Manager.PendingAnimations); } [Fact] public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide() { var engine = BuildBoundaryEngine(); var updater = new RemotePhysicsUpdater( engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion(); motion.Body.Position = new Vector3(191f, 10f, 50f); motion.Body.Orientation = Quaternion.Identity; motion.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; uint canonicalCell = 0xA9B40039u; int canonicalWrites = 0; motion.BindCanonicalCell( () => canonicalCell, value => { canonicalCell = value; canonicalWrites++; }); motion.CellId = canonicalCell; canonicalWrites = 0; motion.Interp.Enqueue( new Vector3(193f, 10f, 50f), heading: 0f, isMovingTo: false, currentBodyPosition: motion.Body.Position); var entity = new WorldEntity { Id = 1u, ServerGuid = 0x70000002u, SourceGfxObjOrSetupId = 0x02000001u, Position = motion.Body.Position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; updater.TickHidden(motion, entity, 2f); Assert.Equal(0xAAB40001u, canonicalCell); Assert.Equal(canonicalCell, entity.ParentCellId); Assert.True(canonicalWrites > 0); Assert.True(entity.Position.X > 192f); } [Fact] public void TickHidden_OutOfContactDoesNotInterpolateOrInventLanding() { var engine = BuildBoundaryEngine(); var updater = new RemotePhysicsUpdater( engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion { Airborne = true, }; motion.Body.Position = new Vector3(10f, 10f, 50f); motion.Body.Orientation = Quaternion.Identity; motion.Body.TransientState = TransientStateFlags.Active; motion.CellId = 0xA9B40001u; motion.Interp.Enqueue( new Vector3(11f, 10f, 50f), heading: 0f, isMovingTo: false, currentBodyPosition: motion.Body.Position); var entity = new WorldEntity { Id = 3u, ServerGuid = 0x70000003u, SourceGfxObjOrSetupId = 0x02000001u, Position = motion.Body.Position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; updater.TickHidden(motion, entity, 0.1f); Assert.False(motion.Body.InContact); Assert.False(motion.Body.OnWalkable); Assert.True(motion.Airborne); Assert.Equal(new Vector3(10f, 10f, 50f), motion.Body.Position); } [Fact] public void Tick_CrossCellIntoPending_CommitsRootAndSuspendsShadowBeforeReturning() { using var fixture = new BoundaryRemoteFixture(); Vector3 sourceShadowPosition = fixture.Remote.LastShadowSyncPos; ulong clockEpoch = fixture.Record.ObjectClockEpoch; bool current = fixture.Updater.Tick( fixture.Remote, fixture.Entity, objectScale: 1f, sequencer: null, animationForVelocityCycle: null, dt: 2f, rootMotionLocalFrame: new MotionDeltaFrame(), liveCenterX: 0, liveCenterY: 0, ownerRuntime: fixture.Live, ownerRecord: fixture.Record, ownerClockEpoch: clockEpoch); Assert.False(current); Assert.Equal(BoundaryRemoteFixture.DestinationCell, fixture.Record.FullCellId); Assert.False(fixture.Record.IsSpatiallyVisible); Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position); Assert.Equal(BoundaryRemoteFixture.DestinationCell, fixture.Entity.ParentCellId); Assert.True(fixture.Entity.Position.X > 192f); Assert.Equal(sourceShadowPosition, fixture.Remote.LastShadowSyncPos); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount); Assert.Equal(1, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); fixture.HydrateDestination(); Assert.True(fixture.Record.IsSpatiallyVisible); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(0, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); ShadowEntry restored = Assert.Single( fixture.Engine.ShadowObjects.AllEntriesForDebug()); Assert.Equal(fixture.Entity.Id, restored.EntityId); Assert.Equal(fixture.Entity.Position, restored.Position); } [Fact] public void AuthoritativeShadowPublisher_AlreadyPendingCannotResurrectShadow() { using var fixture = new BoundaryRemoteFixture(); int publications = 0; Assert.True(LiveEntityShadowPublisher.TryPublishRemote( fixture.Live, fixture.Record, fixture.Entity, fixture.Remote, fixture.Record.PositionAuthorityVersion, () => publications++)); Assert.Equal(1, publications); Assert.True(fixture.Live.RebucketLiveEntity( BoundaryRemoteFixture.Guid, BoundaryRemoteFixture.DestinationCell)); Assert.False(fixture.Record.IsSpatiallyVisible); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(1, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); Assert.False(LiveEntityShadowPublisher.TryPublishRemote( fixture.Live, fixture.Record, fixture.Entity, fixture.Remote, fixture.Record.PositionAuthorityVersion, () => publications++)); Assert.Equal(1, publications); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(1, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); fixture.HydrateDestination(); Assert.True(fixture.Record.IsSpatiallyVisible); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(0, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); } [Fact] public void AuthoritativeShadowPublisher_StaleAuthorityAndGuidReuseCannotPublish() { using var fixture = new BoundaryRemoteFixture(); LiveEntityRecord oldRecord = fixture.Record; WorldEntity oldEntity = fixture.Entity; GameWindow.RemoteMotion oldRemote = fixture.Remote; ulong oldAuthority = oldRecord.PositionAuthorityVersion; int publications = 0; Assert.False(LiveEntityShadowPublisher.TryPublishRemote( fixture.Live, oldRecord, oldEntity, oldRemote, oldAuthority + 1, () => publications++)); var replacement = fixture.ReplaceAtSource(); Assert.False(LiveEntityShadowPublisher.TryPublishRemote( fixture.Live, oldRecord, oldEntity, oldRemote, oldAuthority, () => publications++)); Assert.Equal(0, publications); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Contains( fixture.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == replacement.Entity.Id); Assert.DoesNotContain( fixture.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == oldEntity.Id); } [Fact] public void Tick_CrossCellVisibilityCallbackGuidReuse_CannotPublishOldShadow() { using var fixture = new BoundaryRemoteFixture(); LiveEntityRecord oldRecord = fixture.Record; WorldEntity oldEntity = fixture.Entity; GameWindow.RemoteMotion oldRemote = fixture.Remote; WorldEntity? replacementEntity = null; GameWindow.RemoteMotion? replacementRemote = null; ulong clockEpoch = oldRecord.ObjectClockEpoch; fixture.Live.ProjectionVisibilityChanged += (record, visible) => { if (visible || replacementEntity is not null || !ReferenceEquals(record, oldRecord)) { return; } (LiveEntityRecord replacementRecord, replacementEntity, replacementRemote) = fixture.ReplaceAtSource(); Assert.NotSame(oldRecord, replacementRecord); }; bool current = fixture.Updater.Tick( oldRemote, oldEntity, objectScale: 1f, sequencer: null, animationForVelocityCycle: null, dt: 2f, rootMotionLocalFrame: new MotionDeltaFrame(), liveCenterX: 0, liveCenterY: 0, ownerRuntime: fixture.Live, ownerRecord: oldRecord, ownerClockEpoch: clockEpoch); Assert.False(current); Assert.NotNull(replacementEntity); Assert.NotNull(replacementRemote); Assert.True(fixture.Live.TryGetRecord( BoundaryRemoteFixture.Guid, out LiveEntityRecord currentRecord)); Assert.NotSame(oldRecord, currentRecord); Assert.Same(replacementEntity, currentRecord.WorldEntity); Assert.Same(replacementRemote, currentRecord.RemoteMotionRuntime); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount); Assert.Equal(0, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); Assert.Contains( fixture.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == replacementEntity.Id); Assert.DoesNotContain( fixture.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == oldEntity.Id); } [Fact] public void TickHiddenEntities_DeletionCallbackCannotAdvanceStaleSnapshotOwner() { const uint firstGuid = 0x70000011u; const uint secondGuid = 0x70000012u; var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new LandBlock(), Array.Empty())); var live = new LiveEntityRuntime( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); BindHiddenRemote(live, firstGuid); BindHiddenRemote(live, secondGuid); Assert.Equal(2, live.SpatialRemoteMotionRuntimes.Count); var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var published = new List(); var partPoseDirty = new List(); updater.TickHiddenEntities( live, localPlayerServerGuid: 0x50000001u, dt: 0.1f, entity => { published.Add(entity.ServerGuid); uint other = entity.ServerGuid == firstGuid ? secondGuid : firstGuid; Assert.True(live.UnregisterLiveEntity( new DeleteObject.Parsed(other, InstanceSequence: 1), isLocalPlayer: false)); }, markPartPoseDirty: partPoseDirty.Add); Assert.Single(published); Assert.Equal(published, partPoseDirty); Assert.Single(live.SpatialRemoteMotionRuntimes); } private static PhysicsEngine BuildBoundaryEngine() { static TerrainSurface FlatTerrain() { var heights = new byte[81]; var table = new float[256]; Array.Fill(table, 50f); return new TerrainSurface(heights, table); } var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( 0xA9B4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 0f, 0f); engine.AddLandblock( 0xAAB4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 192f, 0f); return engine; } private static void BindHiddenRemote(LiveEntityRuntime live, uint guid) { const uint cellId = 0x01010001u; PhysicsStateFlags state = PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions; var position = new CreateObject.ServerPosition( cellId, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1); var physics = new PhysicsSpawnData( RawState: (uint)state, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); live.RegisterLiveEntity(new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "hidden fixture", null, null, 0x09000001u, PhysicsState: (uint)state, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics)); WorldEntity entity = live.MaterializeLiveEntity( guid, cellId, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = cellId, })!; var remote = new GameWindow.RemoteMotion(); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.CellId = cellId; remote.Interp.Enqueue( entity.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: entity.Position); live.SetRemoteMotionRuntime(guid, remote); } private sealed class BoundaryRemoteFixture : IDisposable { public const uint Guid = 0x70000071u; public const uint SourceCell = 0xA9B40039u; public const uint DestinationCell = 0xAAB40001u; private LiveEntityPresentationController? _presentation; public BoundaryRemoteFixture() { Engine = BuildBoundaryEngine(); Spatial.AddLandblock(new LoadedLandblock( 0xA9B4FFFFu, new LandBlock(), Array.Empty())); Live = new LiveEntityRuntime( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), record => { if (record.WorldEntity is { } entity) Engine.ShadowObjects.Deregister(entity.Id); _presentation?.Forget(record); }); (Record, Entity, Remote) = SpawnAndBind( instanceSequence: 1, new Vector3(191f, 10f, 50f), enqueueDestination: true); _presentation = new LiveEntityPresentationController( Live, Engine.ShadowObjects, (_, _, _) => true, liveCenter: () => (0, 0)); RegisterShadow(Entity, Remote); Assert.True(_presentation.OnLiveEntityReady(Guid)); Updater = new RemotePhysicsUpdater( Engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); } public PhysicsEngine Engine { get; } public GpuWorldState Spatial { get; } = new(); public LiveEntityRuntime Live { get; } public RemotePhysicsUpdater Updater { get; } public LiveEntityRecord Record { get; } public WorldEntity Entity { get; } public GameWindow.RemoteMotion Remote { get; } public void HydrateDestination() => Spatial.AddLandblock(new LoadedLandblock( 0xAAB4FFFFu, new LandBlock(), Array.Empty())); public (LiveEntityRecord Record, WorldEntity Entity, GameWindow.RemoteMotion Remote) ReplaceAtSource() { Assert.True(Live.UnregisterLiveEntity( new DeleteObject.Parsed(Guid, InstanceSequence: 1), isLocalPlayer: false)); var replacement = SpawnAndBind( instanceSequence: 2, new Vector3(180f, 20f, 50f), enqueueDestination: false); RegisterShadow(replacement.Entity, replacement.Remote); Assert.True(_presentation!.OnLiveEntityReady(Guid)); return replacement; } public void Dispose() { Live.Clear(); _presentation?.Dispose(); } private (LiveEntityRecord Record, WorldEntity Entity, GameWindow.RemoteMotion Remote) SpawnAndBind( ushort instanceSequence, Vector3 position, bool enqueueDestination) { LiveEntityRegistrationResult registration = Live.RegisterLiveEntity( Spawn(instanceSequence, position)); LiveEntityRecord record = Assert.IsType( registration.Record); WorldEntity entity = Assert.IsType( Live.MaterializeLiveEntity( Guid, SourceCell, id => new WorldEntity { Id = id, ServerGuid = Guid, SourceGfxObjOrSetupId = 0x02000001u, Position = position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = SourceCell, })); var remote = new GameWindow.RemoteMotion(); remote.Body.Position = position; remote.Body.Orientation = Quaternion.Identity; remote.CellId = SourceCell; remote.LastShadowSyncPos = position; remote.LastShadowSyncOrientation = Quaternion.Identity; if (enqueueDestination) { remote.Interp.Enqueue( new Vector3(193f, 10f, 50f), heading: 0f, isMovingTo: false, currentBodyPosition: position); } Live.SetRemoteMotionRuntime(Guid, remote); return (record, entity, remote); } private void RegisterShadow( WorldEntity entity, GameWindow.RemoteMotion remote) { Engine.ShadowObjects.Register( entity.Id, entity.SourceGfxObjOrSetupId, remote.Body.Position, remote.Body.Orientation, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0xA9B40000u, collisionType: ShadowCollisionType.Cylinder, cylHeight: 1.835f, state: (uint)PhysicsStateFlags.ReportCollisions, seedCellId: SourceCell, isStatic: false); } private static WorldSession.EntitySpawn Spawn( ushort instanceSequence, Vector3 position) { const PhysicsStateFlags state = PhysicsStateFlags.ReportCollisions; var serverPosition = new CreateObject.ServerPosition( SourceCell, position.X, position.Y, position.Z, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1); var physics = new PhysicsSpawnData( RawState: (uint)state, Position: serverPosition, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( Guid, serverPosition, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "boundary remote", null, null, MotionTableId: 0x09000001u, PhysicsState: (uint)state, InstanceSequence: instanceSequence, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } } private static AnimationSequencer CreateSequencer() => new(new Setup(), new MotionTable(), new NullAnimationLoader()); private sealed class NullAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint id) => null; } }