feat(runtime): classify retail authoritative position routes
This commit is contained in:
parent
0fbc7a1fb7
commit
3f800a4aec
2 changed files with 1107 additions and 0 deletions
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact accepted-wire authority for one presentation-independent position
|
||||||
|
/// route. <see cref="RuntimeEntityKey"/> identifies the incarnation while
|
||||||
|
/// <see cref="PositionAuthorityVersion"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure retail route classifier for new CreateObject residence,
|
||||||
|
/// SmartBox::HandleReceivedPosition (0x00453FD0), and
|
||||||
|
/// CPhysicsObj::MoveOrTeleport (0x00516330).
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -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<CreateObject.ServerPosition> 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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue