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, RetrySetupUnavailable, RejectedAuthority, InvalidData, } /// /// 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); /// /// 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; 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; } }