acdream/src/AcDream.Core/Physics/Motion/PositionManager.cs
Erik 9e17554ed4 fix(physics): P5 commit 3 - retire TS-35, close #167 (ConstraintManager leash)
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 commits e0629145 /
  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).
2026-07-30 12:12:29 +02:00

102 lines
4.6 KiB
C#

using System;
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R5 — port of retail's <c>PositionManager</c> facade (acclient.h:30952,
/// struct #3468; decomp 0x00555160-0x005553d0,
/// <c>r5-positionmanager-sticky-decomp.md</c>). A thin fan-out over three
/// sub-managers: Interpolation, Sticky, Constraint. Owned 1:1 by the entity's
/// <see cref="IPhysicsObjHost"/> (retail <c>CPhysicsObj::position_manager</c>,
/// lazily created).
///
/// <para><b>Interpolation note:</b> retail's <c>adjust_offset</c> chains
/// Interpolation → Sticky → Constraint. acdream's interpolation stage lives in
/// <see cref="RemoteMotionCombiner"/> (the R5-renamed remote-motion combiner,
/// formerly the misnamed <c>Physics.PositionManager</c>) and is NOT chained
/// here in V1 — this facade owns only the two R5 targets (Sticky retires TS-39;
/// Constraint is structural — see <see cref="ConstraintManager"/>). Folding the
/// combiner in as the interp stage is a wiring-slice cleanup.</para>
/// </summary>
public sealed class PositionManager
{
private readonly IPhysicsObjHost _host;
// Lazily created (retail: sticky on first StickTo, constraint on first
// ConstrainTo — 0x00555230 / 0x00555280).
private StickyManager? _sticky;
private ConstraintManager? _constraint;
public PositionManager(IPhysicsObjHost host)
=> _host = host ?? throw new ArgumentNullException(nameof(host));
/// <summary>Exposed for wiring/tests — the lazily-created sub-managers
/// (null until first use).</summary>
public StickyManager? Sticky => _sticky;
public ConstraintManager? Constraint => _constraint;
/// <summary>Retail <c>PositionManager::StickTo</c> (0x00555230) — lazily
/// create the <see cref="StickyManager"/> and begin following
/// <paramref name="objectId"/>.</summary>
public void StickTo(uint objectId, float radius, float height)
{
_sticky ??= new StickyManager(_host);
_sticky.StickTo(objectId, radius, height);
}
/// <summary>Retail <c>PositionManager::UnStick</c> (0x005551e0) — forward
/// to the sticky sub-manager if it exists.</summary>
public void UnStick() => _sticky?.UnStick();
/// <summary>Retail <c>PositionManager::GetStickyObjectID</c>
/// (0x00555270).</summary>
public uint GetStickyObjectId() => _sticky?.TargetId ?? 0u;
/// <summary>Retail <c>PositionManager::ConstrainTo</c> (0x00555280) —
/// lazily create the <see cref="ConstraintManager"/> and arm the leash.
/// Armed as of Campaign P P5 (#167) at every accepted-position seam — see
/// <see cref="ConstraintManager"/>'s class doc for the call sites.</summary>
public void ConstrainTo(Position anchor, float startDistance, float maxDistance)
{
_constraint ??= new ConstraintManager(_host);
_constraint.ConstrainTo(anchor, startDistance, maxDistance);
}
/// <summary>Retail <c>PositionManager::UnConstrain</c>
/// (0x005552b0).</summary>
public void UnConstrain() => _constraint?.UnConstrain();
/// <summary>Retail <c>PositionManager::IsFullyConstrained</c>
/// (0x005552c0) — false when no constraint sub-manager exists.</summary>
public bool IsFullyConstrained() => _constraint?.IsFullyConstrained() ?? false;
/// <summary>
/// Retail <c>PositionManager::HandleUpdateTarget</c> (0x005553d0) — only
/// the sticky sub-manager cares about live target positions (interpolation
/// and constraint don't). Fanned out from
/// <c>CPhysicsObj::HandleUpdateTarget</c>.
/// </summary>
public void HandleUpdateTarget(TargetInfo info) => _sticky?.HandleUpdateTarget(info);
/// <summary>
/// Retail <c>PositionManager::adjust_offset</c> (0x00555190) — chains the
/// sub-managers' contributions into the SAME <paramref name="offset"/>
/// accumulator, in retail order. Retail runs Interpolation → Sticky →
/// Constraint; acdream's interpolation stays in the separate remote-motion
/// combiner (see class note), so this chains Sticky → Constraint only.
/// Constraint is LAST because it clamps the already-composed displacement.
/// </summary>
public void AdjustOffset(MotionDeltaFrame offset, double quantum)
{
_sticky?.AdjustOffset(offset, quantum);
_constraint?.AdjustOffset(offset, quantum);
}
/// <summary>
/// Retail <c>PositionManager::UseTime</c> (0x00555160) — per-tick pump.
/// Retail order Interpolation → Constraint → Sticky; acdream runs the two
/// owned managers (constraint's UseTime is a retail no-op, so effectively
/// just the sticky 1 s watchdog).
/// </summary>
public void UseTime() => _sticky?.UseTime();
}