using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using AcDream.App.Input; using AcDream.App.Net; using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.Update; using AcDream.App.World; using AcDream.Content; using AcDream.Content.Pak; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; using AcDream.Runtime.Session; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using DatReaderWriter.Types; // Alias the DatReaderWriter enum so it doesn't clash with // AcDream.Core.Physics.MotionCommand (a static class of uint constants). using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand; namespace AcDream.App.Tests.Physics; /// /// The OnPosition collapse's referee (contract §5, /// docs/research/2026-08-04-onposition-collapse-contract.md): every scenario /// below is driven for BOTH a 0x50xxxxxx player-range guid and a /// 0x8xxxxxxx creature-range guid through the SAME production /// entry point — /// never the extracted ApplyRemoteContactRouting seam directly. Before /// the collapse this class did not exist as a structural pattern: every /// existing test in this file's siblings drove exactly one guid range, which /// is precisely how the 4b-3 defects (A1's zero-arm leash regression, A2/R3's /// synthesized-velocity defect) survived — a defect in one hand-written copy /// left the other copy's tests green. After the collapse there is one code /// path, so running both guids through it is cheap; the point is it STAYS a /// pair so a future re-divergence (a guid-gated edit smuggled into the /// unified tail) fails a test instead of hiding. /// /// /// This class reuses the /// fixture's exact composition-only construction pattern (the controller's /// 67+ collaborators are wired only by SessionPlayerComposition in /// production, so a source pin would be the alternative — this drives the /// real class instead). /// /// public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests { private const uint SourceLandblock = 0xB1000000u; private const uint SourceCell = SourceLandblock | 0x0001u; private const uint DestinationLandblock = 0xB2000000u; private const uint DestinationCell = DestinationLandblock | 0x0001u; private static readonly Vector3 DestinationWorldOffset = new(192f, 0f, 0f); /// Player-range guid — 0x50xxxxxx, distinct from the /// fixture's own local-player NoopIdentitySource.ServerGuid /// (0x50000099u). private const uint PlayerGuid = 0x50007001u; /// Creature-range guid — 0x8xxxxxxx, matching the /// connected-gate evidence in the 4b-3 contract for how real creature /// guids arrive on the wire. private const uint CreatureGuid = 0x80007001u; private const float SpawnHeight = 7f; private const float FootSphereCenterLift = 0.48f; private static bool IsPlayer(uint guid) => guid == PlayerGuid; // ── Scenario 1: teleport commit ───────────────────────────────────── [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void TeleportCommit_BothGuids_ArmsOnceAndNeverInstallsVelocity(uint guid) { using var fixture = new Fixture(guid); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); var destination = new Vector3(12f, 14f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( destination, DestinationCell, teleportSequence: 5, guid: guid)); Assert.True(fixture.Lifetime.Entities.TryGetActive( guid, out RuntimeEntityRecord canonical)); Assert.NotNull(canonical.PhysicsBody); PhysicsBody body = canonical.PhysicsBody!; Vector3 resolved = destination + DestinationWorldOffset + new Vector3(0f, 0f, FootSphereCenterLift); Assert.Equal(resolved, body.Position); Assert.Equal(body.Position, fixture.Entity.Position); Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); Assert.Equal(DestinationCell, canonical.FullCellId); // The leash armed exactly once (D4: teleport arms on every outcome). Assert.NotNull(host.PositionManager.Constraint); // Row 6/invariant-6 (contract §5 scenario 1): retail's teleport // branch writes NO velocity at all, for either guid. Assert.False(fixture.Remote.HasServerVelocity); Assert.Equal(Vector3.Zero, fixture.Remote.ServerVelocity); // The shadow published at the resolved position. ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(body.Position, shadowEntry.Position); fixture.DrainPlacementFifo(); } // ── Scenario 2: landing packet (the preserved rows 2a/2b asymmetry) ─ [Fact] public void LandingPacket_PlayerGuid_QueueClearedNoShadowPublish_316Preserved() { using var fixture = new Fixture(PlayerGuid); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); // Mid-arc: body not in contact, wire IS grounded — the LANDING // scenario. Pre-populate the interp queue so the clear is observable. fixture.Remote.Body.TransientState = TransientStateFlags.Active; fixture.Remote.Interp.Enqueue( new Vector3(1f, 1f, SpawnHeight), Quaternion.Identity, isMovingTo: false, currentBodyPosition: new Vector3(50f, 50f, SpawnHeight), currentBodyOrientation: Quaternion.Identity); Assert.True(fixture.Remote.Interp.IsActive); Vector3 spawnShadowPos = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id).Position; var landingPos = new Vector3(12f, 14f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( landingPos, SourceCell, teleportSequence: 1, guid: PlayerGuid, isGrounded: true)); // Body snapped to the landing pose; entity synced from the resolved // body; armed exactly once. Assert.Equal(landingPos, fixture.Remote.Body.Position); Assert.Equal(landingPos, fixture.Entity.Position); Assert.Equal(SourceCell, fixture.Entity.ParentCellId); Assert.NotNull(host.PositionManager.Constraint); // Row 2a, PRESERVED for player guids: the interp queue IS cleared. Assert.False(fixture.Remote.Interp.IsActive); // Row 2b / #316, PRESERVED (NOT fixed): the shadow is NOT // republished for a player-guid landing — it stays at whatever it // was before this packet. ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(spawnShadowPos, shadowEntry.Position); Assert.NotEqual(landingPos, shadowEntry.Position); // AP-135's cell-adopt bookkeeping still ran (via the post-routing // wire-cell adopt, not suppressed for AirborneSnap). Assert.Equal(SourceCell, fixture.Remote.CellId); } [Fact] public void LandingPacket_CreatureGuid_ShadowPublishedQueueNotCleared() { using var fixture = new Fixture(CreatureGuid); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); fixture.Remote.Body.TransientState = TransientStateFlags.Active; fixture.Remote.Interp.Enqueue( new Vector3(1f, 1f, SpawnHeight), Quaternion.Identity, isMovingTo: false, currentBodyPosition: new Vector3(50f, 50f, SpawnHeight), currentBodyOrientation: Quaternion.Identity); Assert.True(fixture.Remote.Interp.IsActive); var landingPos = new Vector3(12f, 14f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( landingPos, SourceCell, teleportSequence: 1, guid: CreatureGuid, isGrounded: true)); Assert.Equal(landingPos, fixture.Remote.Body.Position); Assert.Equal(landingPos, fixture.Entity.Position); Assert.NotNull(host.PositionManager.Constraint); // Row 2a, PRESERVED for creature guids: the interp queue is NOT // cleared here (the per-tick AP-139 edge owns it — see the arm's // comment in OnPosition). Assert.True(fixture.Remote.Interp.IsActive); // Row 2b, the NPC/creature half: the shadow DOES publish at the // resolved body pose — this is the behaviour #316 says the player // half is missing. ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(landingPos, shadowEntry.Position); } // ── Scenario 3: wire-airborne, null-classified (login-window shape) ─ [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void WireAirborneNullClassified_BothGuids_WritesOnlyAP135Bookkeeping( uint guid) { using var fixture = new Fixture(guid, nullClassification: true); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); Vector3 spawnBodyPose = fixture.Remote.Body.Position; var wirePos = new Vector3(50f, 50f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( wirePos, SourceCell, teleportSequence: 1, guid: guid, isGrounded: false)); // No body write. Assert.Equal(spawnBodyPose, fixture.Remote.Body.Position); // No shadow republish. ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(spawnBodyPose, shadowEntry.Position); // No arm (round-2 architecture review's discriminator: Constraint is // lazily created only on a genuine arm). Assert.Null(host.PositionManager.Constraint); // Positive half (round-2 B1's lesson): AP-135's two writes DID // happen. Assert.Equal(SourceCell, fixture.Remote.CellId); Assert.Equal(wirePos, fixture.Remote.LastServerPos); Assert.NotEqual(0d, fixture.Remote.LastServerPosTime); } // ── Scenario 4: airborne no-op (NoPositionOperation, non-null route) ─ [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void AirborneNoOperation_BothGuids_WritesOnlyAP135BookkeepingNoArm( uint guid) { using var fixture = new Fixture(guid); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); // Contact SET so this is NOT the landing/AirborneSnap scenario — // this packet must classify NoPositionOperation via the WIRE // has_contact bit (update.IsGrounded), the classifier's OWN // predicate, independent of the body's own contact state. fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; Vector3 spawnBodyPose = fixture.Remote.Body.Position; var wirePos = new Vector3(50f, 50f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( wirePos, SourceCell, teleportSequence: 1, guid: guid, isGrounded: false)); Assert.Equal(spawnBodyPose, fixture.Remote.Body.Position); ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(spawnBodyPose, shadowEntry.Position); Assert.Null(host.PositionManager.Constraint); Assert.Equal(SourceCell, fixture.Remote.CellId); Assert.Equal(wirePos, fixture.Remote.LastServerPos); Assert.NotEqual(0d, fixture.Remote.LastServerPosTime); } // ── Scenario 5: near interpolate ───────────────────────────────────── [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void NearInterpolate_BothGuids_EnqueuesAndArmsOnce(uint guid) { using var fixture = new Fixture(guid); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; // AP-87 backstop: not firstUp, and further than the 4 m snap // threshold so this packet enqueues rather than snaps. fixture.Remote.Body.Position = new Vector3(2f, 2f, SpawnHeight); fixture.Remote.LastServerPos = fixture.Remote.Body.Position; fixture.Remote.LastServerPosTime = (DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds - 0.15; // Within AP-87's 4 m snap threshold (distance ~2.24 m) but well // outside InterpolationManager.Enqueue's 0.05 m "already-close" wipe // — a genuine enqueue, not a snap and not a no-op. var target = new Vector3(4f, 3f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: guid, isGrounded: true)); // Enqueued, not snapped: the body pose is unchanged (the per-tick // catch-up walks toward the queued target — this packet does not // move the body directly). Assert.Equal(new Vector3(2f, 2f, SpawnHeight), fixture.Remote.Body.Position); Assert.True(fixture.Remote.Interp.IsActive); Assert.NotNull(host.PositionManager.Constraint); // Row 3: the wire cell still adopts (near-interpolate is not a // placement arm, so TryAdoptWireCellAfterRouting does not suppress). Assert.Equal(SourceCell, fixture.Remote.CellId); // The render entity tracks the (unchanged) body, not the wire pose — // route 4a's generic-write suppression plus the unified tail's // resync from the resolved body. Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position); } // ── Scenario 6: far snap ───────────────────────────────────────────── [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void FarSnap_BothGuids_PlacesAndArmsOnEveryOutcome(uint guid) { using var fixture = new Fixture(guid); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); EntityPhysicsHost host = fixture.InstallHost(); Assert.Null(host.PositionManager.Constraint); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; Vector3 spawnPose = fixture.Entity.Position; // Far enough (>=96 m from the stub player controller's origin) via a // packet whose destination cell differs, WITHOUT advancing // TELEPORT_TS — the SetPositionSimple (far) classification, not // SetPosition (teleport/cell-less). var destination = new Vector3(12f, 14f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( destination, DestinationCell, teleportSequence: 1, guid: guid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( guid, out RuntimeEntityRecord canonical)); Assert.NotNull(canonical.PhysicsBody); PhysicsBody body = canonical.PhysicsBody!; Assert.NotEqual(spawnPose, body.Position); Assert.Equal(body.Position, fixture.Entity.Position); Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); Assert.Equal(DestinationCell, canonical.FullCellId); Assert.NotNull(host.PositionManager.Constraint); ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(body.Position, shadowEntry.Position); fixture.DrainPlacementFifo(); } // ── Scenario 7: sticky-suppressed steady-state (row 8's asymmetry) ── [Fact] public void StickySuppressed_CreatureGuid_RoutingSkippedButStillArmed() { using var fixture = new Fixture(CreatureGuid); EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009999u); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; Vector3 bodyBefore = fixture.Remote.Body.Position; var target = new Vector3(12f, 14f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: CreatureGuid, isGrounded: true)); // Routing never ran: body and queue are untouched. Assert.Equal(bodyBefore, fixture.Remote.Body.Position); Assert.False(fixture.Remote.Interp.IsActive); // D4: sticky-suppressed still arms, with UnroutedCatchUp. Assert.NotNull(host.PositionManager.Constraint); // Still stuck — nothing in this path unsticks it (only the teleport // hook does). Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId()); } [Fact] public void StickySuppressed_PlayerGuid_GateNeverAppliesRoutingRunsAnyway() { using var fixture = new Fixture(PlayerGuid); EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009999u); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; fixture.Remote.Body.Position = new Vector3(2f, 2f, SpawnHeight); fixture.Remote.LastServerPos = fixture.Remote.Body.Position; fixture.Remote.LastServerPosTime = (DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds - 0.15; var target = new Vector3(4f, 3f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: PlayerGuid, isGrounded: true)); // Row 8's named asymmetry: TS-44 never gates a player guid, so // routing ran despite sticky being armed — the interp queue is now // populated (or the body snapped), unlike the creature case above. Assert.True(fixture.Remote.Interp.IsActive); Assert.NotNull(host.PositionManager.Constraint); // Sticky itself is untouched by this non-teleport path (only the // teleport hook unsticks). Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId()); } // ── Scenario 8: NPC velocity-cycle (row 7/14's data-driven survivor) ─ [Fact] public void VelocityCycle_CreatureGuid_PlansACycleFromWireVelocity() { using var fixture = new Fixture(CreatureGuid, withAnimation: true); EntityPhysicsHost host = fixture.InstallHost(); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; // InitializeState() (fixture ctor) already seeded Ready via the // motion table's StyleDefaults — verify the precondition rather // than re-stating the seed. Assert.Equal( AcDream.Core.Physics.MotionCommand.Ready, fixture.Animated!.Sequencer!.CurrentMotion); var target = new Vector3(12f, 14f, SpawnHeight); var wireVelocity = new Vector3(0.7f, 0f, 0f); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: CreatureGuid, isGrounded: true, velocity: wireVelocity)); Assert.True(fixture.Remote.HasServerVelocity); Assert.Equal(wireVelocity, fixture.Remote.ServerVelocity); Assert.NotEqual( AcDream.Core.Physics.MotionCommand.Ready, fixture.Animated.Sequencer.CurrentMotion); _ = host; } [Fact] public void VelocityCycle_PlayerGuid_SequencerUntouchedByWireVelocity() { using var fixture = new Fixture(PlayerGuid, withAnimation: true); EntityPhysicsHost host = fixture.InstallHost(); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; Assert.Equal( AcDream.Core.Physics.MotionCommand.Ready, fixture.Animated!.Sequencer!.CurrentMotion); var target = new Vector3(12f, 14f, SpawnHeight); var wireVelocity = new Vector3(0.7f, 0f, 0f); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: PlayerGuid, isGrounded: true, velocity: wireVelocity)); // Row 6: the synth-velocity write is still write-only for players — // it happens (the unified NPC formula runs for every guid), but // nothing production reads it here either. Assert.True(fixture.Remote.HasServerVelocity); // Row 7/14: RemoteServerControlledVelocityCycle.Apply's internal // IsPlayerGuid return means the sequencer NEVER changes for a player // guid, regardless of the wire velocity supplied. Assert.Equal( AcDream.Core.Physics.MotionCommand.Ready, fixture.Animated.Sequencer.CurrentMotion); _ = host; } // ── Sabotage check (contract §5, one-time, manual) ────────────────── // // Performed by hand during implementation, not committed as a test (a // committed sabotage would defeat its own purpose): temporarily deleted // the `RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation` // call from the unified tail in `OnPosition` and re-ran this file. // Result: TeleportCommit_BothGuids (both InlineData rows), // LandingPacket_PlayerGuid, LandingPacket_CreatureGuid, // NearInterpolate_BothGuids (both rows), FarSnap_BothGuids (both rows), // and StickySuppressed_CreatureGuid / StickySuppressed_PlayerGuid all // failed — 9 of 9 non-airborne scenarios, both guid halves of every // dual-guid Theory. This is the structural fix for the 4b-3 defect // class: a sabotage that used to leave one guid's tests green now fails // both, because there is one call site left to sabotage. Reverted before // committing. private sealed class Fixture : IDisposable { internal RuntimeEntityObjectLifetime Lifetime { get; } internal LiveEntityRuntime Runtime { get; } internal LiveEntityNetworkUpdateController Controller { get; } internal RemoteServiceWindow ServiceWindow { get; } = new(); internal ShadowObjectRegistry Shadows { get; } internal WorldEntity Entity { get; } internal RemoteMotion Remote { get; private set; } = null!; internal LiveEntityAnimationState? Animated { get; private set; } private readonly GpuWorldState _spatial; private readonly uint _guid; private readonly bool _nullClassification; internal Fixture( uint guid, bool nullClassification = false, bool withAnimation = false) { _guid = guid; _nullClassification = nullClassification; var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( SourceLandblock, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); Lifetime = new RuntimeEntityObjectLifetime(engine); Lifetime.BindEventContext( static () => new RuntimeGenerationToken(1UL), static () => 1UL); Shadows = engine.ShadowObjects; var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( CanonicalLandblock(SourceLandblock), new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); _spatial = spatial; Runtime = new LiveEntityRuntime( spatial, new NoopResources(), NullLiveEntityRuntimeComponentLifecycle.Instance, Lifetime); var wirePosition = new CreateObject.ServerPosition( SourceCell, 10f, 10f, SpawnHeight, 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: 0x02000001u, 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); var spawn = new WorldSession.EntitySpawn( _guid, wirePosition, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "onposition-collapse-fixture", null, null, 0x09000001u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: 1, PositionSequence: 1, MovementSequence: 1, ServerControlSequence: 1, Physics: physics); LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn); Entity = record.WorldEntity ?? throw new InvalidOperationException( "fixture failed to materialize the remote entity"); Assert.True(Runtime.RebucketLiveEntity(_guid, SourceCell)); var remote = new RemoteMotion(); remote.Body.SnapToCell(SourceCell, Entity.Position, Entity.Position); remote.CellId = SourceCell; Runtime.SetRemoteMotionRuntime(_guid, remote); Remote = remote; Shadows.Register( Entity.Id, 0x02000001u, Entity.Position, Entity.Rotation, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceLandblock, collisionType: ShadowCollisionType.Cylinder, cylHeight: 1.835f, seedCellId: SourceCell, isStatic: false); var origin = new LiveWorldOriginState(); origin.SetPlaceholder( (int)((SourceLandblock >> 24) & 0xFFu), (int)((SourceLandblock >> 16) & 0xFFu)); LiveEntityAnimationRuntimeView animatedEntities; if (withAnimation) { var slot = new LiveEntityRuntimeSlot(); slot.Bind(Runtime); animatedEntities = new LiveEntityAnimationRuntimeView(slot); // A working (style, motion) -> animation table for BOTH // Ready and WalkForward, so SetCycle's dispatch through // PerformMovement genuinely succeeds and CurrentMotion // actually changes — a bare `new MotionTable()` has no // cycles, so every SetCycle silently no-ops // (AnimationSequencer.SetCycle: "Failed dispatch ... leaves // sequence AND state untouched"), which would make every // scenario 8 assertion vacuously true regardless of the // collapse. Pattern mirrors // tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs's // own `Fixtures.MakeMtable`. const uint animStyle = 0x8000003Du; const uint readyAnimId = 0x03000101u; const uint walkAnimId = 0x03000102u; var mt = new DatReaderWriter.DBObjs.MotionTable { DefaultStyle = (DRWMotionCommand)animStyle, }; mt.StyleDefaults[(DRWMotionCommand)animStyle] = (DRWMotionCommand)AcDream.Core.Physics.MotionCommand.Ready; mt.Cycles[(int)((animStyle << 16) | (AcDream.Core.Physics.MotionCommand.Ready & 0xFFFFFFu))] = MakeMotionData(readyAnimId); mt.Cycles[(int)((animStyle << 16) | (AcDream.Core.Physics.MotionCommand.WalkForward & 0xFFFFFFu))] = MakeMotionData(walkAnimId); var loader = new FakeAnimationLoader(); loader.Register(readyAnimId, MakeTwoFrameAnim()); loader.Register(walkAnimId, MakeTwoFrameAnim()); var setup = new DatReaderWriter.DBObjs.Setup(); setup.Parts.Add(0x01000000u); setup.DefaultScale.Add(Vector3.One); var sequencer = new AcDream.Core.Physics.AnimationSequencer( setup, mt, loader); sequencer.InitializeState(); Animated = new LiveEntityAnimationState { Entity = Entity, Setup = setup, Animation = new DatReaderWriter.DBObjs.Animation(), LowFrame = 0, HighFrame = 0, Framerate = 0f, Scale = 1f, PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), Sequencer = sequencer, }; animatedEntities[Entity.Id] = Animated; } else { animatedEntities = new LiveEntityAnimationRuntimeView( new LiveEntityRuntimeSlot()); } var remotePlacementDrive = new RuntimeRemotePlacementDriveController( Lifetime, new GameRuntimeClock(), new NoopCollisionSource(), ServiceWindow); var acceptedPositionDrive = new RuntimeAcceptedPositionDriveController( Lifetime, new GameRuntimeClock(), new NoopCollisionSource(), new LocalPlayerOutboundController(static (_, _, _, _, _, _) => { }), static () => new RuntimeGenerationToken(1UL), static () => 0x50000099u, static () => null, static () => false, static () => null); var identity = new NoopIdentitySource(); var deletion = new LiveEntityDeletionController( Runtime, Lifetime, new NoopTeardownCoordinator(), identity); var hydration = new LiveEntityHydrationController( Runtime, Lifetime, new object(), new NoopMaterializer(), new NoopRelationships(), new NoopReadyPublisher(), new AlwaysKnownOrigin(), new NoopNetworkSink(), new NoopTimestampPublisher(), identity, deletion); var entityEffects = new EntityEffectController( Runtime, new AcDream.Core.Vfx.PhysicsScriptRunner( static _ => null, new AcDream.Core.Physics.AnimationHookRouter(), randomUnit: static () => 0.5), new AcDream.Core.Vfx.PhysicsScriptTableResolver(static _ => null), new EntityEffectPoseRegistry()); Controller = new LiveEntityNetworkUpdateController( Runtime, Lifetime.Objects, hydration, entityEffects, new LiveEntityPresentationController( Runtime, Shadows, (_, _, _) => true, new LiveEntityPartArrayEnterWorldPort(_ => { })), new LiveEntityLightController( Runtime, new EntityEffectPoseRegistry(), new AcDream.Core.Lighting.LightingHookSink( new AcDream.Core.Lighting.LightManager(), new EntityEffectPoseRegistry()), static _ => null), new EquippedChildRenderController( new NoopDatReaderWriter(), new object(), Lifetime.Objects, Runtime, new EntityEffectPoseRegistry(), static _ => false, static (_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Superseded, null)), new ProjectileController(Runtime), animatedEntities, new RemoteMovementObservationTracker(), new RemotePhysicsUpdater( Lifetime.Physics, static (_, _) => (0.48f, 1.835f), static (_, _) => ( System.Collections.Immutable .ImmutableArray.Empty, 1f, 0.4f, 0.4f), static (_, _, _, _) => { }), new RemoteInboundMotionDispatcher( static (_, _, _) => false, static (_, _) => { }), new LiveEntityMotionRuntimeController( Runtime, new PhysicsDataCache(), static () => null, new AcDream.Core.Selection.SelectionState(), origin), engine, new NoopDatReaderWriter(), new NoopAnimationLoader(), combatTargetController: null, origin, new NoopTeleportSink(), _nullClassification ? new NoopLocalPlayerControllerSource() : new StubLocalPlayerControllerSource(), new LocalPlayerOutboundController(static (_, _, _, _, _, _) => { }), new NoopPhysicsHostSource(), identity, new FixedScriptTime(), new NoopSessionSource(), publishTimestamps: static (_, _) => { }, new NoopMovementTruthSink(), acceptedPositionDrive, remotePlacementDrive, worldDropProjection: null); } internal void PublishDestinationCollision() { var heights = new byte[81]; Array.Fill(heights, (byte)SpawnHeight); var heightTable = new float[256]; for (int index = 0; index < heightTable.Length; index++) heightTable[index] = index; Lifetime.Physics.ObserveLocalWorldFrame( SourceCell, teleportAdvanced: false); Lifetime.Physics.SetPosition.BeginCollisionGeneration( DestinationLandblock, 1UL); Lifetime.Physics.Engine.AddLandblock( DestinationLandblock, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), worldOffsetX: DestinationWorldOffset.X, worldOffsetY: DestinationWorldOffset.Y); Lifetime.Physics.SetPosition.CommitCollisionGeneration( DestinationLandblock, 1UL, ready: true); uint destinationCanonical = CanonicalLandblock(DestinationLandblock); if (!_spatial.IsLoaded(destinationCanonical)) { _spatial.AddLandblock(new LoadedLandblock( destinationCanonical, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); } } private static uint CanonicalLandblock(uint landblockId) => (landblockId & 0xFFFF0000u) | 0xFFFFu; internal WorldSession.EntityPositionUpdate Update( Vector3 destination, uint cellId, ushort teleportSequence, uint guid, bool isGrounded = true, Vector3? velocity = null) => new( guid, new CreateObject.ServerPosition( cellId, destination.X, destination.Y, destination.Z, 1f, 0f, 0f, 0f), Velocity: velocity, PlacementId: null, IsGrounded: isGrounded, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: teleportSequence, ForcePositionSequence: 0); private static DatReaderWriter.Types.MotionData MakeMotionData(uint animId) { var md = new DatReaderWriter.Types.MotionData(); md.Anims.Add(new DatReaderWriter.Types.AnimData { AnimId = (QualifiedDataId)animId, LowFrame = 0, HighFrame = -1, Framerate = 30f, }); return md; } private static DatReaderWriter.DBObjs.Animation MakeTwoFrameAnim() { var anim = new DatReaderWriter.DBObjs.Animation(); var pf0 = new DatReaderWriter.Types.AnimationFrame(1u); var pf1 = new DatReaderWriter.Types.AnimationFrame(1u); pf0.Frames.Add(new DatReaderWriter.Types.Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }); pf1.Frames.Add(new DatReaderWriter.Types.Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity, }); anim.PartFrames.Add(pf0); anim.PartFrames.Add(pf1); return anim; } private sealed class FakeAnimationLoader : IAnimationLoader { private readonly Dictionary _anims = new(); internal void Register(uint id, DatReaderWriter.DBObjs.Animation anim) => _anims[id] = anim; public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => _anims.TryGetValue(id, out var a) ? a : null; } internal EntityPhysicsHost ArmSticky(uint stickTargetGuid) { EntityPhysicsHost host = InstallHost(); host.PositionManager.StickTo(stickTargetGuid, radius: 1f, height: 1f); Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId()); return host; } internal EntityPhysicsHost InstallHost() { Assert.True(Runtime.TryGetRecord( _guid, out LiveEntityRecord liveRecord)); var host = new EntityPhysicsHost( _guid, getPosition: () => new AcDream.Core.Physics.Position( Remote.CellId, Remote.Body.Position, Remote.Body.Orientation), getVelocity: () => Remote.Body.Velocity, getRadius: () => 0.48f, inContact: () => Remote.Body.InContact, minterpMaxSpeed: () => null, curTime: () => 0d, physicsTimerTime: () => 0d, getObjectA: _ => null, handleUpdateTarget: _ => { }, interruptCurrentMovement: () => { }); Runtime.InstallPhysicsHost(liveRecord, host); Remote.MarkFullPhysicsHostBound(); return host; } internal void DrainPlacementFifo() { while (Lifetime.Physics.SetPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot head)) { if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token)) break; } } public void Dispose() => Lifetime.Dispose(); internal sealed class RemoteServiceWindow : IRuntimeRemotePlacementServiceWindow { private readonly HashSet _within = []; internal void Allow(uint landblockId) => _within.Add((landblockId & 0xFFFF0000u) | 0xFFFFu); public bool IsWithinServiceWindow(uint landblockId) => _within.Contains((landblockId & 0xFFFF0000u) | 0xFFFFu); } private sealed class NoopResources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } private sealed class NoopCollisionSource : IPreparedCollisionSource { public PreparedAssetPresence ProbeCollision( PakAssetType type, uint sourceFileId) => PreparedAssetPresence.Available; public PreparedCollisionReadResult ReadSetupCollision( uint sourceFileId, CancellationToken cancellationToken = default) => PreparedCollisionReadResult.Loaded( new FlatSetupCollision( System.Collections.Immutable .ImmutableArray.Empty, [new FlatCollisionSphere(Vector3.Zero, 0.48f)], height: 0f, radius: 0f, stepUpHeight: 0.4f, stepDownHeight: 0.4f)); public PreparedCollisionReadResult ReadGfxObjCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionReadResult ReadCellStructureCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionReadResult ReadEnvCellTopology( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionSourceStats CollisionStats => default; public void Dispose() { } } private sealed class NoopIdentitySource : ILocalPlayerIdentitySource { public uint ServerGuid => 0x50000099u; } private sealed class NoopTeardownCoordinator : ILiveEntityTeardownCoordinator { public void TearDown(LiveEntityRecord record) { } public void ForgetUnknownOwner(uint serverGuid) { } } private sealed class NoopMaterializer : ILiveEntityProjectionMaterializer { public bool TryMaterialize( RuntimeEntityRecord expectedCanonical, WorldSession.EntitySpawn canonicalSpawn, LiveProjectionPurpose purpose, ulong expectedCreateIntegrationVersion, AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null) => throw new InvalidOperationException( "The fixture pre-materializes the remote entity; " + "TryMaterialize should never be reached for an " + "already-projected accepted Position."); public void ResetSessionState() { } } private sealed class NoopRelationships : ILiveEntityRelationshipProjection { public void OnSpawn(WorldSession.EntitySpawn spawn) { } public void OnParent(ParentEvent.Parsed update) { } public void OnCreateParentAccepted(CreateParentUpdate update) { } public AcDream.App.Rendering.ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) => AcDream.App.Rendering.ChildUnparentDisposition.NotAttached; public bool TryApplyAttachedAppearance( LiveEntityRecord record, ulong objDescAuthorityVersion) => false; } private sealed class NoopReadyPublisher : ILiveEntityReadyPublisher { public bool Publish(LiveEntityReadyCandidate candidate) => true; } private sealed class AlwaysKnownOrigin : ILiveEntityWorldOriginCoordinator { public bool IsKnown => true; public LiveEntityOriginInitialization TryInitialize( WorldSession.EntitySpawn spawn) => new(true, []); } private sealed class NoopNetworkSink : ILiveEntityNetworkUpdateSink { public void ApplySameGeneration(SameGenerationCreateObjectEvents events) { } } private sealed class NoopTimestampPublisher : IAcceptedLocalPhysicsTimestampPublisher { public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps) { } } private sealed class NoopDatReaderWriter : IDatReaderWriter { private readonly StubDatabase _portal = new(); private readonly StubDatabase _highRes = new(); private readonly StubDatabase _language = new(); private readonly StubDatabase _cell = new(); public string SourceDirectory => string.Empty; public IDatDatabase Portal => _portal; public IDatDatabase Cell => _cell; public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; public IDatDatabase Local => _language; public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); public int PortalIteration => 0; public int CellIteration => 0; public int HighResIteration => 0; public int LanguageIteration => 0; public bool TryGetFileBytes( uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave( uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => default; public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public void Dispose() { } } private sealed class StubDatabase : IDatDatabase { public DatDatabase Db => throw new NotSupportedException(); public int Iteration => 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public bool TryGetFileBytes( uint fileId, [MaybeNullWhen(false)] out byte[] value) { value = null; return false; } public bool TryGetFileBytes( uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public void Dispose() { } } private sealed class NoopAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint id) => null; } private sealed class NoopTeleportSink : ILocalPlayerTeleportNetworkSink { public void OnTeleportStarted(uint sequence) { } public void OfferDestination( RuntimeTeleportDestination destination, bool teleportTimestampAdvanced) { } public void ResetSession() { } public void ResetGenerationPresentation() { } } private sealed class StubLocalPlayerControllerSource : IRuntimeLocalPlayerControllerSource { public PlayerMovementController? Controller { get; } = new PlayerMovementController(new PhysicsEngine()); } private sealed class NoopLocalPlayerControllerSource : IRuntimeLocalPlayerControllerSource { public PlayerMovementController? Controller => null; } private sealed class NoopPhysicsHostSource : ILocalPlayerPhysicsHostSource { public EntityPhysicsHost? Host => null; } private sealed class FixedScriptTime : IPhysicsScriptTimeSource { public double CurrentScriptTime => 1_700_000_000d; } private sealed class NoopSessionSource : ILiveWorldSessionSource { public WorldSession? CurrentSession => null; } private sealed class NoopMovementTruthSink : IMovementTruthDiagnosticSink { public void OnOutbound( string kind, uint sequence, MovementResult result, Vector3 wirePosition, uint wireCellId, byte contactByte) { } public void OnServerEcho( WorldSession.EntityPositionUpdate update, Vector3 serverWorldPosition) { } public void ResetSession() { } } } }