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