using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Lighting; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Vfx; using AcDream.Core.World; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand; namespace AcDream.App.Tests.World; /// /// Step 9 deterministic whole-lifecycle gate for the canonical live-object, /// projectile, and DAT-effect ownership graph. It deliberately exercises /// enough simultaneous owners to expose collection mutation and incomplete /// teardown that single-object conformance tests cannot reveal. /// public sealed class LiveEntityLifecycleStressTests { private const int OwnerCount = 96; private const int ReusedOwnerCount = 32; private const uint GuidBase = 0x72000000u; private const uint CellA = 0x01010001u; private const uint CellB = 0x01020001u; private const uint ScriptDid = 0x3300AA01u; private const uint EmitterDid = 0x3200AA01u; private const uint SetupDid = 0x02000124u; private const uint RecallMotion = 0x10000153u; private static readonly PhysicsStateFlags MissileState = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Lighting | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; [Fact] public void NinetySixOwners_ChurnDeleteResetAndGuidReuse_LeaveNoRuntimeResources() { using var fixture = new Fixture(); var oldLocalIds = new List(OwnerCount + ReusedOwnerCount); var lifetimeRecords = new List(OwnerCount + ReusedOwnerCount); for (int i = 0; i < OwnerCount; i++) { uint guid = GuidBase + (uint)i; fixture.Effects.HandleDirect(new PlayPhysicsScript(guid, ScriptDid)); LiveEntityRecord record = fixture.Spawn(guid, generation: 1, i); lifetimeRecords.Add(record); oldLocalIds.Add(record.LocalEntityId!.Value); // Repeated direct network PES plays append to the same owner's // retail FIFO. They must neither deduplicate nor create another // logical owner. The separate recall test below drives the exact // recall motion and CallPES chain. if (i % 12 == 0) { fixture.Effects.HandleDirect(new PlayPhysicsScript(guid, ScriptDid)); fixture.Effects.HandleDirect(new PlayPhysicsScript(guid, ScriptDid)); } } fixture.Runner.Tick(0.0); fixture.Particles.Tick(0.01f); fixture.Projectiles.Tick( currentTime: 0.05, liveCenterX: 1, liveCenterY: 1, playerWorldPosition: null); Assert.Equal(OwnerCount, fixture.Runtime.Count); Assert.Equal(OwnerCount, fixture.Runtime.MaterializedCount); Assert.Equal(OwnerCount, fixture.RenderOwners.Count); Assert.Equal(OwnerCount, fixture.Projectiles.Count); Assert.Equal(OwnerCount, fixture.Effects.ReadyOwnerCount); Assert.Equal(0, fixture.Effects.PendingPacketCount); Assert.Equal(OwnerCount, fixture.Runner.ActiveOwnerCount); Assert.True(fixture.Runner.ActiveScriptCount > OwnerCount); Assert.Equal(OwnerCount, fixture.Particles.ActiveEmitterCount); Assert.Equal(OwnerCount, fixture.ParticleSink.ActiveBindingCount); Assert.Equal(OwnerCount, fixture.ParticleSink.LogicalEmitterCount); Assert.Equal(OwnerCount, fixture.ParticleSink.TrackedOwnerCount); Assert.Equal(OwnerCount, fixture.ParticleSink.RenderPassOwnerCount); Assert.Equal(OwnerCount, fixture.Poses.Count); Assert.Equal(OwnerCount, fixture.Lights.RegisteredCount); Assert.Equal(OwnerCount, fixture.LiveLights.TrackedOwnerCount); Assert.Equal(OwnerCount, fixture.LiveLights.PresentedOwnerCount); Assert.Equal(OwnerCount, fixture.Lighting.OwnedLightOwnerCount); Assert.Equal(OwnerCount, fixture.Lighting.PoseTrackedOwnerCount); Assert.Equal(OwnerCount, fixture.Lighting.RetainedOwnerStateCount); Assert.Equal(OwnerCount, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(OwnerCount, fixture.Engine.ShadowObjects.RetainedRegistrationCount); // Loaded -> pending -> loaded -> pending -> loaded churn keeps the // same projections and all logical owners; only spatial presentation // changes. This is the streaming race that previously replayed create. for (int i = 0; i < OwnerCount; i += 2) Assert.True(fixture.Runtime.RebucketLiveEntity(GuidBase + (uint)i, CellB)); Assert.Equal(OwnerCount / 2, fixture.Spatial.PendingLiveEntityCount); Assert.Equal(OwnerCount, fixture.RenderOwners.Count); Assert.Equal(OwnerCount / 2, fixture.ParticleSink.HiddenPresentationOwnerCount); Assert.Equal(OwnerCount / 2, fixture.Lights.RegisteredCount); Assert.Equal(OwnerCount / 2, fixture.LiveLights.PresentedOwnerCount); fixture.Spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); Assert.Equal(0, fixture.Spatial.PendingLiveEntityCount); Assert.Equal(0, fixture.ParticleSink.HiddenPresentationOwnerCount); Assert.Equal(OwnerCount, fixture.Lights.RegisteredCount); fixture.Spatial.RemoveLandblock(0x0102FFFFu); Assert.Equal(OwnerCount / 2, fixture.Spatial.PendingLiveEntityCount); fixture.Spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); Assert.Equal(0, fixture.Spatial.PendingLiveEntityCount); Assert.Equal(OwnerCount, fixture.RenderOwners.Count); Assert.Equal(OwnerCount, fixture.Projectiles.Count); Assert.Equal(OwnerCount, fixture.Lights.RegisteredCount); Assert.Equal(OwnerCount, fixture.LiveLights.PresentedOwnerCount); // End one third rapidly, then reuse those exact server GUIDs with a // newer instance timestamp. No queue, body, emitter, light, or shadow // from generation 1 may attach to generation 2. for (int i = 0; i < ReusedOwnerCount; i++) { uint guid = GuidBase + (uint)i; Assert.True(fixture.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); } Assert.Equal(OwnerCount - ReusedOwnerCount, fixture.Runtime.Count); Assert.Equal(OwnerCount - ReusedOwnerCount, fixture.RenderOwners.Count); Assert.Equal(OwnerCount - ReusedOwnerCount, fixture.Particles.ActiveEmitterCount); Assert.Equal(OwnerCount - ReusedOwnerCount, fixture.Lights.RegisteredCount); Assert.Equal( OwnerCount - ReusedOwnerCount, fixture.Engine.ShadowObjects.RetainedRegistrationCount); for (int i = 0; i < ReusedOwnerCount; i++) { uint guid = GuidBase + (uint)i; fixture.Effects.HandleDirect(new PlayPhysicsScript(guid, ScriptDid)); LiveEntityRecord replacement = fixture.Spawn(guid, generation: 2, i); lifetimeRecords.Add(replacement); oldLocalIds.Add(replacement.LocalEntityId!.Value); } fixture.Runner.Tick(1.0); fixture.Particles.Tick(0.01f); Assert.Equal(OwnerCount, fixture.Runtime.Count); Assert.Equal(OwnerCount, fixture.RenderOwners.Count); Assert.Equal(OwnerCount, fixture.Projectiles.Count); Assert.Equal(OwnerCount, fixture.Particles.ActiveEmitterCount); Assert.Equal(OwnerCount, fixture.Lights.RegisteredCount); Assert.Equal(OwnerCount, fixture.Engine.ShadowObjects.RetainedRegistrationCount); // Mix accepted deletes with a session reset, and leave packets for // never-created owners pending so the network reset path is covered. for (int i = 0; i < OwnerCount; i += 2) { uint guid = GuidBase + (uint)i; ushort generation = i < ReusedOwnerCount ? (ushort)2 : (ushort)1; Assert.True(fixture.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, generation), isLocalPlayer: false)); } for (int i = 0; i < 8; i++) { fixture.Effects.HandleDirect(new PlayPhysicsScript( 0x73000000u + (uint)i, ScriptDid)); } Assert.Equal(8, fixture.Effects.PendingPacketCount); fixture.Runtime.Clear(); fixture.Effects.ClearNetworkState(); fixture.Particles.Tick(0.01f); Assert.Equal(0, fixture.Runtime.Count); Assert.Equal(0, fixture.Runtime.MaterializedCount); Assert.Empty(fixture.RenderOwners); Assert.Equal(fixture.RenderRegisterCount, fixture.RenderUnregisterCount); Assert.Equal(0, fixture.Projectiles.Count); Assert.Equal(0, fixture.Effects.ReadyOwnerCount); Assert.Equal(0, fixture.Effects.PendingPacketCount); Assert.Equal(0, fixture.Runner.ActiveOwnerCount); Assert.Equal(0, fixture.Runner.ActiveScriptCount); Assert.Equal(0, fixture.Runner.ScheduledCallPesCount); Assert.Equal(0, fixture.Runner.OwnerAnchorCount); Assert.Equal(0, fixture.Particles.ActiveEmitterCount); Assert.Equal(0, fixture.Particles.ActiveParticleCount); Assert.Equal(0, fixture.ParticleSink.ActiveBindingCount); Assert.Equal(0, fixture.ParticleSink.LogicalEmitterCount); Assert.Equal(0, fixture.ParticleSink.TrackedOwnerCount); Assert.Equal(0, fixture.ParticleSink.RenderPassOwnerCount); Assert.Equal(0, fixture.ParticleSink.HiddenPresentationOwnerCount); Assert.Equal(0, fixture.Poses.Count); Assert.Equal(0, fixture.Lights.RegisteredCount); Assert.Equal(0, fixture.Lighting.OwnedLightOwnerCount); Assert.Equal(0, fixture.Lighting.PoseTrackedOwnerCount); Assert.Equal(0, fixture.Lighting.RetainedOwnerStateCount); Assert.Equal(0, fixture.LiveLights.TrackedOwnerCount); Assert.Equal(0, fixture.LiveLights.PresentedOwnerCount); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount); Assert.Equal(0, fixture.Engine.ShadowObjects.SuspendedRegistrationCount); Assert.Equal(0, fixture.Spatial.PendingLiveEntityCount); Assert.Equal(0, fixture.Spatial.PendingBucketCount); Assert.Equal(0, fixture.Spatial.PendingRescueCount); Assert.Equal(0, fixture.Spatial.PersistentGuidCount); Assert.Equal(0, fixture.Spatial.PendingVisibilityTransitionCount); Assert.DoesNotContain(fixture.Spatial.Entities, entity => entity.ServerGuid != 0); Assert.All(lifetimeRecords, record => { Assert.Null(record.PhysicsBody); Assert.Null(record.ProjectileRuntime); Assert.Null(record.EffectProfile); Assert.Null(record.WorldEntity); }); // A stale retained shadow payload would resurrect here despite the // visible-cell count already reading zero. foreach (uint localId in oldLocalIds) { fixture.Engine.ShadowObjects.UpdatePosition( localId, new Vector3(20f, 20f, 10f), Quaternion.Identity, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0x01010000u, seedCellId: CellA); } Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount); Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered); // Session teardown is idempotent and cannot recreate bookkeeping. fixture.Runtime.Clear(); fixture.Effects.ClearNetworkState(); Assert.Equal(0, fixture.Runtime.Count); Assert.Equal(0, fixture.Effects.PendingPacketCount); } [Fact] public void RepeatedRetailRecallMotion_HiddenTeleportUnhide_DrainsEveryPresentationOwner() { using var fixture = new RecallPortalFixture(); for (int cycle = 0; cycle < 12; cycle++) { fixture.PlayRecallMotion(cycle + 1.0); Assert.Equal(1, fixture.Particles.ActiveEmitterCount); Assert.Equal(1, fixture.ParticleSink.ActiveBindingCount); fixture.ApplyHidden(); Assert.Equal(1, fixture.Presentation.DeferredShadowRestoreCount); fixture.BeginDeferredTeleport(); Assert.Equal(1, fixture.Teleport.PendingPlacementCount); Assert.Equal(1, fixture.Presentation.ActivePlacementCount); fixture.LoadDestination(); Assert.Equal(0, fixture.Teleport.PendingPlacementCount); Assert.Equal(0, fixture.Presentation.ActivePlacementCount); Assert.Equal(1, fixture.Presentation.DeferredShadowRestoreCount); fixture.ApplyVisible(); Assert.Equal(0, fixture.Presentation.DeferredShadowRestoreCount); Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered); fixture.UnloadPriorLandblock(); } Assert.Equal(12, fixture.TypedScripts.Count(type => type == LiveEntityPresentationController.HiddenScriptType)); Assert.Equal(12, fixture.TypedScripts.Count(type => type == LiveEntityPresentationController.UnHideScriptType)); Assert.Equal(12, fixture.RecallHookCount); fixture.Clear(); Assert.Equal(0, fixture.Runtime.Count); Assert.Equal(0, fixture.Effects.ReadyOwnerCount); Assert.Equal(0, fixture.Runner.ActiveOwnerCount); Assert.Equal(0, fixture.Runner.OwnerAnchorCount); Assert.Equal(0, fixture.Particles.ActiveEmitterCount); Assert.Equal(0, fixture.ParticleSink.ActiveBindingCount); Assert.Equal(0, fixture.Poses.Count); Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount); Assert.Equal(0, fixture.Teleport.PendingPlacementCount); Assert.Equal(0, fixture.Presentation.ReadyOwnerCount); Assert.Equal(0, fixture.Presentation.DeferredShadowRestoreCount); Assert.Equal(0, fixture.Presentation.ActivePlacementCount); Assert.Equal(0, fixture.Spatial.PendingLiveEntityCount); Assert.Equal(0, fixture.Spatial.PendingBucketCount); } private sealed class Fixture : IDisposable { private readonly Dictionary _scripts = new(); private EntityEffectController? _effects; private LiveEntityLightController? _liveLights; internal Fixture() { Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); AddEmptyPhysicsLandblock(Engine, 0x0101FFFFu, 0f, 0f); AddEmptyPhysicsLandblock(Engine, 0x0102FFFFu, 0f, 192f); var emitterRegistry = new EmitterDescRegistry(); emitterRegistry.Register(PersistentEmitter()); Particles = new ParticleSystem(emitterRegistry, new Random(42)); ParticleSink = new ParticleHookSink(Particles, Poses); Lighting = new LightingHookSink(Lights, Poses); Router.Register(ParticleSink); Router.Register(Lighting); _scripts.Add(ScriptDid, PersistentScript()); Runner = new PhysicsScriptRunner( did => _scripts.TryGetValue(did, out DatPhysicsScript? script) ? script : null, Router, randomUnit: () => 0.5, canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true); Runtime = new LiveEntityRuntime( Spatial, new DelegateLiveEntityResourceLifecycle( entity => { Assert.True(RenderOwners.Add(entity.Id)); RenderRegisterCount++; Poses.PublishMeshRefs(entity); ParticleSink.SetEntityRenderPass(entity.Id, ParticleRenderPass.Scene); }, entity => { Runner.StopAllForEntity(entity.Id); ParticleSink.StopAllForEntity(entity.Id, fadeOut: false); Poses.Remove(entity.Id); Assert.True(RenderOwners.Remove(entity.Id)); RenderUnregisterCount++; }), record => { var cleanups = new List { () => _effects?.OnLiveEntityUnregistered(record), }; if (record.WorldEntity is { } entity) { cleanups.Add(() => Engine.ShadowObjects.Deregister(entity.Id)); cleanups.Add(() => _liveLights?.Forget(entity.Id)); } LiveEntityTeardown.Run(cleanups); }); Effects = new EntityEffectController( Runtime, Runner, new PhysicsScriptTableResolver(_ => null), Poses); _effects = Effects; Router.Register(Effects); Projectiles = new ProjectileController(Runtime, Engine); LiveLights = new LiveEntityLightController( Runtime, Poses, Lighting, setupId => setupId == SetupDid ? Setup : null); _liveLights = LiveLights; Runtime.ProjectionVisibilityChanged += OnProjectionVisibilityChanged; } internal GpuWorldState Spatial { get; } = new(); internal PhysicsEngine Engine { get; } = new(); internal EntityEffectPoseRegistry Poses { get; } = new(); internal AnimationHookRouter Router { get; } = new(); internal ParticleSystem Particles { get; } internal ParticleHookSink ParticleSink { get; } internal LightManager Lights { get; } = new(); internal LightingHookSink Lighting { get; } internal PhysicsScriptRunner Runner { get; } internal LiveEntityRuntime Runtime { get; } internal EntityEffectController Effects { get; } internal ProjectileController Projectiles { get; } internal LiveEntityLightController LiveLights { get; } internal Setup Setup { get; } = ProjectileSetup(); internal HashSet RenderOwners { get; } = new(); internal int RenderRegisterCount { get; private set; } internal int RenderUnregisterCount { get; private set; } internal LiveEntityRecord Spawn(uint guid, ushort generation, int ordinal) { Vector3 position = new( 8f + (ordinal % 12) * 6f, 8f + (ordinal / 12) * 6f, 10f); WorldSession.EntitySpawn spawn = MissileSpawn(guid, generation, position); LiveEntityRegistrationResult registration = Runtime.RegisterLiveEntity(spawn); LiveEntityRecord record = Assert.IsType(registration.Record); Runtime.SetEffectProfile( guid, EntityEffectProfile.CreateLive(new Setup(), spawn.Physics!.Value)); WorldEntity entity = Assert.IsType(Runtime.MaterializeLiveEntity( guid, CellA, localId => new WorldEntity { Id = localId, ServerGuid = guid, SourceGfxObjOrSetupId = SetupDid, Position = position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = CellA, })); Assert.True(Effects.OnLiveEntityReady(guid)); Assert.True(Projectiles.TryBind(record, Setup, 0.0, 1, 1)); Engine.ShadowObjects.Register( entity.Id, SetupDid, entity.Position, entity.Rotation, radius: 0.1f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0x01010000u, state: (uint)MissileState, seedCellId: CellA, isStatic: false); Assert.True(LiveLights.Register(guid)); return record; } private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible) { if (record.WorldEntity is { } entity) ParticleSink.SetEntityPresentationVisible(entity.Id, visible); } public void Dispose() { Runtime.Clear(); Effects.ClearNetworkState(); Runtime.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged; LiveLights.Dispose(); } } private sealed class RecallPortalFixture : IDisposable { private const uint Guid = 0x75000001u; private const uint CellOne = 0x01010001u; private const uint CellTwo = 0x01020001u; private const uint RecallScriptDid = 0x3300BB01u; private const uint IdleAnimationDid = 0x0300BB01u; private const uint RecallAnimationDid = 0x0300BB02u; private const uint Style = 0x8000003Du; private const uint ReadyMotion = 0x41000003u; private readonly AnimationHookFrameQueue _hookQueue; private readonly TestRemoteMotion _remote = new(); private uint _currentCell = CellOne; private uint _destinationCell; private ushort _stateSequence = 1; private bool _cleared; internal RecallPortalFixture() { Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var emitterRegistry = new EmitterDescRegistry(); emitterRegistry.Register(PersistentEmitter()); Particles = new ParticleSystem(emitterRegistry, new Random(73)); ParticleSink = new ParticleHookSink(Particles, Poses); Router.Register(ParticleSink); DatPhysicsScript recallScript = RecallScript(); Runner = new PhysicsScriptRunner( did => did == RecallScriptDid ? recallScript : null, Router, randomUnit: () => 0.5); EntityEffectController? effects = null; LiveEntityPresentationController? presentation = null; RemoteTeleportController? teleport = null; Runtime = new LiveEntityRuntime( Spatial, new DelegateLiveEntityResourceLifecycle( entity => { Poses.PublishMeshRefs(entity); ParticleSink.SetEntityRenderPass(entity.Id, ParticleRenderPass.Scene); }, entity => { Runner.StopAllForEntity(entity.Id); ParticleSink.StopAllForEntity(entity.Id, fadeOut: false); Poses.Remove(entity.Id); }), record => { effects?.OnLiveEntityUnregistered(record); presentation?.Forget(record); teleport?.Forget(record); if (record.WorldEntity is { } entity) Engine.ShadowObjects.Deregister(entity.Id); }); Effects = effects = new EntityEffectController( Runtime, Runner, new PhysicsScriptTableResolver(_ => null), Poses); Router.Register(Effects); WorldSession.EntitySpawn spawn = RecallSpawn(Guid, CellOne); Runtime.RegisterLiveEntity(spawn); Runtime.SetEffectProfile( Guid, EntityEffectProfile.CreateLive(new Setup(), spawn.Physics!.Value)); Entity = Runtime.MaterializeLiveEntity( Guid, CellOne, localId => new WorldEntity { Id = localId, ServerGuid = Guid, SourceGfxObjOrSetupId = SetupDid, Position = new Vector3(10f, 10f, 10f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = CellOne, })!; Assert.True(Effects.OnLiveEntityReady(Guid)); _remote.Body.SnapToCell(CellOne, Entity.Position, Entity.Position); _remote.CellId = CellOne; Runtime.SetRemoteMotionRuntime(Guid, _remote); Engine.ShadowObjects.Register( Entity.Id, SetupDid, Entity.Position, Entity.Rotation, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0x0101FFFFu, collisionType: ShadowCollisionType.Cylinder, cylHeight: 1.835f, seedCellId: CellOne, isStatic: false); Presentation = presentation = new LiveEntityPresentationController( Runtime, Engine.ShadowObjects, (_, type, _) => { TypedScripts.Add(type); return true; }, liveCenter: () => (1, 1)); Assert.True(Presentation.OnLiveEntityReady(Guid)); Teleport = teleport = new RemoteTeleportController( Engine, Runtime, (_, _) => (0.48f, 1.835f), (position, _) => position, (_, _, _) => { }, (guid, generation, defer) => Presentation.CompleteAuthoritativePlacement(guid, generation, defer), (guid, generation) => Presentation.BeginAuthoritativePlacement(guid, generation), resolvePlacement: (position, cell, _, _, _, _) => new ResolveResult( position, cell, IsOnGround: true, InContact: true, OnWalkable: true, ContactPlane: new Plane(Vector3.UnitZ, 0f), ContactPlaneCellId: cell)); Runtime.ProjectionVisibilityChanged += OnProjectionVisibilityChanged; _hookQueue = new AnimationHookFrameQueue(Router, Poses); } internal GpuWorldState Spatial { get; } = new(); internal PhysicsEngine Engine { get; } = new(); internal EntityEffectPoseRegistry Poses { get; } = new(); internal AnimationHookRouter Router { get; } = new(); internal ParticleSystem Particles { get; } internal ParticleHookSink ParticleSink { get; } internal PhysicsScriptRunner Runner { get; } internal LiveEntityRuntime Runtime { get; } internal EntityEffectController Effects { get; } internal WorldEntity Entity { get; } internal LiveEntityPresentationController Presentation { get; } internal RemoteTeleportController Teleport { get; } internal List TypedScripts { get; } = []; internal int RecallHookCount { get; private set; } internal void PlayRecallMotion(double gameTime) { AnimationSequencer recallSequencer = BuildRecallSequencer(); recallSequencer.SetCycle(Style, ReadyMotion); recallSequencer.PlayAction(RecallMotion); recallSequencer.Advance(0.15f); IReadOnlyList hooks = recallSequencer.ConsumePendingHooks(); RecallHookCount += hooks.Count(hook => hook is CallPESHook call && (uint)call.PES == RecallScriptDid); _hookQueue.Capture(Entity.Id, recallSequencer, hooks); _hookQueue.Drain(); Runner.Tick(gameTime); Particles.Tick(0.01f); } internal void ApplyHidden() { Assert.True(Runtime.TryApplyState( new SetState.Parsed( Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), InstanceSequence: 1, StateSequence: ++_stateSequence), out _, out _)); Assert.True(Presentation.OnStateAccepted(Guid)); } internal void BeginDeferredTeleport() { _destinationCell = _currentCell == CellOne ? CellTwo : CellOne; Assert.False(Spatial.IsLoaded((_destinationCell & 0xFFFF0000u) | 0xFFFFu)); Teleport.BeginPlacement(Guid, generation: 1); Assert.True(Runtime.RebucketLiveEntity(Guid, _destinationCell)); RemoteTeleportController.Result result = Teleport.TryApply( _remote, Entity, new Vector3(Entity.Position.X + 3f, Entity.Position.Y, Entity.Position.Z), _destinationCell, Entity.Position, Quaternion.Identity, gameTime: _stateSequence, destinationProjectionVisible: false, generation: 1, positionSequence: 1); Assert.True(result.Applied); Assert.False(result.ContactResolved); } internal void LoadDestination() { Spatial.AddLandblock(EmptyLandblock( (_destinationCell & 0xFFFF0000u) | 0xFFFFu)); } internal void ApplyVisible() { Assert.True(Runtime.TryApplyState( new SetState.Parsed( Guid, (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: 1, StateSequence: ++_stateSequence), out _, out _)); Assert.True(Presentation.OnStateAccepted(Guid)); } internal void UnloadPriorLandblock() { uint priorLandblock = (_currentCell & 0xFFFF0000u) | 0xFFFFu; Spatial.RemoveLandblock(priorLandblock); _currentCell = _destinationCell; } internal void Clear() { if (_cleared) return; _cleared = true; Runtime.Clear(); Effects.ClearNetworkState(); _hookQueue.Clear(); Particles.Tick(0.01f); } public void Dispose() { Clear(); Runtime.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged; Teleport.Dispose(); Presentation.Dispose(); } private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible) { if (record.WorldEntity is { } entity) ParticleSink.SetEntityPresentationVisible(entity.Id, visible); } private static AnimationSequencer BuildRecallSequencer() { var setup = new Setup(); setup.Parts.Add(0x01000001u); setup.DefaultScale.Add(Vector3.One); var table = new MotionTable { DefaultStyle = (DRWMotionCommand)Style, }; table.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)ReadyMotion; int readyKey = unchecked((int)((Style << 16) | (ReadyMotion & 0xFFFFFFu))); table.Cycles[readyKey] = MotionData(IdleAnimationDid); var links = new MotionCommandData(); links.MotionData[unchecked((int)RecallMotion)] = MotionData(RecallAnimationDid); table.Links[readyKey] = links; Animation idle = Animation(frames: 2); Animation recall = Animation(frames: 2); recall.PartFrames[0].Hooks.Add(new CallPESHook { Direction = AnimationHookDir.Forward, PES = RecallScriptDid, Pause = 0f, }); var loader = new RecallAnimationLoader(); loader.Add(IdleAnimationDid, idle); loader.Add(RecallAnimationDid, recall); return new AnimationSequencer(setup, table, loader); } private static MotionData MotionData(uint animationDid) { var data = new MotionData(); QualifiedDataId id = animationDid; data.Anims.Add(new AnimData { AnimId = id, LowFrame = 0, HighFrame = -1, Framerate = 10f, }); return data; } private static Animation Animation(int frames) { var animation = new Animation(); for (int i = 0; i < frames; i++) { var frame = new AnimationFrame(1u); frame.Frames.Add(new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }); animation.PartFrames.Add(frame); } return animation; } private sealed class RecallAnimationLoader : IAnimationLoader { private readonly Dictionary _animations = new(); internal void Add(uint id, Animation animation) => _animations[id] = animation; public Animation? LoadAnimation(uint id) => _animations.TryGetValue(id, out Animation? animation) ? animation : null; } private sealed class TestRemoteMotion : ILiveEntityRemotePlacementRuntime { private uint _cellId; private Func? _readCell; private Action? _writeCell; public PhysicsBody Body { get; } = new(); 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 Quaternion LastShadowSyncOrientation { get; set; } public void BindCanonicalCell(Func read, Action write) { _readCell = read; _writeCell = write; } public void HitGround() { } public void LeaveGround() { } } } private static DatPhysicsScript PersistentScript() { var script = new DatPhysicsScript(); script.ScriptData.Add(new PhysicsScriptData { StartTime = 0.0, Hook = new CreateParticleHook { EmitterInfoId = EmitterDid, PartIndex = uint.MaxValue, EmitterId = 1u, Offset = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }, }, }); script.ScriptData.Add(new PhysicsScriptData { StartTime = 60.0, Hook = new DestroyParticleHook { EmitterId = 1u }, }); return script; } private static DatPhysicsScript RecallScript() { var script = new DatPhysicsScript(); script.ScriptData.Add(new PhysicsScriptData { StartTime = 0.0, Hook = new CreateParticleHook { EmitterInfoId = EmitterDid, PartIndex = uint.MaxValue, EmitterId = 7u, Offset = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }, }, }); return script; } private static EmitterDesc PersistentEmitter() => new() { DatId = EmitterDid, Type = AcDream.Core.Vfx.ParticleType.Still, EmitterKind = ParticleEmitterKind.BirthratePerSec, MaxParticles = 2, InitialParticles = 1, TotalParticles = 0, TotalDuration = 0f, Lifespan = 120f, LifetimeMin = 120f, LifetimeMax = 120f, Birthrate = 0f, StartSize = 0.25f, EndSize = 0.25f, StartAlpha = 1f, EndAlpha = 1f, }; private static Setup ProjectileSetup() => new() { Spheres = { new Sphere { Origin = new Vector3(0f, -0.165f, 0.1045f), Radius = 0.102f, }, }, Lights = { [0] = new LightInfo { ViewSpaceLocation = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }, Color = new ColorARGB { Red = 255, Green = 255, Blue = 255, Alpha = 255, }, Intensity = 1f, Falloff = 8f, }, }, }; private static WorldSession.EntitySpawn MissileSpawn( uint guid, ushort generation, Vector3 position) { var wirePosition = new CreateObject.ServerPosition( CellA, position.X, position.Y, position.Z, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: generation); var physics = new PhysicsSpawnData( RawState: (uint)MissileState, Position: wirePosition, Movement: null, AnimationFrame: null, SetupTableId: SetupDid, MotionTableId: null, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: 1f, Friction: 0f, Elasticity: 0f, Translucency: null, Velocity: new Vector3(2f, 0f, 0f), Acceleration: null, AngularVelocity: Vector3.Zero, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, wirePosition, SetupDid, Array.Empty(), Array.Empty(), Array.Empty(), BasePaletteId: null, ObjScale: 1f, Name: "Stress missile", ItemType: null, MotionState: null, MotionTableId: null, PhysicsState: (uint)MissileState, Friction: 0f, Elasticity: 0f, InstanceSequence: generation, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private static WorldSession.EntitySpawn RecallSpawn(uint guid, uint cell) { var wirePosition = new CreateObject.ServerPosition( cell, 10f, 10f, 10f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 1, ServerControlledMove: 1, ForcePosition: 1, ObjDesc: 1, Instance: 1); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: wirePosition, Movement: null, AnimationFrame: null, SetupTableId: SetupDid, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: 1f, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, wirePosition, SetupDid, Array.Empty(), Array.Empty(), Array.Empty(), BasePaletteId: null, ObjScale: 1f, Name: "Recall stress owner", ItemType: null, MotionState: null, MotionTableId: 0x09000001u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private static void AddEmptyPhysicsLandblock( PhysicsEngine engine, uint landblockId, float offsetX, float offsetY) { var heights = new byte[81]; var table = new float[256]; Array.Fill(table, -1000f); engine.AddLandblock( landblockId, new TerrainSurface(heights, table), Array.Empty(), Array.Empty(), offsetX, offsetY); } private static LoadedLandblock EmptyLandblock(uint canonicalId) => new(canonicalId, new LandBlock(), Array.Empty()); }