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