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; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.App.Tests.Rendering; public sealed class LiveEntityAnimationSchedulerTests { private const uint LocalGuid = 0x50000001u; private const uint RemoteGuid = 0x70000001u; private const uint Cell = 0x01010001u; [Fact] public void HiddenLocalPose_IsSampledOnlyForMarkedObjectQuantum() { var (live, record, animation) = BuildHiddenAnimated(LocalGuid); LiveEntityAnimationScheduler scheduler = BuildScheduler( live, localPlayerGuid: LocalGuid); Assert.True(scheduler.Tick( PhysicsBody.MaxQuantum, animation.Entity.Position, localHiddenPartPoseDirty: true, liveCenterX: 0, liveCenterY: 0) .TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked)); bool repeated = scheduler.Tick( PhysicsBody.MaxQuantum, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0) .TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed); Assert.True(marked.ComposeParts); Assert.NotNull(marked.SequenceFrames); Assert.False(repeated); Assert.False(consumed.ComposeParts); } [Fact] public void HiddenRemotePose_IsSampledOnceAfterAdmittedHiddenQuantum() { var (live, record, animation) = BuildHiddenAnimated(RemoteGuid); var remote = new GameWindow.RemoteMotion { CellId = Cell, }; remote.Body.Position = animation.Entity.Position; remote.Body.Orientation = animation.Entity.Rotation; remote.Body.InWorld = true; live.SetRemoteMotionRuntime(RemoteGuid, remote); LiveEntityAnimationScheduler scheduler = BuildScheduler( live, localPlayerGuid: LocalGuid); Assert.True(scheduler.Tick( PhysicsBody.MaxQuantum, playerPosition: null, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0) .TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked)); bool repeated = scheduler.Tick( elapsedSeconds: 0f, playerPosition: null, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0) .TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed); Assert.True(marked.ComposeParts); Assert.NotNull(marked.SequenceFrames); Assert.False(repeated); Assert.False(consumed.ComposeParts); } [Fact] public void VisibleAnimationWithoutRemoteMotion_AppliesCompleteRootFrame() { var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid); var retainedBodyOwner = BuildRemote(animation.Entity); retainedBodyOwner.Body.SnapToCell( Cell, animation.Entity.Position, animation.Entity.Position); live.SetRemoteMotionRuntime(RemoteGuid, retainedBodyOwner); Assert.True(live.ClearRemoteMotionRuntime(RemoteGuid)); LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid); float startX = animation.Entity.Position.X; Assert.True(scheduler.Tick( 0.1f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0) .ContainsKey(animation.Entity.Id)); Assert.True(animation.Entity.Position.X > startX + 1f); } [Fact] public void BodylessNonWalkableAnimation_SuppressesRootOriginButPreservesOrientation() { Quaternion rootTurn = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, MathF.PI / 2f); var (live, _, animation) = BuildVisibleAnimatedWithPosFrames( RemoteGuid, rootTurn); Vector3 startPosition = animation.Entity.Position; LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid); scheduler.Tick( PhysicsBody.MinQuantum * 1.01f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(startPosition, animation.Entity.Position); Assert.InRange( MathF.Abs(Quaternion.Dot( rootTurn, Quaternion.Normalize(animation.Entity.Rotation))), 0.99999f, 1.00001f); } [Fact] public void RemoteWithoutAnimation_RemainsInOrdinaryObjectWorkset() { var (live, record, entity) = BuildMaterialized( RemoteGuid, PhysicsStateFlags.Gravity); var remote = new GameWindow.RemoteMotion { CellId = Cell, Airborne = true, }; remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.Body.Velocity = new Vector3(3f, 0f, 0f); remote.Body.State = PhysicsStateFlags.Gravity; remote.Body.TransientState = TransientStateFlags.Active; live.SetRemoteMotionRuntime(RemoteGuid, remote); LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid); float startX = entity.Position.X; scheduler.Tick( 0.1f, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.True(entity.Position.X > startX); Assert.Same(record, live.SpatialRootObjects[RemoteGuid]); } [Fact] public void AnimationlessRemote_NearPositionCorrectionIsQueuedAndConsumed() { var (live, record, entity) = BuildMaterialized( RemoteGuid, PhysicsStateFlags.ReportCollisions); var remote = BuildRemote(entity); remote.Interp.Enqueue( entity.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: remote.Body.Position); live.SetRemoteMotionRuntime(RemoteGuid, remote); Assert.Null(record.AnimationRuntime); Assert.True(live.IsCurrentSpatialRemoteMotion(record, remote)); LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid); float startX = entity.Position.X; scheduler.Tick( 0.1f, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 1, liveCenterY: 1); Assert.True(entity.Position.X > startX + 0.5f); Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f); Assert.Same(record, live.SpatialRootObjects[RemoteGuid]); } [Fact] public void RetainedProjectileWithoutRemote_WhenMissileClears_AdvancesOnceAsOrdinaryBody() { var (live, record, animation) = BuildVisibleAnimatedWithPosFrames( RemoteGuid, rootOrigin: Vector3.Zero); var physics = new PhysicsEngine(); var projectiles = new ProjectileController(live, physics); record.FinalPhysicsState = ProjectileState; Assert.True(record.ObjectClock.IsActive); Assert.True(projectiles.TryBind( record, ProjectileSetup(), currentTime: 1.0, liveCenterX: 1, liveCenterY: 1)); Assert.True(record.ObjectClock.IsActive); PhysicsBody body = record.PhysicsBody!; body.set_velocity(new Vector3(3f, 0f, 0f)); body.TransientState = TransientStateFlags.Active; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Gravity; Assert.True(projectiles.ApplyAuthoritativeState( record, record.FinalPhysicsState, currentTime: 1.1, liveCenterX: 1, liveCenterY: 1)); Assert.True(projectiles.HandlesMovement(RemoteGuid)); float startX = animation.Entity.Position.X; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, publishRootPose: _ => rootPublishes++, physics: physics, projectiles: projectiles); scheduler.Tick( 0.1f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 1, liveCenterY: 1); float distance = animation.Entity.Position.X - startX; Assert.InRange(distance, 0.2f, 0.4f); Assert.Equal(1, rootPublishes); Assert.Same(body, record.PhysicsBody); Assert.Same(body, record.ProjectileRuntime!.Body); } [Fact] public void RetainedProjectileWithRemote_WhenMissileClears_TransfersMovementToRemoteOnce() { var (live, record, animation) = BuildVisibleAnimatedWithPosFrames( RemoteGuid, rootOrigin: Vector3.Zero); var physics = new PhysicsEngine(); var projectiles = new ProjectileController(live, physics); record.FinalPhysicsState = ProjectileState; var remote = new GameWindow.RemoteMotion { CellId = Cell, Airborne = false, }; remote.Body.Position = animation.Entity.Position; remote.Body.Orientation = animation.Entity.Rotation; remote.Body.State = ProjectileState; remote.Body.InWorld = true; remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact | TransientStateFlags.OnWalkable; live.SetRemoteMotionRuntime(RemoteGuid, remote); Assert.True(record.ObjectClock.IsActive); Assert.Equal(Cell, record.FullCellId); Assert.True(record.IsSpatiallyVisible); Assert.True(projectiles.TryBind( record, ProjectileSetup(), currentTime: 1.0, liveCenterX: 1, liveCenterY: 1)); Assert.Equal(Cell, record.FullCellId); Assert.True(record.IsSpatiallyVisible); Assert.True(record.ObjectClock.IsActive); Assert.Same(remote.Body, record.ProjectileRuntime!.Body); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Gravity; Assert.True(projectiles.ApplyAuthoritativeState( record, record.FinalPhysicsState, currentTime: 1.1, liveCenterX: 1, liveCenterY: 1)); Assert.False(projectiles.HandlesMovement(RemoteGuid)); remote.Interp.Enqueue( animation.Entity.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: remote.Body.Position); Assert.True(record.ObjectClock.IsActive); Assert.True(remote.Body.IsActive); Assert.True(live.IsCurrentSpatialRemoteMotion(record, remote)); Assert.True(live.IsCurrentSpatialAnimation(record, animation)); float startX = animation.Entity.Position.X; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, publishRootPose: _ => rootPublishes++, physics: physics, projectiles: projectiles); IReadOnlyDictionary schedules = scheduler.Tick( 0.1f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 1, liveCenterY: 1); Assert.Contains(animation.Entity.Id, schedules.Keys); Assert.Equal(1, rootPublishes); float distance = animation.Entity.Position.X - startX; Assert.InRange(distance, 0.79f, 0.81f); Assert.Same(remote.Body, record.PhysicsBody); Assert.Same(remote.Body, record.ProjectileRuntime!.Body); } [Fact] public void DeleteFromFirstQuantumCallback_StopsCatchUpBatchAndPosePublish() { var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid); int captures = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, (_, _) => { captures++; live.UnregisterLiveEntity( new DeleteObject.Parsed(RemoteGuid, record.Generation), isLocalPlayer: false); }); IReadOnlyDictionary schedules = scheduler.Tick( PhysicsBody.MaxQuantum * 2.5f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, captures); Assert.Empty(schedules); Assert.False(live.TryGetRecord(RemoteGuid, out _)); } [Fact] public void VisibleRemoteDeleteInsideHook_StopsBeforeStaleEntityPublication() { var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid); var remote = BuildRemote(animation.Entity); live.SetRemoteMotionRuntime(RemoteGuid, remote); Vector3 retainedEntityPosition = animation.Entity.Position; int captures = 0; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, (_, _) => { captures++; live.UnregisterLiveEntity( new DeleteObject.Parsed(RemoteGuid, record.Generation), isLocalPlayer: false); }, _ => rootPublishes++); IReadOnlyDictionary schedules = scheduler.Tick( PhysicsBody.MaxQuantum * 2.5f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, captures); Assert.Equal(0, rootPublishes); Assert.Empty(schedules); Assert.Equal(retainedEntityPosition, animation.Entity.Position); } [Fact] public void HiddenRemoteDeleteInsideHook_StopsBeforeStaleEntityPublication() { var (live, record, animation) = BuildHiddenAnimated(RemoteGuid); var remote = BuildRemote(animation.Entity); live.SetRemoteMotionRuntime(RemoteGuid, remote); Vector3 retainedEntityPosition = animation.Entity.Position; int captures = 0; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, (_, _) => { captures++; live.UnregisterLiveEntity( new DeleteObject.Parsed(RemoteGuid, record.Generation), isLocalPlayer: false); }, _ => rootPublishes++); IReadOnlyDictionary schedules = scheduler.Tick( PhysicsBody.MaxQuantum * 2.5f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, captures); Assert.Equal(0, rootPublishes); Assert.Empty(schedules); Assert.Equal(retainedEntityPosition, animation.Entity.Position); } [Fact] public void HiddenAnimationWithoutRemoteDeleteInsideHook_StopsBeforeManagerTailOrPublication() { var (live, record, animation) = BuildHiddenAnimated(RemoteGuid); int captures = 0; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, (_, _) => { captures++; live.UnregisterLiveEntity( new DeleteObject.Parsed(RemoteGuid, record.Generation), isLocalPlayer: false); }, _ => rootPublishes++); IReadOnlyDictionary schedules = scheduler.Tick( PhysicsBody.MaxQuantum * 2.5f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, captures); Assert.Equal(0, rootPublishes); Assert.Empty(schedules); Assert.False(live.TryGetRecord(RemoteGuid, out _)); } [Fact] public void WithdrawAndReenterInsideHook_InvalidatesOldCatchUpBatch() { var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid); ulong startingEpoch = record.ObjectClockEpoch; int captures = 0; int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, (_, _) => { captures++; Assert.True(live.WithdrawLiveEntityProjection(RemoteGuid)); Assert.Same( animation.Entity, live.MaterializeLiveEntity( RemoteGuid, Cell, _ => throw new InvalidOperationException( "A retained projection must not be reconstructed."))); }, _ => rootPublishes++); IReadOnlyDictionary schedules = scheduler.Tick( PhysicsBody.MaxQuantum * 2.5f, animation.Entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, captures); Assert.Equal(0, rootPublishes); Assert.Empty(schedules); Assert.Equal(startingEpoch + 2, record.ObjectClockEpoch); Assert.True(record.ObjectClock.IsActive); Assert.Equal(0d, record.ObjectClock.PendingSeconds); } [Fact] public void LocalRootPosePublishesWithoutAnimationAndBetweenObjectQuanta() { var (live, _, entity) = BuildMaterialized(LocalGuid, PhysicsStateFlags.None); var published = new List(); LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, publishRootPose: current => published.Add(current.Position)); scheduler.Tick( PhysicsBody.MinQuantum * 0.25f, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); entity.SetPosition(entity.Position + new Vector3(0.25f, 0f, 0f)); scheduler.Tick( PhysicsBody.MinQuantum * 0.25f, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(2, published.Count); Assert.Equal(entity.Position, published[1]); } [Fact] public void AttachedProjection_IsNotAnIndependentOrdinaryRoot() { var (live, record, entity) = BuildMaterialized( RemoteGuid, PhysicsStateFlags.None); int rootPublishes = 0; LiveEntityAnimationScheduler scheduler = BuildScheduler( live, LocalGuid, publishRootPose: _ => rootPublishes++); Assert.Same( entity, live.MaterializeLiveEntity( RemoteGuid, Cell, _ => throw new InvalidOperationException(), LiveEntityProjectionKind.Attached)); Assert.False(record.ObjectClock.IsActive); Assert.DoesNotContain(RemoteGuid, live.SpatialRootObjects.Keys); scheduler.Tick( PhysicsBody.MaxQuantum, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(0, rootPublishes); Assert.Same( entity, live.MaterializeLiveEntity( RemoteGuid, Cell, _ => throw new InvalidOperationException(), LiveEntityProjectionKind.World)); Assert.True(record.ObjectClock.IsActive); Assert.Contains(RemoteGuid, live.SpatialRootObjects.Keys); scheduler.Tick( PhysicsBody.MaxQuantum, entity.Position, localHiddenPartPoseDirty: false, liveCenterX: 0, liveCenterY: 0); Assert.Equal(1, rootPublishes); } private static GameWindow.RemoteMotion BuildRemote(WorldEntity entity) { var remote = new GameWindow.RemoteMotion { CellId = Cell, }; remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.Body.InWorld = true; remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact | TransientStateFlags.OnWalkable; return remote; } private static LiveEntityAnimationScheduler BuildScheduler( LiveEntityRuntime live, uint localPlayerGuid, Action? captureHooks = null, Action? publishRootPose = null, PhysicsEngine? physics = null, ProjectileController? projectiles = null) { physics ??= new PhysicsEngine(); var remotePhysics = new RemotePhysicsUpdater( physics, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var ordinaryPhysics = new LiveEntityOrdinaryPhysicsUpdater( physics, (_, _) => (0.48f, 1.835f)); return new LiveEntityAnimationScheduler( () => live, () => localPlayerGuid, remotePhysics, ordinaryPhysics, () => projectiles, publishRootPose ?? (_ => { }), captureHooks ?? ((_, _) => { })); } private static (LiveEntityRuntime Live, LiveEntityRecord Record, GameWindow.AnimatedEntity Animation) BuildHiddenAnimated(uint guid) { var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new LandBlock(), Array.Empty())); var live = new LiveEntityRuntime( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!; WorldEntity entity = live.MaterializeLiveEntity( guid, Cell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = Cell, })!; var setup = new Setup(); var animation = new GameWindow.AnimatedEntity { Entity = entity, Setup = setup, Animation = new Animation(), LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = 1f, PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), Sequencer = new AnimationSequencer( setup, new MotionTable(), new NullAnimationLoader()), }; record.HasPartArray = true; live.SetAnimationRuntime(guid, animation); return (live, record, animation); } private static (LiveEntityRuntime Live, LiveEntityRecord Record, GameWindow.AnimatedEntity Animation) BuildVisibleAnimatedWithPosFrames( uint guid, Quaternion? rootOrientation = null, Vector3? rootOrigin = null) { var (live, record, entity) = BuildMaterialized(guid, PhysicsStateFlags.None); const uint animationId = 0x0300AA01u; var setup = new Setup(); setup.Parts.Add(0x0100AA01u); setup.DefaultScale.Add(Vector3.One); var datAnimation = new Animation { Flags = AnimationFlags.PosFrames, }; for (int i = 0; i < 4; i++) { var partFrame = new AnimationFrame(1); partFrame.Frames.Add(new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }); datAnimation.PartFrames.Add(partFrame); datAnimation.PosFrames.Add(new Frame { Origin = rootOrigin ?? Vector3.UnitX, Orientation = rootOrientation ?? Quaternion.Identity, }); } var loader = new DictionaryAnimationLoader(animationId, datAnimation); var sequencer = new AnimationSequencer(setup, new MotionTable(), loader); Assert.True(sequencer.InitializeSetupDefaultAnimation(animationId)); var animation = new GameWindow.AnimatedEntity { Entity = entity, Setup = setup, Animation = datAnimation, LowFrame = 0, HighFrame = 3, Framerate = 30f, Scale = 1f, PartTemplate = new[] { new GameWindow.AnimatedPartTemplate( 0x0100AA01u, SurfaceOverrides: null, IsDrawable: true), }, PartAvailability = new[] { true }, Sequencer = sequencer, }; record.HasPartArray = true; live.SetAnimationRuntime(guid, animation); return (live, record, animation); } private static readonly PhysicsStateFlags ProjectileState = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; private static Setup ProjectileSetup() => new() { Spheres = { new Sphere { Origin = Vector3.Zero, Radius = 0.1f, }, }, }; private static (LiveEntityRuntime Live, LiveEntityRecord Record, WorldEntity Entity) BuildMaterialized(uint guid, PhysicsStateFlags state) { var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new LandBlock(), Array.Empty())); var live = new LiveEntityRuntime( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid, state)).Record!; WorldEntity entity = live.MaterializeLiveEntity( guid, Cell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = Cell, })!; return (live, record, entity); } private static WorldSession.EntitySpawn Spawn( uint guid, PhysicsStateFlags state = PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions) { var position = new CreateObject.ServerPosition( Cell, 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); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "hidden scheduler fixture", null, null, 0x09000001u, PhysicsState: (uint)state, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class NullAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint id) => null; } private sealed class DictionaryAnimationLoader : IAnimationLoader { private readonly uint _id; private readonly Animation _animation; public DictionaryAnimationLoader(uint id, Animation animation) { _id = id; _animation = animation; } public Animation? LoadAnimation(uint id) => id == _id ? _animation : null; } }