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;
}
}