feat(physics): R5-V1 — port PositionManager/Sticky/Constraint + TargetManager (Core, unwired)
The retail movement-manager family the R4 MoveToManager port left as do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's PositionManager facade + StickyManager + ConstraintManager + the TargetManager voyeur system, with full conformance tests. NO wiring yet — purely additive, no behavior change. Wiring (retiring TS-39 sticky + AP-79 target adapter) is R5-V2/V3. New Core classes (src/AcDream.Core/Physics/Motion/): - StickyManager (0x00555400): follow-a-target steering. adjust_offset's dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0, follow speed ×5 / fallback 15) — speed-clamped signed-distance steer + bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown. - ConstraintManager (0x00556090): the server-position rubber-band leash. 90% IsFullyConstrained jump gate + grounded linear brake taper. Structural only — acdream never ARMS it (retail arms from SmartBox::HandleReceivedPosition, which acdream lacks, with two x87 constants BN elided). IsFullyConstrained stays false = TS-35 behavior; leash-arming + the unknown constants are a deferred issue. - PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out. - TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer voyeur subscription system (0.5 s throttle, 10 s staleness, send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A faithful superset of the AP-79 adapter — SetTarget subscribes ON the target; the target's HandleTargetting pushes updates back. - IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/ radius/contact/GetObjectA + target-tracking fan-out) the App wires per entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator. Supporting: - TargetInfo extended to the full retail 10-field struct (additive defaults keep the R4 4-arg call sites compiling). - MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall, GlobalToLocalVec. - Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion Combiner, freeing the name and removing the ambiguity that breaks every file importing both Physics + Physics.Motion (GameWindow will in V2/V3). Tests: 42 new conformance cases (Sticky/Constraint/Position facade + TargetManager incl. the full cross-entity voyeur round-trip). Full suite 4006 green (+2 skipped), no regressions. Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
517bbfdae4
commit
3d89446d98
25 changed files with 7279 additions and 12 deletions
120
src/AcDream.Core/Physics/Motion/ConstraintManager.cs
Normal file
120
src/AcDream.Core/Physics/Motion/ConstraintManager.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
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>Arming is UNPORTED in acdream (R5).</b> Retail arms the leash ONLY
|
||||
/// from <c>SmartBox::HandleReceivedPosition</c> (on every inbound server
|
||||
/// position packet) with two constants from
|
||||
/// <c>CPhysicsObj::GetStart/MaxConstraintDistance</c> whose values BN elided
|
||||
/// (x87 returns — unknown, need a cdb read). acdream's position reconciliation
|
||||
/// is not SmartBox, so nothing calls <see cref="ConstrainTo"/> — the leash
|
||||
/// stays disarmed and <see cref="IsFullyConstrained"/> stays false, matching
|
||||
/// register TS-35's current stub behavior. The class is ported for structural
|
||||
/// completeness of <see cref="PositionManager"/>; the leash-arming port + the
|
||||
/// two unknown constants are a deferred issue (port-plan §Constraint scope).</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. Always false
|
||||
/// while the leash is disarmed (acdream never arms it — see class note).
|
||||
/// </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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue