using System; using System.Numerics; namespace AcDream.Core.Physics.Motion; /// /// R5 — verbatim port of retail's ConstraintManager (acclient.h:31529, /// struct #3467; decomp 0x00556090-0x005562xx, /// r5-constraintmanager-decomp.md). The server-position rubber-band /// leash: while the owning object is in ground contact it progressively /// resists movement past a start→max distance band from a pinned anchor, and /// hard-clamps at the max. Read as a jump gate by /// CMotionInterp::jump_is_allowed (block jump while /// ). /// /// Armed as of Campaign P P5 (2026-07-30, #167, closed /// register row TS-35). Retail arms the leash ONLY from /// SmartBox::HandleReceivedPosition (0x00453fd0, on every inbound /// server position packet) with the start/max band from /// CPhysicsObj::GetStart/MaxConstraintDistance — byte-decoded (the x87 /// returns BN elided) in . acdream's /// equivalents call at every accepted-position /// seam: LiveEntityNetworkUpdateController for remotes (anchored to /// the object's own position, right after the hard-teleport branch returns) /// and PlayerMovementController.SetPosition/BlipPosition for /// the local player (anchored to the received position). See /// docs/research/2026-07-30-constraint-leash-constants.md. /// public sealed class ConstraintManager { private readonly IPhysicsObjHost _host; /// +0x04 retail is_constrained. public bool IsConstrained { get; private set; } /// +0x08 retail constraint_pos_offset — recomputed every /// as the LENGTH of that tick's step (NOT the /// distance to the anchor — see the ACE correctness note, port-plan §2b); /// the next tick's contact branch compares it against start/max. public float ConstraintPosOffset { get; private set; } /// +0x0c retail constraint_pos — the leash anchor. Stored /// by , never read by /// (retail + ACE — write-only in this class). public Position ConstraintPos { get; private set; } /// +0x48 retail constraint_distance_start — the near edge /// of the brake band. public float ConstraintDistanceStart { get; private set; } /// +0x4c retail constraint_distance_max — the far edge /// (full clamp). public float ConstraintDistanceMax { get; private set; } public ConstraintManager(IPhysicsObjHost host) => _host = host ?? throw new ArgumentNullException(nameof(host)); /// /// Retail ConstraintManager::ConstrainTo (0x00556240). Pin the leash /// anchor and band; initialize to the /// CURRENT distance from anchor to the mover (the leash starts already /// extended to wherever the object is, not at zero). /// public void ConstrainTo(Position anchor, float startDistance, float maxDistance) { IsConstrained = true; ConstraintPos = anchor; ConstraintDistanceStart = startDistance; ConstraintDistanceMax = maxDistance; ConstraintPosOffset = Vector3.Distance(anchor.Frame.Origin, _host.Position.Frame.Origin); } /// Retail ConstraintManager::UnConstrain (0x005560c0) — /// clears the constrained flag only (leaves the band/anchor/offset stale /// until the next ). public void UnConstrain() => IsConstrained = false; /// /// Retail ConstraintManager::IsFullyConstrained (0x005560d0): /// constraint_distance_max * 0.9 < constraint_pos_offset — the /// object counts as fully constrained once it has strained past 90 % of the /// max leash. Read by jump_is_allowed to block jumps. False while /// unconstrained (no prior or after /// ) since and /// both default to zero (0*0.9 < 0 /// is false). /// public bool IsFullyConstrained() => ConstraintDistanceMax * 0.9f < ConstraintPosOffset; /// /// Retail ConstraintManager::adjust_offset (0x00556180). The last /// stage of 's chain — clamps the /// ALREADY-composed per-tick (interp + sticky) /// while grounded, then records its length for the next tick. No-op unless /// . See port-plan §2b. /// public void AdjustOffset(MotionDeltaFrame offset, double quantum) { _ = quantum; // retail body doesn't use the quantum directly. if (!IsConstrained) return; if (_host.InContact) // transient_state & 1 — clamp only while grounded. { if (ConstraintPosOffset < ConstraintDistanceMax) { if (ConstraintPosOffset > ConstraintDistanceStart) { // Linear brake taper: 1.0 just past start → 0.0 at max. float taper = (ConstraintDistanceMax - ConstraintPosOffset) / (ConstraintDistanceMax - ConstraintDistanceStart); offset.Origin *= taper; } } else { offset.Origin = Vector3.Zero; // past max — fully pinned. } } // Unconditional (grounded OR airborne): track this tick's step length. ConstraintPosOffset = offset.Origin.Length(); } }