Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The live reading of 0.400 says the controller held its default at that instant, not why. Two very different causes produce it: the Setup-derived prepare/publish path never runs for the local player, or it runs and a later writer clobbers the result. Fixing without knowing which is a coin flip. One reading at each hop — prepare (Setup value computed and scaled), publish (assigned to the controller), resolve (what the resolver is actually handed) — with a decision table on the flag mapping each pattern to its cause, including the 0.000 case that would mean a null Setup took the retail dummy path. Edge-triggered per site, so the per-tick resolve site prints once per distinct pair and cannot drown the two one-shot sites it exists to be compared against. Prepare prints the raw authored pair beside the scaled one, so a surprise separates wrong-Setup from wrong-scale without a second run. Lives in PhysicsDiagnostics per code-structure rule 5 rather than as per-call-site env reads. Zero cost when off. Suite 11,231 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
227 lines
8.7 KiB
C#
227 lines
8.7 KiB
C#
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,
|
|
|
|
/// <summary>
|
|
/// The authored Setup collision payload has not been read yet. Resolves
|
|
/// when the prepared-asset read completes.
|
|
/// </summary>
|
|
RetrySetupUnavailable,
|
|
|
|
/// <summary>
|
|
/// #284: Runtime's world frame is not published yet, so a landblock-local
|
|
/// Create origin cannot be converted
|
|
/// (<see cref="RuntimePhysicsState.TryGetWorldFrameOffset"/>). Previously
|
|
/// this reported itself as <see cref="RetrySetupUnavailable"/>, 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.
|
|
/// </summary>
|
|
RetryWorldFrameUnavailable,
|
|
|
|
RejectedAuthority,
|
|
InvalidData,
|
|
}
|
|
|
|
internal static class RuntimeSetPositionMoverPreparationStatusExtensions
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal static bool IsRetryable(
|
|
this RuntimeSetPositionMoverPreparationStatus status) =>
|
|
status is RuntimeSetPositionMoverPreparationStatus
|
|
.RetrySetupUnavailable
|
|
or RuntimeSetPositionMoverPreparationStatus
|
|
.RetryWorldFrameUnavailable;
|
|
|
|
/// <summary>
|
|
/// The parked-placement reason a retryable status contributes to the
|
|
/// ownership ledger, or <see cref="RuntimeSetPositionParkReason.None"/>
|
|
/// when the status is not a park.
|
|
/// </summary>
|
|
internal static RuntimeSetPositionParkReason ParkReason(
|
|
this RuntimeSetPositionMoverPreparationStatus status) =>
|
|
status switch
|
|
{
|
|
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable =>
|
|
RuntimeSetPositionParkReason.AwaitingSetupCollision,
|
|
RuntimeSetPositionMoverPreparationStatus
|
|
.RetryWorldFrameUnavailable =>
|
|
RuntimeSetPositionParkReason.AwaitingWorldFrame,
|
|
_ => RuntimeSetPositionParkReason.None,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// #284: why a first-entry placement is currently parked. Retained on the
|
|
/// operation so <c>CaptureOwnership</c> can report parked work by cause
|
|
/// instead of leaving it invisible until a downstream symptom appears.
|
|
/// </summary>
|
|
internal enum RuntimeSetPositionParkReason
|
|
{
|
|
None,
|
|
AwaitingSetupCollision,
|
|
AwaitingWorldFrame,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <c>CPhysicsObj::SetPosition</c>; an unavailable lookup must not
|
|
/// manufacture that fallback while asynchronous preparation is still live.
|
|
/// </summary>
|
|
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)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Explicit, immutable inputs surrounding retail's authored mover shape.
|
|
/// None of these values are inferred from presentation state.
|
|
/// </summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// Pure preparation port of the mover inputs consumed by retail
|
|
/// <c>CPhysicsObj::SetPosition</c> (0x005160C0). It preserves the complete DAT
|
|
/// sphere order; Core's SPHEREPATH port applies retail's two-sphere cap later.
|
|
/// </summary>
|
|
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<FlatCollisionSphere> spheres = setup?.Spheres
|
|
?? ImmutableArray<FlatCollisionSphere>.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;
|
|
}
|
|
}
|