using System;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
namespace AcDream.Runtime.Physics;
///
/// C4 route 4a (2026-08-03): the Runtime-owned decision for the two
///
/// remote branches that perform NO SetPosition — retail
/// CPhysicsObj::MoveOrTeleport (0x00516330)'s airborne no-op
/// (arg4==0 -> return 0 @0x0051636D, nothing written at all) and its
/// near InterpolateTo queue (player_distance < 96 m
/// @0x005163AF). Both classify identically for player-remote and NPC-remote
/// incarnations — retail's disassembly makes no this==player
/// distinction on this path (see ConstraintDistance) — so one Runtime
/// owner decides and applies both, replacing the two independent per-kind
/// copies that used to live in LiveEntityNetworkUpdateController.
///
///
/// Exactly two dispositions are owned here. Everything else —
/// SetPositionSimple, SetPosition, RejectedAuthority,
/// RejectedData, and "not classified at all" () —
/// belongs to , which owns the far
/// (>=96 m) snap from C4 route 4b-2 and states the policy for the
/// leftovers. "Not Interpolate" must never be read as "far".
///
///
internal static class RuntimeRemoteSteadyStatePosition
{
///
/// AP-87 (register row, carried forward — not retired): retail's
/// InterpolateTo has no concept of "the body isn't already tracking the
/// target". acdream's catch-up + per-tick sweep needs one — an unplaced
/// body (a first-UP / spawn-seed origin, or any large correction)
/// enqueued instead of snapped would let InterpolationManager's 100 m
/// far-blip fire and the per-tick sweep run from a cell that does not
/// contain the body, producing the reverted #184 invisible-but-solid
/// monster.
///
private const float BodySnapThreshold = 4f;
///
/// Bug B (2026-08-04): the same constant, exposed read-only so the
/// [remote-slide-up] line can print the threshold its
/// bodyToTarget is about to be compared against instead of the
/// reader having to remember it. TEMPORARY — strip with the
/// ACDREAM_PROBE_REMOTE_SLIDE family.
///
internal const float DiagnosticBodySnapThreshold = BodySnapThreshold;
internal enum Action : byte
{
/// AP-87 backstop: the body wasn't already tracking the
/// target closely enough, has no consumer to walk the queue, or has
/// never received a server sample.
Snapped,
/// The ordinary near catch-up: queued for the per-tick
/// InterpolationManager/ConstraintManager chain to walk toward.
Enqueued,
}
///
/// True when route 4a owns this classification outright, so the legacy App
/// path must not run for it AT ALL — not partially, not "just the render
/// write". This is the staged cutover's ONLY discriminator: the
/// classification itself, never a heuristic or a flag.
///
internal static bool OwnsSteadyState(RuntimeAuthoritativePositionRoute? route) =>
IsAirborneNoOperation(route) || IsNearInterpolate(route);
///
/// Retail's arg4 == 0 return-0 branch: the accepted wire packet
/// reports no ground contact, so MoveOrTeleport writes nothing and
/// its caller SmartBox::HandleReceivedPosition (0x00453FD0) skips
/// ConstrainTo (@0x00454272, inside
/// if (MoveOrTeleport(...) != 0)) as well.
///
internal static bool IsAirborneNoOperation(
RuntimeAuthoritativePositionRoute? route) =>
route is
{
Disposition: RuntimeAuthoritativePositionDisposition.NoPositionOperation,
};
/// Retail's player_distance < 96f InterpolateTo
/// branch.
internal static bool IsNearInterpolate(
RuntimeAuthoritativePositionRoute? route) =>
route is
{
Disposition: RuntimeAuthoritativePositionDisposition.Interpolate,
};
///
/// Applies the retail near-InterpolateTo branch for one remote whose
/// accepted Position has already classified to
/// .
/// Callers must NOT invoke this for
///
/// — that branch writes nothing at all (retail returns 0) and has no
/// operation for this method to perform.
///
///
/// The acdream-only TS-44 sticky suppression is deliberately NOT here: it
/// is an NPC-only caller gate and stays one, so this seam is exactly the
/// kind-agnostic retail decision plus AP-87.
///
///
///
/// The returned is this seam's observable outcome and
/// is what the focused tests assert AP-87's snap against its enqueue with.
/// Production has no use for it and deliberately discards it at both call
/// sites — do not delete it as dead, because collapsing it to
/// would make the two AP-87 outcomes
/// indistinguishable from outside.
///
///
internal static Action ApplyInterpolate(
RemoteMotion remote,
Vector3 worldPosition,
Quaternion orientation,
bool isMovingTo,
bool willBeDrTicked)
{
ArgumentNullException.ThrowIfNull(remote);
// AP-87, all three conditions, verbatim from the NPC copy this
// replaces. `firstUp` is a belt hint, not the load-bearing guard: it
// is unreliable because a UM that enters a locomotion cycle can stamp
// LastServerPosTime before the first UP. It is retained rather than
// silently dropped, and is exact for BOTH kinds — the player-remote
// caller stamps LastServerPosTime before it routes (its diagnostic
// roll-forward block), so `firstUp` is structurally false there and
// this evaluates to exactly the player copy's own two conditions.
bool firstUp = remote.LastServerPosTime <= 0.0;
float bodyToTarget = Vector3.Distance(remote.Body.Position, worldPosition);
if (firstUp || !willBeDrTicked || bodyToTarget > BodySnapThreshold)
{
// Bug B (2026-08-04) blip producer CANDIDATE 1. Emitted BEFORE the
// snap so body/queue state is the pre-snap truth the reader needs.
// docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md §2.
// Pure read; the GUID comes from the attribution latch the routing
// seam stamps. TEMPORARY — strip with the probe family.
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
AcDream.Core.Physics.PhysicsDiagnostics.RemoteSlideAttributionGuid))
{
(int depth, int failCount) =
remote.Interp.DiagnosticInterpolationState;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideBodySnap(
guid: AcDream.Core.Physics.PhysicsDiagnostics
.RemoteSlideAttributionGuid,
firstUp: firstUp,
willBeDrTicked: willBeDrTicked,
bodyToTarget: bodyToTarget,
threshold: BodySnapThreshold,
bodyPosition: remote.Body.Position,
targetPosition: worldPosition,
interpQueueDepth: depth,
interpFailCount: failCount);
}
remote.Interp.Clear();
remote.Body.Position = worldPosition;
remote.Body.Orientation = orientation;
return Action.Snapped;
}
Quaternion? immediate = remote.Interp.Enqueue(
worldPosition,
orientation,
isMovingTo,
remote.Body.Position,
remote.Body.Orientation);
if (immediate is { } close)
remote.Body.Orientation = close;
// Bug B (2026-08-04): the NON-blip outcome. Its presence across a
// slide window is what separates Shape B (queue fed, so the
// InterpolationManager stall snap can arm) from Shape A (queue never
// fed at all). TEMPORARY — strip with the probe family.
if (AcDream.Core.Physics.PhysicsDiagnostics.ShouldLogRemoteSlide(
AcDream.Core.Physics.PhysicsDiagnostics.RemoteSlideAttributionGuid))
{
(int depth, int failCount) =
remote.Interp.DiagnosticInterpolationState;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteSlideEnqueue(
guid: AcDream.Core.Physics.PhysicsDiagnostics
.RemoteSlideAttributionGuid,
bodyToTarget: bodyToTarget,
targetPosition: worldPosition,
interpQueueDepth: depth,
interpFailCount: failCount);
}
return Action.Enqueued;
}
///
/// D2/D4: retail arms CPhysicsObj::ConstrainTo strictly AFTER
/// MoveOrTeleport returns nonzero, anchored to the object's own
/// CURRENT (i.e. post-move) position — SmartBox::HandleReceivedPosition
/// 0x00453FD0 reads &arg2->m_position at 0x00454272, inside
/// the if (MoveOrTeleport(...) != 0) at 0x00454254. It therefore
/// does NOT run on the airborne no-op.
///
///
/// C4 route 4b-3 (D4): this is now the ONLY arming site — the legacy
/// pre-operation call is deleted, matching retail's single
/// @0x00454272. is the routing outcome (which arm
/// actually claimed the packet), NOT the raw classification, because
/// that is the only input that correctly distinguishes a GROUNDED
/// (arms —
/// retail has no state here, but the analogue of "MoveOrTeleport returned
/// nonzero" is true) from the wire-airborne leftover shape (D2's
/// return-0 replacement, which never reaches this call at all — see the
/// caller). The complete partition:
///
///
/// -
/// — arms on EVERY placement outcome (retail discards
/// SetPosition's error and returns 1 unconditionally
/// @0x00516438).
/// -
/// — arms unconditionally, same reason
/// (@0x005163E8).
/// -
/// — arms; retail's InterpolateTo branch returns 1
/// (@0x005163BE).
/// -
/// — arms; only reachable here when the body is already known to be in
/// contact (the caller's free-flight carve-out already
/// returned).
/// -
/// — never arms; retail's arg4 == 0 branch returns 0
/// (@0x0051636D). Both production callers already early-return on this
/// classification before reaching any arming call, so this case is
/// defensive.
///
///
internal static bool TryArmConstraintAfterOperation(
RuntimeRemoteAcceptedPositionArm arm,
RemoteMotion remote)
{
ArgumentNullException.ThrowIfNull(remote);
bool arms = arm switch
{
RuntimeRemoteAcceptedPositionArm.TeleportPlacement => true,
RuntimeRemoteAcceptedPositionArm.FarSnapPlacement => true,
RuntimeRemoteAcceptedPositionArm.NearInterpolate => true,
RuntimeRemoteAcceptedPositionArm.UnroutedCatchUp => true,
RuntimeRemoteAcceptedPositionArm.AirborneNoOperation => false,
_ => false,
};
if (!arms || remote.Host is not { } host)
return false;
ArmConstraintAfterOperation(host);
return true;
}
///
/// The leash arming itself: ConstraintPosOffset is captured as
/// distance(anchor, host.Position) at call time, and the anchor here IS
/// host.Position read live, so a fresh accepted Position always restarts
/// the leash at zero displacement — retail's per-packet re-anchor.
/// docs/research/2026-07-30-constraint-leash-constants.md §2/§3.2.
///
internal static void ArmConstraintAfterOperation(EntityPhysicsHost host)
{
ArgumentNullException.ThrowIfNull(host);
Position anchor = host.Position;
host.PositionManager.ConstrainTo(
anchor,
ConstraintDistance.GetStartConstraintDistance(anchor.ObjCellId),
ConstraintDistance.GetMaxConstraintDistance(anchor.ObjCellId));
}
}