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 0: C5b's untested load-bearing claim ─────────────────── /// /// C5b (#275) review, findings L1/L2. C5b made the steady-state merge stop /// stamping the wire cell and justified it with a claim it never tested: /// "production installs the bucket at W2 in the same call (verified: no /// return between the recovery call and W2 is conditioned on /// IsSpatiallyProjected or FullCellId)". The commit also asserted "no /// fixture covers pickup at that layer" — this file drives the real /// at ~26 call /// sites, and every W2 assertion the commit added hand-calls /// RebucketLiveEntity/CommitRebucket itself, so nothing /// drove the sequence end-to-end. This does. /// /// The scenario is the leave-world/re-entry edge the production /// comment at LiveEntityNetworkUpdateController.cs:2073-2077 names: /// the record keeps WorldEntity as its logical/render-resource /// owner while IsSpatiallyProjected is false, and "a fresh retail /// Position is the re-entry edge; testing only for the retained object /// reference leaves dropped inventory permanently invisible after /// InventoryPutObjectIn3D". The materializer DECLINES (the post-C5b /// self-projection state — DatLiveEntityProjectionMaterializer.cs:767 /// now sees a canonical cell the merge did not advance), so the only /// things that can restore the bucket and the cell are the two wire-cell /// channels AD-60 says deliberately survive C5b. /// /// Measured at the review, and NOT what C5b's commit message /// assumed: W2 and W3 are REDUNDANT here. Sabotaging W2 alone (both /// "adopt the committed cell instead of the wire cell" and "skip the /// rebucket entirely") leaves this test — and every other test in this /// file — GREEN, because the post-routing wire-cell adopt /// (TryAdoptWireCellAfterRouting, W3/AP-135) writes /// RemoteMotion.CellId, which reads through to canonical /// FullCellId via CommitCanonicalCell, whose graphical /// CellCommitted recovery re-installs the bucket. Only removing /// BOTH surviving channels turns this red — and it is then the ONLY red /// in the file. So what this test pins is AD-60's actual surviving claim /// ("two wire-cell writers deliberately remain downstream of the merge"), /// not W2 in isolation. Naming it after W2 would have been a third /// contract asserting a mechanism that is not the one doing the work. /// /// /// Both guid classes per this file's own discipline. /// [Theory] [InlineData(PlayerGuid)] [InlineData(CreatureGuid)] public void WithdrawnProjection_AcceptedPositionRestoresBucketAndWireCell( uint guid) { using var fixture = new Fixture(guid, decliningMaterializer: true); Assert.True(fixture.Runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.True(record.IsSpatiallyProjected); Assert.Equal(SourceCell, record.FullCellId); // The withdrawal: render bucket gone, logical record + WorldEntity // retained. This is what makes RequiresSpatialProjectionRecovery true // on the next accepted Position. Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(guid)); Assert.False(record.IsSpatiallyProjected); Assert.NotNull(record.WorldEntity); Assert.Equal(SourceCell, record.FullCellId); const uint WireCell = SourceLandblock | 0x0002u; fixture.Controller.OnPosition(fixture.Update( new Vector3(13f, 15f, SpawnHeight), WireCell, teleportSequence: 1, guid: guid)); // The recovery branch really ran and really declined — without this // the assertions below could pass on an entity that never needed // recovering at all. Assert.True(fixture.Lifetime.Entities.TryGetActive( guid, out RuntimeEntityRecord canonical)); // The packet really was accepted and really did reach the recovery // branch, which really did decline — without these the cell assertions // below could pass on a rejected packet, or on an entity that never // needed recovering at all. Assert.Equal( WireCell, canonical.Snapshot.Position!.Value.LandblockId); Assert.True(fixture.MaterializerDeclined); // W2 is what closes both halves. Assert.True(record.IsSpatiallyProjected); Assert.Equal(WireCell, record.FullCellId); Assert.Equal(WireCell, canonical.FullCellId); Assert.Equal(WireCell, fixture.Entity.ParentCellId); } // ── 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. // ── C4 route 5 (A5 fix): missile matrix ───────────────────────────── // // The architecture review's FAIL-level finding: zero App tests drove // OnPosition with a missile packet, so A1 (App discards the seam's // status) and A2 (an unbound missile's Position silently dropped) both // lived in the ~35 lines of "thin glue" the implementer argued were // covered by two well-tested layers. These tests extend THIS file's own // fixture — never a lighter one — with a genuine Missile-flagged, // RuntimeProjectile-bound, RemoteMotion-less incarnation, and assert the // SAME observable surface the remote scenarios above assert: body/entity // position and ParentCellId (that pair is the exact A1 assertion), plus // the projectile-specific half — no RemoteMotion is EVER created // (invariant 8's mutual exclusion) and no early wire-pose write occurs. // Destinations for the commit scenarios are airborne (well above // PublishDestinationCollision's flat terrain), isolating the placement // assertions from the shared pipeline's ordinary ground-contact response // (Claim 4's confound, restated here for the App layer). private const uint MissileGuid = 0x80007101u; private static readonly Vector3 MissileAirborneDestination = new(12f, 14f, SpawnHeight + 10f); [Fact] public void MissileTeleportCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody() { using var fixture = new Fixture(MissileGuid, isMissile: true); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 5, guid: MissileGuid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); Assert.NotNull(canonical.Projectile); PhysicsBody body = canonical.PhysicsBody!; Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset; Assert.Equal(resolved, body.Position); Assert.Equal(body.Position, fixture.Entity.Position); // A1's own assertion: ParentCellId agrees with the RESOLVED body's // OWN cell — the review's concrete wrong-cell scenario, checked // positively here rather than only on the no-op scenarios below. Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId); Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); // D-P4: the collision table was force-ended (no seeded owner here to // assert a 1->0 transition — that half is covered at // tests/AcDream.Runtime.Tests — but the call must not throw or // leave the entity uncollidable long-term; Tick below proves it is // still a live, ordinary object). ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); Assert.Equal(body.Position, shadowEntry.Position); fixture.DrainPlacementFifo(); } [Fact] public void MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody() { using var fixture = new Fixture(MissileGuid, isMissile: true); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); PhysicsBody body = canonical.PhysicsBody!; Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset; Assert.Equal(resolved, body.Position); Assert.Equal(body.Position, fixture.Entity.Position); Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId); Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); fixture.DrainPlacementFifo(); } /// /// B1 EnvCell id — an indoor-format low word (0x100+, outside /// LandDefs.AdjustToOutside's 1-0x40 outdoor range) staged onto /// the body BEFORE the store-path dispatch below. This is what makes /// the regression assertion actually discriminate: for an OUTDOOR /// source/destination pair, PhysicsBody.Position's setter /// delta-syncs CellPosition through AdjustToOutside and /// happens to re-derive the correct destination landblock anyway (pure /// geometry, no collision data needed) — so a wrong /// body.CellPosition.ObjCellId read would coincidentally agree /// with record.FullCellId and the test would pass either way. /// An INDOOR source cell takes SyncCellPositionDelta's OTHER /// branch (PhysicsBody.cs:295-300): it carries the position delta /// but never re-derives the cell id, so body.CellPosition.ObjCellId /// stays PINNED at the stale indoor source cell through the store path. /// record.FullCellId has no such blind spot — it is merged from /// the wire unconditionally — so this is the scenario where the two /// expressions genuinely diverge and MAJOR 1's revert is provable. /// private const uint IndoorSourceCell = SourceLandblock | 0x0100u; /// /// Residual 1 close (round-2 review): the STORE path — Refused — /// at the App layer, closing both the coverage gap AND standing as the /// regression test for MAJOR 1 (the ParentCellId revert). The /// destination is deliberately left outside the service window /// ( is never called for /// ), so /// CanAttemptDestination refuses before the engine ever runs and /// StoreAcceptedDestinationPose resolves the destination through /// Runtime's own world frame instead of a commit. Refused is /// still a storing (A1-admitted) outcome, so the App-level presentation /// sync runs — the entity's position moves to the DESTINATION and its /// ParentCellId tracks record.FullCellId, never the stale /// INDOOR source cell a body.CellPosition.ObjCellId read would /// have produced (see 's doc comment for /// why an outdoor source cell would not have discriminated here). /// /// /// C5b (#275), trap T6: record.FullCellId is now the COMMITTED /// cell, so a REFUSED placement leaves it at the source. The missile arm /// returns before the OnPosition generic tail, so its residency is /// placement-receipt-driven and nothing else — which is retail /// (a projectile's cell comes from SetPosition, full stop). The /// MAJOR 1 regression check is unchanged in substance and is asserted /// directly below as the identity it always meant: /// ParentCellId == record.FullCellId != body.CellPosition.ObjCellId. /// /// [Fact] public void MissileFarRefused_StorePathStillMovesEntityToDestinationParentCellIdAgreesWithCommittedCell() { using var fixture = new Fixture(MissileGuid, isMissile: true); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonicalBeforeStage)); PhysicsBody stagedBody = canonicalBeforeStage.PhysicsBody!; stagedBody.SnapToCell( IndoorSourceCell, stagedBody.Position, stagedBody.Position); Assert.Equal(IndoorSourceCell, stagedBody.CellPosition.ObjCellId); fixture.PublishDestinationCollision(); // Deliberately NOT allowed — CanAttemptDestination refuses. fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); Assert.NotNull(canonical.Projectile); PhysicsBody body = canonical.PhysicsBody!; Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset; Assert.Equal(resolved, body.Position); Assert.True(body.InWorld); // The store fallback never re-derives an INDOOR cell id — confirms // the divergence this test is built to exercise actually occurred. Assert.Equal(IndoorSourceCell, body.CellPosition.ObjCellId); // The exact B1 regression check: ParentCellId IS record.FullCellId — // never body.CellPosition.ObjCellId, which just asserted it is STILL // the stale indoor source cell. Assert.Equal(body.Position, fixture.Entity.Position); Assert.Equal(canonical.FullCellId, fixture.Entity.ParentCellId); Assert.NotEqual( body.CellPosition.ObjCellId, fixture.Entity.ParentCellId); // C5b: the refused placement committed nothing, so the committed cell // is still the source. The wire cell is NOT residency here. Assert.Equal(SourceCell, canonical.FullCellId); Assert.NotEqual(DestinationCell, fixture.Entity.ParentCellId); fixture.DrainPlacementFifo(); } [Fact] public void MissileNear_NoOp_BodyUnchangedNoRemoteMotionNoWirePoseWrite() { using var fixture = new Fixture(MissileGuid, isMissile: true); Vector3 spawnPose = fixture.Entity.Position; var target = new Vector3(4f, 3f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( target, SourceCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); PhysicsBody body = canonical.PhysicsBody!; Assert.Equal(spawnPose, body.Position); // A1's regression, asserted directly: no early wire-pose write — // the render entity was never moved to `target`. Assert.Equal(spawnPose, fixture.Entity.Position); Assert.NotEqual(target, fixture.Entity.Position); } [Fact] public void MissileAirborne_NoOp_BodyUnchangedNoRemoteMotion() { using var fixture = new Fixture(MissileGuid, isMissile: true); Vector3 spawnPose = fixture.Entity.Position; var wirePos = new Vector3(50f, 50f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( wirePos, SourceCell, teleportSequence: 1, guid: MissileGuid, isGrounded: false)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); PhysicsBody body = canonical.PhysicsBody!; Assert.Equal(spawnPose, body.Position); Assert.Equal(spawnPose, fixture.Entity.Position); } [Fact] public void MissileNullClassification_Swallowed_NoRemoteMotionNoWriteNoPredictionChange() { using var fixture = new Fixture( MissileGuid, nullClassification: true, isMissile: true); Vector3 spawnPose = fixture.Entity.Position; ulong predictionBefore = fixture.Projectile!.PredictionAuthorityVersion; var wirePos = new Vector3(50f, 50f, SpawnHeight); fixture.Controller.OnPosition(fixture.Update( wirePos, SourceCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); Assert.Null(canonical.RemoteMotion); PhysicsBody body = canonical.PhysicsBody!; Assert.Equal(spawnPose, body.Position); Assert.Equal(spawnPose, fixture.Entity.Position); Assert.Equal( predictionBefore, fixture.Projectile.PredictionAuthorityVersion); } /// /// A2/R1's regression scenario, driven end to end: Missile bit set, but /// no RuntimeProjectile bound (TryBind refused, or has not run /// yet — ProjectileController.cs:160-166's unsupported-Setup /// case, or the pre-bind window). Retail places every non-player object /// unconditionally; the fixed classifier must classify this packet /// Remote and route it through the SAME generic remote placement path /// an ordinary remote uses — never the frozen silent drop the /// unconjoined discriminator produced. /// [Fact] public void MissileUnbound_FallsThroughToRemoteTail_TracksInsteadOfFreezing() { using var fixture = new Fixture(MissileGuid, isMissile: false); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); // Flip Missile AFTER construction, deliberately WITHOUT binding a // RuntimeProjectile — the unbound shape A2 names. fixture.Lifetime.Entities.SetFinalPhysicsState( canonical, canonical.FinalPhysicsState | PhysicsStateFlags.Missile); Assert.Null(canonical.Projectile); EntityPhysicsHost host = fixture.InstallHost(); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); Vector3 spawnPose = fixture.Entity.Position; fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); // Placed via the ordinary remote far-snap arm — not frozen, and // armed exactly like FarSnap_BothGuids above. Assert.NotEqual(spawnPose, fixture.Remote.Body.Position); Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position); Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); Assert.NotNull(host.PositionManager.Constraint); fixture.DrainPlacementFifo(); } /// /// A3/R2's adopted-body scenario: an ordinary remote (populated Interp /// queue, an armed ConstrainTo leash) whose Missile bit is later set by /// a State packet — ProjectileController.TryBind's shared-body /// branch adopts the SAME RemoteMotion/body rather than replacing /// it. Retail's teleport_hook per-manager guards are satisfied /// for this shape, so all six actions run; the App-level pre-dispatch /// hook call (mirroring the remote teleport arm's own /// RunRemoteTeleportHook wiring) must un-arm the leash and clear /// the queue before the placement, exactly like retail's ordering. /// [Fact] public void MissileAdoptedBody_TeleportCommit_UnConstrainsAndClearsInterpQueue() { using var fixture = new Fixture(MissileGuid, isMissile: false); EntityPhysicsHost host = fixture.InstallHost(); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; // Arm the leash and populate the queue directly — the pre-teleport // "live remote" state the adopted-body scenario requires. host.PositionManager.ConstrainTo( new AcDream.Core.Physics.Position( SourceCell, fixture.Remote.Body.Position, Quaternion.Identity), startDistance: 1f, maxDistance: 5f); Assert.NotNull(host.PositionManager.Constraint); fixture.Remote.Interp.Enqueue( fixture.Remote.Body.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: fixture.Remote.Body.Position); Assert.True(fixture.Remote.Interp.IsActive); // TryBind's shared-body branch: adopt the SAME body into a // RuntimeProjectile, and set Missile — the record now carries BOTH // a RemoteMotion and a bound projectile, exactly like production. Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); fixture.Lifetime.Entities.SetFinalPhysicsState( canonical, canonical.FinalPhysicsState | PhysicsStateFlags.Missile); fixture.Lifetime.Physics.BindProjectile( canonical, canonical.PhysicsBody!, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f)); Assert.NotNull(canonical.Projectile); Assert.NotNull(canonical.RemoteMotion); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 5, guid: MissileGuid, isGrounded: true)); // UnConstrain and StopInterpolating both ran. UnConstrain unmarks // IsConstrained rather than nulling the manager back out (it was // lazily CREATED by the ConstrainTo test-setup call above, and // creation is one-way) — this is the exact discriminator // WireAirborneNullClassified_BothGuids_WritesOnlyAP135Bookkeeping's // own comment names ("Constraint is lazily created only on a // genuine arm"). Assert.NotNull(host.PositionManager.Constraint); Assert.False(host.PositionManager.Constraint!.IsConstrained); Assert.False(fixture.Remote.Interp.IsActive); // The placement itself still committed through the projectile arm. Assert.Equal( MissileAirborneDestination + DestinationWorldOffset, canonical.PhysicsBody!.Position); fixture.DrainPlacementFifo(); } /// /// B1/B2 fix (round-2 review): the far-branch counterpart to /// . /// Retail's far branch (SetPositionSimple, /// player_distance >= 96 m) runs StopInterpolating /// @0x005163C9-@0x005163CB whenever position_manager != 0 — the /// SAME guard the teleport branch's full teleport_hook shares /// for its own StopInterpolating action — but the far branch /// does NOT run the other five teleport_hook actions (no /// UnConstrain). For a bare missile this is structurally inert /// (no RemoteMotion), which is why the pre-round-2 AP-141 row /// could call the far-branch skip "faithful by consequence." The /// adopted-body case breaks that: it carries a live Interp /// queue the far branch must ALSO clear, while its armed /// ConstrainTo leash must stay armed (proving the far branch /// really does run only StopInterpolating, not the full hook). /// /// /// Round-3 nit (C2): "leash still armed" pins acdream's OWN divergence, /// not retail's behaviour. Retail's HandleReceivedPosition /// @0x00454254/@0x00454272 re-anchors an existing leash at the object's /// just-updated position on every nonzero MoveOrTeleport return — /// including the far branch's. acdream's far arm never calls /// ConstrainTo at all (D-P4, AP-141 clause (b)), so "still armed" /// here means "left exactly as staged," not "correctly re-anchored." A /// future reader should not read this assertion as full far-branch /// leash fidelity — only the StopInterpolating half is ported. /// /// [Fact] public void MissileAdoptedBody_FarCommit_ClearsInterpQueueButLeavesConstraintArmed() { using var fixture = new Fixture(MissileGuid, isMissile: false); EntityPhysicsHost host = fixture.InstallHost(); fixture.Remote.Body.TransientState = TransientStateFlags.Active | TransientStateFlags.Contact; host.PositionManager.ConstrainTo( new AcDream.Core.Physics.Position( SourceCell, fixture.Remote.Body.Position, Quaternion.Identity), startDistance: 1f, maxDistance: 5f); Assert.NotNull(host.PositionManager.Constraint); Assert.True(host.PositionManager.Constraint!.IsConstrained); fixture.Remote.Interp.Enqueue( fixture.Remote.Body.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: fixture.Remote.Body.Position); Assert.True(fixture.Remote.Interp.IsActive); Assert.True(fixture.Lifetime.Entities.TryGetActive( MissileGuid, out RuntimeEntityRecord canonical)); fixture.Lifetime.Entities.SetFinalPhysicsState( canonical, canonical.FinalPhysicsState | PhysicsStateFlags.Missile); fixture.Lifetime.Physics.BindProjectile( canonical, canonical.PhysicsBody!, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f)); Assert.NotNull(canonical.Projectile); Assert.NotNull(canonical.RemoteMotion); fixture.PublishDestinationCollision(); fixture.ServiceWindow.Allow(DestinationLandblock); // teleportSequence: 1 (unchanged from the fixture's baseline) with a // cross-landblock destination classifies as the FAR disposition // (SetPositionSimple) — the same discriminator // MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody // uses above, just against the adopted-body shape instead of a bare // missile. fixture.Controller.OnPosition(fixture.Update( MissileAirborneDestination, DestinationCell, teleportSequence: 1, guid: MissileGuid, isGrounded: true)); // StopInterpolating ran — the queue is cleared. Assert.False(fixture.Remote.Interp.IsActive); // UnConstrain did NOT run — the far branch is one action, not six. // The leash is still armed. Assert.NotNull(host.PositionManager.Constraint); Assert.True(host.PositionManager.Constraint!.IsConstrained); // The placement itself still committed through the projectile arm. Assert.Equal( MissileAirborneDestination + DestinationWorldOffset, canonical.PhysicsBody!.Position); fixture.DrainPlacementFifo(); } 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; } /// /// C4 route 5 (A5 fix): the bound projectile component for an /// isMissile fixture — for an ordinary /// remote fixture. Exposed so tests can assert prediction-version /// movement without re-deriving it from . /// internal RuntimeProjectile? Projectile { get; private set; } private readonly GpuWorldState _spatial; private readonly uint _guid; private readonly bool _nullClassification; /// /// C5b review L1/L2: true once the hydration controller's materializer /// was actually reached and DECLINED. Only meaningful when the fixture /// was built with decliningMaterializer: true. /// internal bool MaterializerDeclined => _decliningMaterializer?.Declined ?? false; private readonly DecliningMaterializer? _decliningMaterializer; internal Fixture( uint guid, bool nullClassification = false, bool withAnimation = false, bool isMissile = false, bool decliningMaterializer = false) { _guid = guid; _nullClassification = nullClassification; _decliningMaterializer = decliningMaterializer ? new DecliningMaterializer() : null; 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); // C4 route 5 (A5 fix): a missile fixture carries the Missile bit // from spawn — the SAME data-driven bit // RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition // (D-P1) reads, so this fixture's record classifies exactly like // a real missile would once TryBind/BindProjectile below binds // the component. PhysicsStateFlags baseState = PhysicsStateFlags.ReportCollisions | (isMissile ? PhysicsStateFlags.Missile : PhysicsStateFlags.None); var physics = new PhysicsSpawnData( RawState: (uint)baseState, 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)baseState, 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)); // C4 route 5 (A5 fix): a missile fixture binds a // RuntimeProjectile directly through the SAME production Runtime // entry point ProjectileController.TryBind eventually calls // (RuntimePhysicsState.BindProjectile) — never a RemoteMotion. // This is the exact shape invariant 8's mutual exclusion pins: // Missile-set means a projectile arm and NO RemoteMotion ever // exists for the entity. if (isMissile) { var body = new PhysicsBody { Position = Entity.Position, Orientation = Entity.Rotation, LastUpdateTime = 1d, State = baseState, TransientState = TransientStateFlags.Active, }; body.SnapToCell(SourceCell, Entity.Position, Entity.Position); RuntimeEntityRecord canonical = record.Canonical!; Lifetime.Entities.SetPhysicsBody(canonical, body); canonical.ObjectClock.Activate(); Lifetime.Physics.AcknowledgeSpatialProjection(canonical, spatial: true); Projectile = (RuntimeProjectile)Lifetime.Physics.BindProjectile( canonical, body, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f)); Shadows.Register( Entity.Id, 0x02000001u, Entity.Position, Entity.Rotation, radius: 0.1f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceLandblock, collisionType: ShadowCollisionType.Sphere, state: (uint)baseState, seedCellId: SourceCell, isStatic: false); } else { 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(), (ILiveEntityProjectionMaterializer?)_decliningMaterializer ?? 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) { } } /// /// C5b review L1/L2: models the post-C5b /// DatLiveEntityProjectionMaterializer self-projection branch /// DECLINING (DatLiveEntityProjectionMaterializer.cs:767-787's /// expectedCanonical.FullCellId != 0u gate not being satisfied), /// which is the state C5b's contract §14 item (1) says production now /// reaches. Records that it was reached so the test cannot pass /// vacuously, and returns — the value /// OnPosition's recovery call site deliberately ignores. /// private sealed class DecliningMaterializer : ILiveEntityProjectionMaterializer { internal bool Declined { get; private set; } public bool TryMaterialize( RuntimeEntityRecord expectedCanonical, WorldSession.EntitySpawn canonicalSpawn, LiveProjectionPurpose purpose, ulong expectedCreateIntegrationVersion, AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null) { Declined = true; return false; } public void ResetSessionState() => Declined = false; } 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 OnLocalPlayerFirstEntryCompleted() { } 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() { } } } }