using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Tests.Entities; /// /// C4 route 5 (D-P1, REVISED after the review round — A2/R1): /// /// derives RuntimePositionEntityKind from /// canonical.FinalPhysicsState & PhysicsStateFlags.Missile /// CONJOINED with a bound, body-agreeing RuntimeProjectile — never /// the Missile bit alone. Retail places every non-player object /// unconditionally; a Missile-flagged record with no bound projectile /// (TryBind permanently refused, or has not run yet) must still /// classify Remote so it keeps tracking through the ordinary remote /// placement path, exactly like the deleted ApplyAuthoritativePosition's /// TryGetCurrent fall-through. Mirrors /// 's fixture shape. /// public sealed class RuntimeProjectilePositionKindTests { private const uint Cell = 0x0101FFFFu; private const uint OtherCell = 0x0102FFFFu; [Fact] public void MissileBitSetAndBound_ClassifiesProjectileAuthoritative() { using var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL); const uint guid = 0x70006001u; RuntimeEntityRecord canonical = lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!; lifetime.Entities.SetFinalPhysicsState( canonical, canonical.FinalPhysicsState | PhysicsStateFlags.Missile); BindProjectile(lifetime, canonical, Cell); WorldSession.EntityPositionUpdate update = PositionUpdate( guid, OtherCell, positionSequence: 2, teleportSequence: 0); Assert.True(lifetime.TryApplyPosition( update, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after)); Assert.True((after.FinalPhysicsState & PhysicsStateFlags.Missile) != 0); Assert.NotNull(after.Projectile); RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition( after, update, disposition, timestamps, playerDistance: 10f); Assert.NotNull(route); Assert.Equal( RuntimeSetPositionOperationKind.ProjectileAuthoritative, route!.Value.OperationKind); } /// /// A2/R1: the Missile bit alone is not sufficient. TryBind's permanent /// refusal (an unsupported multi-sphere Setup) or the pre-bind window /// leaves record.Projectile null while the bit stays set — this /// must still classify Remote so the packet is placed by the ordinary /// remote path, never silently dropped. /// [Fact] public void MissileBitSetButUnbound_ClassifiesRemoteAuthoritative() { using var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL); const uint guid = 0x70006005u; RuntimeEntityRecord canonical = lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!; lifetime.Entities.SetFinalPhysicsState( canonical, canonical.FinalPhysicsState | PhysicsStateFlags.Missile); // Deliberately never bind a RuntimeProjectile. Assert.Null(canonical.Projectile); WorldSession.EntityPositionUpdate update = PositionUpdate( guid, OtherCell, positionSequence: 2, teleportSequence: 0); Assert.True(lifetime.TryApplyPosition( update, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after)); Assert.True((after.FinalPhysicsState & PhysicsStateFlags.Missile) != 0); Assert.Null(after.Projectile); RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition( after, update, disposition, timestamps, playerDistance: 10f); Assert.NotNull(route); Assert.Equal( RuntimeSetPositionOperationKind.RemoteAuthoritative, route!.Value.OperationKind); } [Fact] public void MissileBitClear_ClassifiesRemoteAuthoritative() { using var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL); const uint guid = 0x70006002u; RuntimeEntityRecord canonical = lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!; Assert.True((canonical.FinalPhysicsState & PhysicsStateFlags.Missile) == 0); WorldSession.EntityPositionUpdate update = PositionUpdate( guid, OtherCell, positionSequence: 2, teleportSequence: 0); Assert.True(lifetime.TryApplyPosition( update, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after)); RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition( after, update, disposition, timestamps, playerDistance: 10f); Assert.NotNull(route); Assert.Equal( RuntimeSetPositionOperationKind.RemoteAuthoritative, route!.Value.OperationKind); } /// /// Trap T4 / invariant 8's mutual-exclusion proof, exercised at the flip /// itself: a State packet installing Missile mid-life (ACE's ordinary /// arrow-becomes-live-missile edge, or its converse on impact) makes the /// VERY NEXT Position packet classify the OTHER kind — no stale /// classification survives the flip. /// [Fact] public void MissileBitFlipMidLife_NextPositionReclassifies() { using var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL); const uint guid = 0x70006003u; RuntimeEntityRecord canonical = lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!; Assert.True((canonical.FinalPhysicsState & PhysicsStateFlags.Missile) == 0); WorldSession.EntityPositionUpdate firstUpdate = PositionUpdate( guid, OtherCell, positionSequence: 2, teleportSequence: 0); Assert.True(lifetime.TryApplyPosition( firstUpdate, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, acknowledgeProjection: null, out PositionTimestampDisposition firstDisposition, out _, out AcceptedPhysicsTimestamps firstTimestamps)); Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord beforeFlip)); RuntimeAuthoritativePositionRoute? beforeRoute = lifetime.ClassifyRemoteAcceptedPosition( beforeFlip, firstUpdate, firstDisposition, firstTimestamps, playerDistance: 10f); Assert.Equal( RuntimeSetPositionOperationKind.RemoteAuthoritative, beforeRoute!.Value.OperationKind); // A State packet (0x0013-family) installs Missile — the classifier // itself never sees a State packet; only the NEXT Position does. lifetime.Entities.SetFinalPhysicsState( beforeFlip, beforeFlip.FinalPhysicsState | PhysicsStateFlags.Missile); // TryBind's production ordering: a State packet setting Missile is // immediately followed by binding (ApplyAuthoritativeState -> // TryBind). A2/R1 pins the classifier on the BOUND shape, so this // scenario's flip is only complete once the component exists too. BindProjectile(lifetime, beforeFlip, OtherCell); WorldSession.EntityPositionUpdate secondUpdate = PositionUpdate( guid, Cell, positionSequence: 3, teleportSequence: 0); Assert.True(lifetime.TryApplyPosition( secondUpdate, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, acknowledgeProjection: null, out PositionTimestampDisposition secondDisposition, out _, out AcceptedPhysicsTimestamps secondTimestamps)); Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord afterFlip)); RuntimeAuthoritativePositionRoute? afterRoute = lifetime.ClassifyRemoteAcceptedPosition( afterFlip, secondUpdate, secondDisposition, secondTimestamps, playerDistance: 10f); Assert.NotNull(afterRoute); Assert.Equal( RuntimeSetPositionOperationKind.ProjectileAuthoritative, afterRoute!.Value.OperationKind); } /// /// Attaches a canonical body (if not already present) and binds a /// RuntimeProjectile to it through the SAME production entry /// point ProjectileController.TryBind eventually calls /// (RuntimePhysicsState.BindProjectile) — the classifier's /// conjunctive test (A2/R1) reads exactly this state. /// private static void BindProjectile( RuntimeEntityObjectLifetime lifetime, RuntimeEntityRecord record, uint cellId) { if (record.PhysicsBody is not { } body) { body = new PhysicsBody { Position = new Vector3(10f, 20f, 5f), Orientation = Quaternion.Identity, LastUpdateTime = 1d, State = record.FinalPhysicsState, TransientState = TransientStateFlags.Active, }; body.SnapToCell(cellId, body.Position, body.Position); lifetime.Entities.SetPhysicsBody(record, body); } lifetime.Physics.BindProjectile( record, body, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f)); } private static WorldSession.EntityPositionUpdate PositionUpdate( uint guid, uint cellId, ushort positionSequence, ushort teleportSequence) => new( guid, new CreateObject.ServerPosition( cellId, 12f, 14f, 7f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: positionSequence, TeleportSequence: teleportSequence, ForcePositionSequence: 0); private static WorldSession.EntitySpawn Spawn( uint guid, uint cellId, ushort instance) { var position = new CreateObject.ServerPosition( cellId, 10f, 20f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: instance); var physics = new PhysicsSpawnData( RawState: 0x408u, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "remote-projectile-kind", null, null, 0x09000001u, PhysicsState: 0x408u, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } }