PhysicsBody.IsFullyConstrained now reflects real ConstraintManager state (pushed every tick by the same per-tick pumps commit 2 wired), so jump_is_allowed's already-ported gate (WeenieError 0x47) actually fires while an object is rubber-banding hard against a server position correction, closing the last piece of #167. Housekeeping: - Delete register row TS-35 (retired: the write side is no longer stubbed). - Rewrite the stale doc comments on PhysicsBody.IsFullyConstrained, ConstraintManager (class + IsFullyConstrained), PositionManager.ConstrainTo, EntityPhysicsHost.PositionManager, and PlayerMovementController.PositionManager that described the leash as permanently unarmed/stubbed. - Close #167 in ISSUES.md citing the research doc and commitse0629145/7719d25b. - Add an "as-ported" addendum to docs/research/2026-07-30-constraint-leash-constants.md naming the actual current seam owners (the doc's own open question flagged this as implementer-verify-required post-J-slices). - Update docs/plans/2026-07-29-physics-parity-campaign.md's P5 status and CLAUDE.md's Campaign P summary to reflect #167's closure (items #153/#72 remain open in P5). Verification: complete solution suite green - 9,978 tests, 5 skips, 0 failures across all 9 test projects (Core.Tests, Runtime.Tests, App.Tests, Headless.Tests, Core.Net.Tests, Content.Tests, UI.Abstractions.Tests, Bake.Tests, Cli.Tests).
125 lines
5.8 KiB
C#
125 lines
5.8 KiB
C#
using System;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.Core.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R5 — verbatim port of retail's <c>ConstraintManager</c> (acclient.h:31529,
|
|
/// struct #3467; decomp 0x00556090-0x005562xx,
|
|
/// <c>r5-constraintmanager-decomp.md</c>). The server-position <b>rubber-band
|
|
/// leash</b>: 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
|
|
/// <c>CMotionInterp::jump_is_allowed</c> (block jump while
|
|
/// <see cref="IsFullyConstrained"/>).
|
|
///
|
|
/// <para><b>Armed as of Campaign P P5 (2026-07-30, #167, closed
|
|
/// register row TS-35).</b> Retail arms the leash ONLY from
|
|
/// <c>SmartBox::HandleReceivedPosition</c> (0x00453fd0, on every inbound
|
|
/// server position packet) with the start/max band from
|
|
/// <c>CPhysicsObj::GetStart/MaxConstraintDistance</c> — byte-decoded (the x87
|
|
/// returns BN elided) in <see cref="ConstraintDistance"/>. acdream's
|
|
/// equivalents call <see cref="ConstrainTo"/> at every accepted-position
|
|
/// seam: <c>LiveEntityNetworkUpdateController</c> for remotes (anchored to
|
|
/// the object's own position, right after the hard-teleport branch returns)
|
|
/// and <c>PlayerMovementController.SetPosition</c>/<c>BlipPosition</c> for
|
|
/// the local player (anchored to the received position). See
|
|
/// <c>docs/research/2026-07-30-constraint-leash-constants.md</c>.</para>
|
|
/// </summary>
|
|
public sealed class ConstraintManager
|
|
{
|
|
private readonly IPhysicsObjHost _host;
|
|
|
|
/// <summary>+0x04 retail <c>is_constrained</c>.</summary>
|
|
public bool IsConstrained { get; private set; }
|
|
|
|
/// <summary>+0x08 retail <c>constraint_pos_offset</c> — recomputed every
|
|
/// <see cref="AdjustOffset"/> 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.</summary>
|
|
public float ConstraintPosOffset { get; private set; }
|
|
|
|
/// <summary>+0x0c retail <c>constraint_pos</c> — the leash anchor. Stored
|
|
/// by <see cref="ConstrainTo"/>, never read by <see cref="AdjustOffset"/>
|
|
/// (retail + ACE — write-only in this class).</summary>
|
|
public Position ConstraintPos { get; private set; }
|
|
|
|
/// <summary>+0x48 retail <c>constraint_distance_start</c> — the near edge
|
|
/// of the brake band.</summary>
|
|
public float ConstraintDistanceStart { get; private set; }
|
|
|
|
/// <summary>+0x4c retail <c>constraint_distance_max</c> — the far edge
|
|
/// (full clamp).</summary>
|
|
public float ConstraintDistanceMax { get; private set; }
|
|
|
|
public ConstraintManager(IPhysicsObjHost host)
|
|
=> _host = host ?? throw new ArgumentNullException(nameof(host));
|
|
|
|
/// <summary>
|
|
/// Retail <c>ConstraintManager::ConstrainTo</c> (0x00556240). Pin the leash
|
|
/// anchor and band; initialize <see cref="ConstraintPosOffset"/> to the
|
|
/// CURRENT distance from anchor to the mover (the leash starts already
|
|
/// extended to wherever the object is, not at zero).
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Retail <c>ConstraintManager::UnConstrain</c> (0x005560c0) —
|
|
/// clears the constrained flag only (leaves the band/anchor/offset stale
|
|
/// until the next <see cref="ConstrainTo"/>).</summary>
|
|
public void UnConstrain() => IsConstrained = false;
|
|
|
|
/// <summary>
|
|
/// Retail <c>ConstraintManager::IsFullyConstrained</c> (0x005560d0):
|
|
/// <c>constraint_distance_max * 0.9 < constraint_pos_offset</c> — the
|
|
/// object counts as fully constrained once it has strained past 90 % of the
|
|
/// max leash. Read by <c>jump_is_allowed</c> to block jumps. False while
|
|
/// unconstrained (no prior <see cref="ConstrainTo"/> or after
|
|
/// <see cref="UnConstrain"/>) since <see cref="ConstraintDistanceMax"/> and
|
|
/// <see cref="ConstraintPosOffset"/> both default to zero (<c>0*0.9 < 0</c>
|
|
/// is false).
|
|
/// </summary>
|
|
public bool IsFullyConstrained()
|
|
=> ConstraintDistanceMax * 0.9f < ConstraintPosOffset;
|
|
|
|
/// <summary>
|
|
/// Retail <c>ConstraintManager::adjust_offset</c> (0x00556180). The last
|
|
/// stage of <see cref="PositionManager.AdjustOffset"/>'s chain — clamps the
|
|
/// ALREADY-composed per-tick <paramref name="offset"/> (interp + sticky)
|
|
/// while grounded, then records its length for the next tick. No-op unless
|
|
/// <see cref="IsConstrained"/>. See port-plan §2b.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|