using System.Collections.Immutable; using System.Numerics; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Entities; namespace AcDream.Runtime.Physics; internal enum RuntimeSetPositionMoverPreparationStatus { Prepared, /// /// The authored Setup collision payload has not been read yet. Resolves /// when the prepared-asset read completes. /// RetrySetupUnavailable, /// /// #284: Runtime's world frame is not published yet, so a landblock-local /// Create origin cannot be converted /// (). Previously /// this reported itself as , which sent /// anyone diagnosing a parked placement to the asset pipeline instead of /// the missing local-player Create that actually publishes the frame. /// Retryable exactly like Setup - only the reason differs. /// RetryWorldFrameUnavailable, RejectedAuthority, InvalidData, } internal static class RuntimeSetPositionMoverPreparationStatusExtensions { /// /// True for the statuses whose work can still succeed on a later pump. /// Call sites must use this rather than comparing against one reason, so /// a newly added retry reason cannot be silently treated as a rejection. /// internal static bool IsRetryable( this RuntimeSetPositionMoverPreparationStatus status) => status is RuntimeSetPositionMoverPreparationStatus .RetrySetupUnavailable or RuntimeSetPositionMoverPreparationStatus .RetryWorldFrameUnavailable; /// /// The parked-placement reason a retryable status contributes to the /// ownership ledger, or /// when the status is not a park. /// internal static RuntimeSetPositionParkReason ParkReason( this RuntimeSetPositionMoverPreparationStatus status) => status switch { RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable => RuntimeSetPositionParkReason.AwaitingSetupCollision, RuntimeSetPositionMoverPreparationStatus .RetryWorldFrameUnavailable => RuntimeSetPositionParkReason.AwaitingWorldFrame, _ => RuntimeSetPositionParkReason.None, }; } /// /// #284: why a first-entry placement is currently parked. Retained on the /// operation so CaptureOwnership can report parked work by cause /// instead of leaving it invisible until a downstream symptom appears. /// internal enum RuntimeSetPositionParkReason { None, AwaitingSetupCollision, AwaitingWorldFrame, } /// /// Distinguishes a Setup payload which has not arrived yet from a completed /// lookup whose result is absent. Retail supplies its dummy placement sphere /// only inside CPhysicsObj::SetPosition; an unavailable lookup must not /// manufacture that fallback while asynchronous preparation is still live. /// internal readonly record struct RuntimeSetPositionMoverSetup( bool IsResolved, uint SetupTableId, FlatSetupCollision? Collision) { internal static RuntimeSetPositionMoverSetup Unavailable => default; internal static RuntimeSetPositionMoverSetup ResolvedAbsent => new(true, 0u, null); internal static RuntimeSetPositionMoverSetup Resolved( uint setupTableId, FlatSetupCollision collision) => new( true, setupTableId != 0u ? setupTableId : throw new ArgumentOutOfRangeException(nameof(setupTableId)), collision ?? throw new ArgumentNullException(nameof(collision))); } /// /// Explicit, immutable inputs surrounding retail's authored mover shape. /// None of these values are inferred from presentation state. /// internal readonly record struct RuntimeSetPositionMoverPreparation( RuntimeSetPositionMoverSetup Setup, RuntimeSetPositionOperationKind Kind, double GameTime, PhysicsPlacementClass PlacementClass, PhysicsSetPositionFlags Flags, Vector3 Line = default, float ScatterRadiusX = 0f, float ScatterRadiusY = 0f, uint ScatterAttempts = 0u, float ShadowWorldOffsetX = 0f, float ShadowWorldOffsetY = 0f, RuntimePortalPlacementAuthority Portal = default, bool ResolveWorldOffsetFromRuntimeFrame = false); /// /// Pure preparation port of the mover inputs consumed by retail /// CPhysicsObj::SetPosition (0x005160C0). It preserves the complete DAT /// sphere order; Core's SPHEREPATH port applies retail's two-sphere cap later. /// internal static class RuntimeSetPositionMoverPreparer { internal static bool TryBuild( RuntimeEntityRecord record, in CreateObject.ServerPosition acceptedPosition, uint canonicalSetupTableId, RuntimeSetPositionOperationKind acceptedKind, RuntimePortalPlacementAuthority acceptedPortal, ulong velocityAuthorityVersion, in RuntimeSetPositionMoverPreparation preparation, out RuntimeSetPositionCommand command) { ArgumentNullException.ThrowIfNull(record); command = default; if (!preparation.Setup.IsResolved || preparation.Kind != acceptedKind || preparation.Portal != acceptedPortal || (canonicalSetupTableId == 0u ? preparation.Setup.SetupTableId != 0u || preparation.Setup.Collision is not null : preparation.Setup.SetupTableId != canonicalSetupTableId || preparation.Setup.Collision is null)) { return false; } CreateObject.ServerPosition position = acceptedPosition; Vector3 cellLocal = new( position.PositionX, position.PositionY, position.PositionZ); Vector3 world = new( cellLocal.X + preparation.ShadowWorldOffsetX, cellLocal.Y + preparation.ShadowWorldOffsetY, cellLocal.Z); Quaternion orientation = new( position.RotationX, position.RotationY, position.RotationZ, position.RotationW); float scale = record.Snapshot.Physics?.Scale ?? record.Snapshot.ObjScale ?? 1f; FlatSetupCollision? setup = preparation.Setup.Collision; ImmutableArray spheres = setup?.Spheres ?? ImmutableArray.Empty; // CPartArray::GetStepUpHeight/GetStepDownHeight are Setup properties, // not sphere properties. An authored Setup with no collision spheres // still supplies both values; only a genuinely absent Setup takes the // retail dummy path with exact zero steps and no scale multiplication. float stepUp = setup is not null ? setup.StepUpHeight * scale : 0f; float stepDown = setup is not null ? setup.StepDownHeight * scale : 0f; // #338 (TEMPORARY): first of three readings along the chain. Prints // the raw authored pair alongside the scaled one, so a surprise here // separates "wrong Setup" from "wrong scale" without a second run. PhysicsDiagnostics.LogStepHeights( "prepare", stepUp, stepDown, setup is not null ? $"authored=({setup.StepUpHeight:F3},{setup.StepDownHeight:F3}) scale={scale:F3}" : "setup=NULL (retail dummy path, exact zero steps)"); EntityCollisionFlags collisionFlags = EntityCollisionFlagsExt.FromPwdBitfield( record.Snapshot.ObjectDescriptionFlags ?? 0u); ObjectInfoState moverFlags = collisionFlags.ToMoverState(); if (collisionFlags.HasFlag(EntityCollisionFlags.IsPlayer)) moverFlags |= ObjectInfoState.IsPlayer; var request = new PhysicsSetPositionRequest( world, orientation, position.LandblockId, cellLocal, spheres, scale, stepUp, stepDown, record.FinalPhysicsState, moverFlags, record.Key?.LocalEntityId ?? 0u, preparation.PlacementClass, preparation.Flags, preparation.Line, preparation.ScatterRadiusX, preparation.ScatterRadiusY, preparation.ScatterAttempts); command = new RuntimeSetPositionCommand( request, preparation.Kind, preparation.GameTime, velocityAuthorityVersion, preparation.ShadowWorldOffsetX, preparation.ShadowWorldOffsetY, preparation.Portal); return true; } }