diff --git a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs new file mode 100644 index 00000000..ab849c48 --- /dev/null +++ b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs @@ -0,0 +1,554 @@ +using System.Numerics; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Runtime.Entities; + +namespace AcDream.Runtime.Physics; + +internal enum RuntimePositionEntityKind : byte +{ + Unknown, + LocalPlayer, + Remote, + Projectile, +} + +internal enum RuntimeCreateResidenceKind : byte +{ + Unknown, + TopLevel, + Parented, + PickedUp, +} + +internal enum RuntimeAcceptedPositionSource : byte +{ + Unknown, + PositionEvent, + SameIncarnationCreate, +} + +internal enum RuntimeLeaveWorldCause : byte +{ + Unknown, + Pickup, + Parent, +} + +internal enum RuntimeAuthoritativePositionDisposition : byte +{ + RejectedAuthority, + RejectedData, + AwaitFreshPosition, + NoPositionOperation, + Interpolate, + SetPosition, + SetPositionSimple, +} + +internal enum RuntimeTeleportHookPhase : byte +{ + None, + BeforePositionOperation, + AfterPositionOperation, + AfterEnterWorld, +} + +/// +/// Exact accepted-wire authority for one presentation-independent position +/// route. identifies the incarnation while +/// rejects a superseded accepted pose. +/// The previous and accepted TELEPORT_TS values remain explicit so the pure +/// classifier can apply retail's wrap-safe freshness rule without consulting +/// App state. +/// +internal readonly record struct RuntimeAuthoritativePositionAuthority( + RuntimeGenerationToken Generation, + RuntimeEntityKey Entity, + ulong PositionAuthorityVersion, + ushort AcceptedPositionSequence, + ushort PreviousTeleportSequence, + ushort AcceptedTeleportSequence, + PositionTimestampDisposition TimestampDisposition) +{ + internal bool IsStructurallyValid => Generation.Value != 0UL + && Entity.LocalEntityId != 0u + && PositionAuthorityVersion != 0UL + && TimestampDisposition is PositionTimestampDisposition.Apply + or PositionTimestampDisposition.ForcePosition; + + internal bool TeleportAdvanced => + TimestampDisposition is PositionTimestampDisposition.Apply + && PhysicsTimestampGate.IsNewer( + PreviousTeleportSequence, + AcceptedTeleportSequence); + + internal bool TeleportRegressed => + PhysicsTimestampGate.IsNewer( + AcceptedTeleportSequence, + PreviousTeleportSequence); +} + +/// +/// Placement facts which must not be collapsed into presentation visibility. +/// Hidden and NoDraw objects still run retail SetPosition. Hidden suppresses +/// the complete collision batch after its retail state rewrite; NoDraw is +/// render-only. Source/peer ReportCollisions and IgnoreCollisions decisions +/// remain downstream in RuntimeCollisionReportingState. +/// +internal readonly record struct RuntimePositionPlacementFacts( + PhysicsStateFlags PhysicsState, + bool HasAuthoredMoverShape) +{ + internal bool CollisionBatchEligible => + (PhysicsState & PhysicsStateFlags.Hidden) == 0; +} + +internal readonly record struct RuntimeCreatePositionRouteRequest( + RuntimeAuthoritativePositionAuthority Authority, + RuntimePositionEntityKind EntityKind, + RuntimeCreateResidenceKind Residence, + CreateObject.ServerPosition? AcceptedWirePosition, + RuntimePositionPlacementFacts PlacementFacts); + +internal readonly record struct RuntimeAcceptedPositionRouteRequest( + RuntimeAuthoritativePositionAuthority Authority, + RuntimePositionEntityKind EntityKind, + RuntimeAcceptedPositionSource Source, + CreateObject.ServerPosition AcceptedWirePosition, + uint? PlacementFrame, + Vector3? PositionPackVelocity, + uint? CommittedCellId, + bool HasContact, + float PlayerDistance, + bool UsePositionFromServer, + bool HasAnimations, + RuntimePositionPlacementFacts PlacementFacts); + +internal readonly record struct RuntimeLeaveWorldRouteRequest( + RuntimeAuthoritativePositionAuthority Authority, + RuntimePositionEntityKind EntityKind, + RuntimeLeaveWorldCause Cause, + RuntimePositionPlacementFacts PlacementFacts); + +/// +/// Immutable action plan for retail HandleReceivedPosition/MoveOrTeleport. +/// It deliberately contains no renderer, world entity, UI, or host callback. +/// Graphical and headless hosts will consume the same plan during Group 2. +/// +internal readonly record struct RuntimeAuthoritativePositionRoute( + RuntimeAuthoritativePositionAuthority Authority, + RuntimeAuthoritativePositionDisposition Disposition, + RuntimeSetPositionOperationKind OperationKind, + PhysicsSetPositionFlags SetPositionFlags, + uint PlacementFrame, + bool UnparentBeforeRouting, + bool ApplyPlacementFrameBeforeRouting, + bool LeaveWorld, + RuntimeTeleportHookPhase TeleportHookPhase, + bool StopInterpolating, + bool ConstrainAfterRouting, + bool PreserveHeading, + bool ZeroVelocity, + bool SendPositionImmediately, + bool CollisionBatchEligible) +{ + internal bool Accepted => Disposition is not + RuntimeAuthoritativePositionDisposition.RejectedAuthority + and not RuntimeAuthoritativePositionDisposition.RejectedData; + + internal bool PerformsSetPosition => Disposition is + RuntimeAuthoritativePositionDisposition.SetPosition + or RuntimeAuthoritativePositionDisposition.SetPositionSimple; + + internal bool RunsTeleportHook => + TeleportHookPhase is not RuntimeTeleportHookPhase.None; +} + +/// +/// Pure retail route classifier for new CreateObject residence, +/// SmartBox::HandleReceivedPosition (0x00453FD0), and +/// CPhysicsObj::MoveOrTeleport (0x00516330). +/// +internal static class RuntimeAuthoritativePositionRouteClassifier +{ + private const float MaxPhysicsDistance = 96f; + private const PhysicsSetPositionFlags InitialCreateFlags = + PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide; + private const PhysicsSetPositionFlags AuthoritativeTeleportFlags = + PhysicsSetPositionFlags.Teleport + | PhysicsSetPositionFlags.Slide + | PhysicsSetPositionFlags.SendPositionEvent; + + internal static RuntimeAuthoritativePositionRoute ClassifyCreate( + in RuntimeCreatePositionRouteRequest request) + { + if (!ValidCreateAuthority(request.Authority) + || !ValidEntityKind(request.EntityKind) + || request.Residence is RuntimeCreateResidenceKind.Unknown) + { + return RejectedAuthority(request.Authority); + } + + RuntimeSetPositionOperationKind operation = OperationKind( + request.EntityKind, + initialCreate: true); + bool reporting = request.PlacementFacts.CollisionBatchEligible; + if (request.Residence is RuntimeCreateResidenceKind.Parented + or RuntimeCreateResidenceKind.PickedUp) + { + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + operation, + PhysicsSetPositionFlags.None, + 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: true, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + if (request.AcceptedWirePosition is not { } position + || !ValidPosition(position)) + { + return RejectedData(request.Authority, operation, reporting); + } + + // New CreateObject enters through CPhysicsObj::enter_world(Position*) + // -> enter_world(1), which uses 0x11 for every top-level object. + // Static state and an authored-shapeless Setup do not suppress the + // SetPosition transaction; Core supplies retail's placement dummy + // sphere when required. + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.SetPosition, + operation, + InitialCreateFlags, + 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: false, + TeleportHookPhase: request.EntityKind + is RuntimePositionEntityKind.LocalPlayer + ? RuntimeTeleportHookPhase.AfterEnterWorld + : RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + internal static RuntimeAuthoritativePositionRoute ClassifyAcceptedPosition( + in RuntimeAcceptedPositionRouteRequest request) + { + RuntimeSetPositionOperationKind operation = OperationKind( + request.EntityKind, + initialCreate: false); + bool reporting = request.PlacementFacts.CollisionBatchEligible; + if (!ValidAcceptedAuthority(request.Authority, request.EntityKind) + || request.Source is RuntimeAcceptedPositionSource.Unknown + || !ValidEntityKind(request.EntityKind)) + { + return RejectedAuthority(request.Authority, operation, reporting); + } + if (!ValidPosition(request.AcceptedWirePosition)) + return RejectedData(request.Authority, operation, reporting); + + bool force = request.Authority.TimestampDisposition + is PositionTimestampDisposition.ForcePosition; + if (force) + { + // The FORCE_POSITION branch precedes unset_parent and + // SetPlacementFrame in HandleReceivedPosition. + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.SetPositionSimple, + operation, + AuthoritativeTeleportFlags, + request.PlacementFrame ?? 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: true, + ZeroVelocity: false, + SendPositionImmediately: true, + reporting); + } + + uint placement = request.PlacementFrame ?? 0u; + if (request.EntityKind is RuntimePositionEntityKind.LocalPlayer) + { + if (request.Authority.TeleportAdvanced) + { + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.SetPosition, + operation, + AuthoritativeTeleportFlags, + placement, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation, + StopInterpolating: false, + ConstrainAfterRouting: true, + PreserveHeading: false, + ZeroVelocity: true, + SendPositionImmediately: false, + reporting); + } + + bool interpolate = request.UsePositionFromServer + && request.HasContact; + return new RuntimeAuthoritativePositionRoute( + request.Authority, + interpolate + ? RuntimeAuthoritativePositionDisposition.Interpolate + : RuntimeAuthoritativePositionDisposition.NoPositionOperation, + operation, + PhysicsSetPositionFlags.None, + placement, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: true, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + // Accepted wire position is intentionally distinct from committed + // residence. A target frame with a nonzero cell does not make a + // cellless canonical body resident; only a later Runtime SetPosition + // or simulation commit may change FullCellId. + bool cellless = !request.CommittedCellId.HasValue + || request.CommittedCellId.Value == 0u; + if (request.Authority.TeleportAdvanced || cellless) + { + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.SetPosition, + operation, + AuthoritativeTeleportFlags, + placement, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation, + StopInterpolating: false, + ConstrainAfterRouting: true, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + // Same-incarnation CreateObject passes arg5=true directly to + // HandleReceivedPosition; it never consults the body's current + // contact bit for MoveOrTeleport classification. + bool effectiveContact = request.Source + is RuntimeAcceptedPositionSource.SameIncarnationCreate + || request.HasContact; + if (!effectiveContact) + { + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.NoPositionOperation, + operation, + PhysicsSetPositionFlags.None, + placement, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + // Retail has no explicit NaN policy here. Reject a nonfinite derived + // distance at the modern authority boundary rather than recreating an + // x87 unordered-compare accident as gameplay behavior. + if (!float.IsFinite(request.PlayerDistance) + || request.PlayerDistance < 0f) + { + return RejectedData(request.Authority, operation, reporting); + } + + bool nearby = request.PlayerDistance < MaxPhysicsDistance; + return new RuntimeAuthoritativePositionRoute( + request.Authority, + nearby + ? RuntimeAuthoritativePositionDisposition.Interpolate + : RuntimeAuthoritativePositionDisposition.SetPositionSimple, + operation, + nearby ? PhysicsSetPositionFlags.None : AuthoritativeTeleportFlags, + placement, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: !nearby, + ConstrainAfterRouting: true, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + internal static RuntimeAuthoritativePositionRoute ClassifyLeaveWorld( + in RuntimeLeaveWorldRouteRequest request) + { + RuntimeSetPositionOperationKind operation = OperationKind( + request.EntityKind, + initialCreate: false); + bool reporting = request.PlacementFacts.CollisionBatchEligible; + if (!ValidCreateAuthority(request.Authority) + || !ValidEntityKind(request.EntityKind) + || request.Cause is RuntimeLeaveWorldCause.Unknown) + { + return RejectedAuthority(request.Authority, operation, reporting); + } + + return new RuntimeAuthoritativePositionRoute( + request.Authority, + RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + operation, + PhysicsSetPositionFlags.None, + 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: true, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + } + + private static bool ValidCreateAuthority( + in RuntimeAuthoritativePositionAuthority authority) => + authority.IsStructurallyValid + && authority.TimestampDisposition is PositionTimestampDisposition.Apply + && authority.PreviousTeleportSequence == authority.AcceptedTeleportSequence; + + private static bool ValidAcceptedAuthority( + in RuntimeAuthoritativePositionAuthority authority, + RuntimePositionEntityKind kind) + { + if (!authority.IsStructurallyValid || authority.TeleportRegressed) + return false; + return authority.TimestampDisposition switch + { + PositionTimestampDisposition.Apply => true, + PositionTimestampDisposition.ForcePosition => + kind is RuntimePositionEntityKind.LocalPlayer + && authority.PreviousTeleportSequence + == authority.AcceptedTeleportSequence, + _ => false, + }; + } + + private static bool ValidEntityKind(RuntimePositionEntityKind kind) => + kind is RuntimePositionEntityKind.LocalPlayer + or RuntimePositionEntityKind.Remote + or RuntimePositionEntityKind.Projectile; + + private static bool ValidPosition(in CreateObject.ServerPosition position) + { + var origin = new Vector3( + position.PositionX, + position.PositionY, + position.PositionZ); + var orientation = new Quaternion( + position.RotationX, + position.RotationY, + position.RotationZ, + position.RotationW); + return float.IsFinite(origin.X) + && float.IsFinite(origin.Y) + && float.IsFinite(origin.Z) + && float.IsFinite(orientation.X) + && float.IsFinite(orientation.Y) + && float.IsFinite(orientation.Z) + && float.IsFinite(orientation.W) + && PositionFrameValidation.IsValid( + position.LandblockId, + origin, + orientation); + } + + private static RuntimeSetPositionOperationKind OperationKind( + RuntimePositionEntityKind kind, + bool initialCreate) => kind switch + { + RuntimePositionEntityKind.LocalPlayer when initialCreate => + RuntimeSetPositionOperationKind.InitialLogin, + RuntimePositionEntityKind.LocalPlayer => + RuntimeSetPositionOperationKind.LocalAuthoritative, + RuntimePositionEntityKind.Projectile => + RuntimeSetPositionOperationKind.ProjectileAuthoritative, + _ => RuntimeSetPositionOperationKind.RemoteAuthoritative, + }; + + private static RuntimeAuthoritativePositionRoute RejectedAuthority( + in RuntimeAuthoritativePositionAuthority authority, + RuntimeSetPositionOperationKind operation = + RuntimeSetPositionOperationKind.RemoteAuthoritative, + bool reporting = false) => new( + authority, + RuntimeAuthoritativePositionDisposition.RejectedAuthority, + operation, + PhysicsSetPositionFlags.None, + 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); + + private static RuntimeAuthoritativePositionRoute RejectedData( + in RuntimeAuthoritativePositionAuthority authority, + RuntimeSetPositionOperationKind operation, + bool reporting) => new( + authority, + RuntimeAuthoritativePositionDisposition.RejectedData, + operation, + PhysicsSetPositionFlags.None, + 0u, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, + LeaveWorld: false, + TeleportHookPhase: RuntimeTeleportHookPhase.None, + StopInterpolating: false, + ConstrainAfterRouting: false, + PreserveHeading: false, + ZeroVelocity: false, + SendPositionImmediately: false, + reporting); +} diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs new file mode 100644 index 00000000..d895998e --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs @@ -0,0 +1,553 @@ +using System.Numerics; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Runtime.Entities; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime.Tests.Physics; + +public sealed class RuntimeAuthoritativePositionRouteClassifierTests +{ + [Theory] + [InlineData(RuntimePositionEntityKind.LocalPlayer, + RuntimeSetPositionOperationKind.InitialLogin, + RuntimeTeleportHookPhase.AfterEnterWorld)] + [InlineData(RuntimePositionEntityKind.Remote, + RuntimeSetPositionOperationKind.RemoteAuthoritative, + RuntimeTeleportHookPhase.None)] + [InlineData(RuntimePositionEntityKind.Projectile, + RuntimeSetPositionOperationKind.ProjectileAuthoritative, + RuntimeTeleportHookPhase.None)] + internal void TopLevelCreate_AlwaysUsesRetailEnterWorldFlags( + RuntimePositionEntityKind kind, + RuntimeSetPositionOperationKind expectedKind, + RuntimeTeleportHookPhase expectedHookPhase) + { + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + kind, + RuntimeCreateResidenceKind.TopLevel, + Position(), + new RuntimePositionPlacementFacts( + PhysicsStateFlags.Static + | PhysicsStateFlags.Hidden + | PhysicsStateFlags.NoDraw, + HasAuthoredMoverShape: false))); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + route.Disposition); + Assert.Equal(expectedKind, route.OperationKind); + Assert.Equal(0x11u, (uint)route.SetPositionFlags); + Assert.Equal(expectedHookPhase, route.TeleportHookPhase); + Assert.True(route.PerformsSetPosition); + Assert.False(route.CollisionBatchEligible); + } + + [Theory] + [InlineData(RuntimeCreateResidenceKind.Parented)] + [InlineData(RuntimeCreateResidenceKind.PickedUp)] + internal void NewParentedOrPickedUpCreate_LeavesWorldAndAwaitsPosition( + RuntimeCreateResidenceKind residence) + { + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + residence, + AcceptedWirePosition: null, + default)); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + route.Disposition); + Assert.True(route.LeaveWorld); + Assert.False(route.PerformsSetPosition); + } + + [Theory] + [MemberData(nameof(InvalidPositions))] + internal void CreateAndPositionRejectMalformedAuthoritativeFrames( + CreateObject.ServerPosition invalid) + { + RuntimeAuthoritativePositionRoute create = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + RuntimeCreateResidenceKind.TopLevel, + invalid, + default)); + RuntimeAuthoritativePositionRoute update = ClassifyRemote( + position: invalid, + committedCellId: Cell, + hasContact: true, + playerDistance: 1f); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData, + create.Disposition); + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData, + update.Disposition); + } + + [Theory] + [InlineData(RuntimeAcceptedPositionSource.PositionEvent)] + [InlineData(RuntimeAcceptedPositionSource.SameIncarnationCreate)] + internal void AcceptedPosition_UnparentsAndAppliesDefaultZeroPlacementBeforeRoute( + RuntimeAcceptedPositionSource source) + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + source: source, + placementFrame: null, + committedCellId: Cell, + hasContact: true, + playerDistance: 10f); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate, + route.Disposition); + Assert.True(route.UnparentBeforeRouting); + Assert.True(route.ApplyPlacementFrameBeforeRouting); + Assert.Equal(0u, route.PlacementFrame); + } + + [Fact] + public void AnimatedAcceptedPosition_UnparentsButSkipsPlacementFrameReset() + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + committedCellId: Cell, + hasContact: true, + playerDistance: 10f, + hasAnimations: true); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate, + route.Disposition); + Assert.True(route.UnparentBeforeRouting); + Assert.False(route.ApplyPlacementFrameBeforeRouting); + } + + [Theory] + [InlineData(95.999f, RuntimeAuthoritativePositionDisposition.Interpolate)] + [InlineData(96f, RuntimeAuthoritativePositionDisposition.SetPositionSimple)] + internal void SameIncarnationCreate_ForcesContactRoute( + float distance, + RuntimeAuthoritativePositionDisposition expected) + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + source: RuntimeAcceptedPositionSource.SameIncarnationCreate, + committedCellId: Cell, + hasContact: false, + playerDistance: distance); + + Assert.Equal(expected, route.Disposition); + Assert.True(route.ConstrainAfterRouting); + } + + [Theory] + [InlineData(ushort.MaxValue, 0)] + [InlineData(0x8000, 0)] + internal void RemoteFreshTeleport_UsesWrapSafeTimestampAndRetailHook( + ushort previousTeleport, + ushort acceptedTeleport) + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + authority: Authority(previousTeleport, acceptedTeleport), + committedCellId: Cell, + hasContact: false, + playerDistance: float.NaN); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + route.Disposition); + Assert.Equal(0x1012u, (uint)route.SetPositionFlags); + Assert.Equal(RuntimeTeleportHookPhase.BeforePositionOperation, + route.TeleportHookPhase); + Assert.True(route.ConstrainAfterRouting); + } + + [Fact] + public void RemoteCellless_UsesCommittedResidenceNotAcceptedWireCell() + { + CreateObject.ServerPosition accepted = Position(Cell, 200f); + + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + position: accepted, + committedCellId: null, + hasContact: true, + playerDistance: 1f); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + route.Disposition); + Assert.Equal(RuntimeTeleportHookPhase.BeforePositionOperation, + route.TeleportHookPhase); + Assert.Equal(Cell, accepted.LandblockId); + } + + [Fact] + public void RemoteNoContact_PerformsNoPositionOperationOrConstraint() + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + committedCellId: Cell, + hasContact: false, + playerDistance: float.NaN); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.NoPositionOperation, + route.Disposition); + Assert.False(route.ConstrainAfterRouting); + Assert.False(route.PerformsSetPosition); + } + + [Theory] + [InlineData(0f, RuntimeAuthoritativePositionDisposition.Interpolate, false)] + [InlineData(95.999f, RuntimeAuthoritativePositionDisposition.Interpolate, false)] + [InlineData(96f, RuntimeAuthoritativePositionDisposition.SetPositionSimple, true)] + [InlineData(500f, RuntimeAuthoritativePositionDisposition.SetPositionSimple, true)] + internal void RemoteContact_UsesExactNinetySixBoundary( + float distance, + RuntimeAuthoritativePositionDisposition expected, + bool stopInterpolating) + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + committedCellId: Cell, + hasContact: true, + playerDistance: distance); + + Assert.Equal(expected, route.Disposition); + Assert.Equal(stopInterpolating, route.StopInterpolating); + Assert.True(route.ConstrainAfterRouting); + Assert.Equal(stopInterpolating ? 0x1012u : 0u, + (uint)route.SetPositionFlags); + } + + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(-0.001f)] + internal void RemoteContact_RejectsMalformedDerivedDistance(float distance) + { + RuntimeAuthoritativePositionRoute route = ClassifyRemote( + committedCellId: Cell, + hasContact: true, + playerDistance: distance); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData, + route.Disposition); + } + + [Fact] + public void RemotePositionPackVelocity_IsDeadRoutingInput() + { + RuntimeAcceptedPositionRouteRequest request = RemoteRequest( + committedCellId: Cell, + hasContact: true, + playerDistance: 10f) with + { + PositionPackVelocity = new Vector3(float.NaN, float.PositiveInfinity, 7f), + }; + + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier + .ClassifyAcceptedPosition(request); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate, + route.Disposition); + Assert.True(route.Accepted); + } + + [Fact] + public void LocalFreshForce_PreservesHeadingBlipsAndAcknowledgesImmediately() + { + RuntimeAuthoritativePositionRoute route = ClassifyLocal( + Authority( + previousTeleport: 10, + acceptedTeleport: 10, + disposition: PositionTimestampDisposition.ForcePosition)); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, + route.Disposition); + Assert.Equal(0x1012u, (uint)route.SetPositionFlags); + Assert.True(route.PreserveHeading); + Assert.True(route.SendPositionImmediately); + Assert.False(route.UnparentBeforeRouting); + Assert.False(route.ApplyPlacementFrameBeforeRouting); + Assert.False(route.ConstrainAfterRouting); + } + + [Fact] + public void ForcePosition_IsRejectedForRemoteOrUnequalTeleport() + { + RuntimeAuthoritativePositionAuthority force = Authority( + 10, 10, PositionTimestampDisposition.ForcePosition); + RuntimeAuthoritativePositionRoute remote = ClassifyRemote(authority: force); + RuntimeAuthoritativePositionRoute unequal = ClassifyLocal( + Authority(10, 11, PositionTimestampDisposition.ForcePosition)); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority, + remote.Disposition); + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority, + unequal.Disposition); + } + + [Fact] + public void LocalFreshTeleport_SetsPositionConstrainsAndZerosVelocity() + { + RuntimeAuthoritativePositionRoute route = ClassifyLocal( + Authority(ushort.MaxValue, 0)); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + route.Disposition); + Assert.Equal(0x1012u, (uint)route.SetPositionFlags); + Assert.True(route.UnparentBeforeRouting); + Assert.True(route.ApplyPlacementFrameBeforeRouting); + Assert.True(route.ConstrainAfterRouting); + Assert.True(route.ZeroVelocity); + Assert.Equal(RuntimeTeleportHookPhase.AfterPositionOperation, + route.TeleportHookPhase); + } + + [Theory] + [InlineData(false, false, false, RuntimeAuthoritativePositionDisposition.NoPositionOperation)] + [InlineData(false, true, true, RuntimeAuthoritativePositionDisposition.NoPositionOperation)] + [InlineData(true, false, true, RuntimeAuthoritativePositionDisposition.NoPositionOperation)] + [InlineData(true, true, false, RuntimeAuthoritativePositionDisposition.Interpolate)] + [InlineData(true, true, true, RuntimeAuthoritativePositionDisposition.Interpolate)] + internal void LocalOrdinary_AlwaysConstrainsAndInterpolatesOnlyByOptionAndContact( + bool usePositionFromServer, + bool hasContact, + bool hasVelocity, + RuntimeAuthoritativePositionDisposition expected) + { + RuntimeAuthoritativePositionRoute route = ClassifyLocal( + Authority(), + usePositionFromServer, + hasContact, + hasVelocity + ? new Vector3(float.NaN, float.PositiveInfinity, 1f) + : null); + + Assert.Equal(expected, route.Disposition); + Assert.True(route.ConstrainAfterRouting); + } + + [Theory] + [InlineData(RuntimeLeaveWorldCause.Pickup)] + [InlineData(RuntimeLeaveWorldCause.Parent)] + internal void PickupAndParent_LeaveWorldAndAwaitLaterFreshPosition( + RuntimeLeaveWorldCause cause) + { + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier.ClassifyLeaveWorld( + new RuntimeLeaveWorldRouteRequest( + Authority(), + RuntimePositionEntityKind.Projectile, + cause, + default)); + + Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + route.Disposition); + Assert.Equal(RuntimeSetPositionOperationKind.ProjectileAuthoritative, + route.OperationKind); + Assert.True(route.LeaveWorld); + Assert.False(route.PerformsSetPosition); + } + + [Fact] + public void HiddenAndNoDrawDoNotGatePlacement_ButReportingRemainsSeparate() + { + RuntimePositionPlacementFacts visible = new( + PhysicsStateFlags.None, + HasAuthoredMoverShape: true); + RuntimePositionPlacementFacts noDraw = visible with + { + PhysicsState = PhysicsStateFlags.NoDraw, + }; + RuntimePositionPlacementFacts hidden = visible with + { + PhysicsState = PhysicsStateFlags.Hidden, + }; + RuntimePositionPlacementFacts ignored = visible with + { + PhysicsState = PhysicsStateFlags.IgnoreCollisions, + }; + + RuntimeAuthoritativePositionRoute visibleRoute = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + RuntimeCreateResidenceKind.TopLevel, + Position(), + visible)); + RuntimeAuthoritativePositionRoute noDrawRoute = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + RuntimeCreateResidenceKind.TopLevel, + Position(), + noDraw)); + RuntimeAuthoritativePositionRoute hiddenRoute = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + RuntimeCreateResidenceKind.TopLevel, + Position(), + hidden)); + RuntimeAuthoritativePositionRoute ignoredRoute = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + Authority(), + RuntimePositionEntityKind.Remote, + RuntimeCreateResidenceKind.TopLevel, + Position(), + ignored)); + + Assert.All([visibleRoute, noDrawRoute, hiddenRoute, ignoredRoute], route => + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + route.Disposition)); + Assert.True(visibleRoute.CollisionBatchEligible); + Assert.True(noDrawRoute.CollisionBatchEligible); + Assert.False(hiddenRoute.CollisionBatchEligible); + Assert.True(ignoredRoute.CollisionBatchEligible); + } + + [Fact] + public void ProjectilePosition_UsesRemoteMoveOrTeleportClassification() + { + RuntimeAcceptedPositionRouteRequest request = RemoteRequest( + committedCellId: Cell, + hasContact: true, + playerDistance: 96f) with + { + EntityKind = RuntimePositionEntityKind.Projectile, + }; + + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier + .ClassifyAcceptedPosition(request); + + Assert.Equal(RuntimeSetPositionOperationKind.ProjectileAuthoritative, + route.OperationKind); + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, + route.Disposition); + } + + [Fact] + public void StaleOrMalformedAuthorityIsRejectedWithoutRouting() + { + RuntimeAuthoritativePositionRoute stale = ClassifyRemote( + authority: Authority(previousTeleport: 0, acceptedTeleport: ushort.MaxValue)); + RuntimeAuthoritativePositionRoute wrongGeneration = ClassifyRemote( + authority: Authority() with { Generation = default }); + RuntimeAuthoritativePositionRoute wrongEntity = ClassifyRemote( + authority: Authority() with { Entity = default }); + RuntimeAuthoritativePositionRoute wrongVersion = ClassifyRemote( + authority: Authority() with { PositionAuthorityVersion = 0UL }); + + Assert.All([stale, wrongGeneration, wrongEntity, wrongVersion], route => + Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority, + route.Disposition)); + } + + public static TheoryData InvalidPositions => new() + { + Position(cell: 0u), + Position(x: float.NaN), + Position(x: float.PositiveInfinity), + Position(rotationW: 0f), + Position(rotationW: float.NaN), + }; + + private const uint Cell = 0x0101FFFFu; + + private static RuntimeAuthoritativePositionAuthority Authority( + ushort previousTeleport = 10, + ushort acceptedTeleport = 10, + PositionTimestampDisposition disposition = PositionTimestampDisposition.Apply) => + new( + new RuntimeGenerationToken(7), + new RuntimeEntityKey(0x70000001u, 3), + PositionAuthorityVersion: 11UL, + AcceptedPositionSequence: 20, + previousTeleport, + acceptedTeleport, + disposition); + + private static RuntimeAuthoritativePositionRoute ClassifyRemote( + RuntimeAuthoritativePositionAuthority? authority = null, + RuntimeAcceptedPositionSource source = RuntimeAcceptedPositionSource.PositionEvent, + CreateObject.ServerPosition? position = null, + uint? placementFrame = 0u, + uint? committedCellId = Cell, + bool hasContact = true, + float playerDistance = 10f, + bool hasAnimations = false) => + RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition( + RemoteRequest( + authority, + source, + position, + placementFrame, + committedCellId, + hasContact, + playerDistance, + hasAnimations)); + + private static RuntimeAcceptedPositionRouteRequest RemoteRequest( + RuntimeAuthoritativePositionAuthority? authority = null, + RuntimeAcceptedPositionSource source = RuntimeAcceptedPositionSource.PositionEvent, + CreateObject.ServerPosition? position = null, + uint? placementFrame = 0u, + uint? committedCellId = Cell, + bool hasContact = true, + float playerDistance = 10f, + bool hasAnimations = false) => + new( + authority ?? Authority(), + RuntimePositionEntityKind.Remote, + source, + position ?? Position(), + placementFrame, + PositionPackVelocity: Vector3.One, + committedCellId, + hasContact, + playerDistance, + UsePositionFromServer: false, + hasAnimations, + default); + + private static RuntimeAuthoritativePositionRoute ClassifyLocal( + RuntimeAuthoritativePositionAuthority authority, + bool usePositionFromServer = false, + bool hasContact = true, + Vector3? velocity = null) => + RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition( + new RuntimeAcceptedPositionRouteRequest( + authority, + RuntimePositionEntityKind.LocalPlayer, + RuntimeAcceptedPositionSource.PositionEvent, + Position(), + PlacementFrame: null, + PositionPackVelocity: velocity, + CommittedCellId: Cell, + hasContact, + PlayerDistance: 0f, + usePositionFromServer, + HasAnimations: false, + default)); + + private static CreateObject.ServerPosition Position( + uint cell = Cell, + float x = 10f, + float rotationW = 1f) => + new( + cell, + x, + 20f, + 30f, + rotationW, + 0f, + 0f, + 0f); +}