using System; namespace AcDream.Core.Physics.Motion; /// /// R5 — port of retail's PositionManager facade (acclient.h:30952, /// struct #3468; decomp 0x00555160-0x005553d0, /// r5-positionmanager-sticky-decomp.md). A thin fan-out over three /// sub-managers: Interpolation, Sticky, Constraint. Owned 1:1 by the entity's /// (retail CPhysicsObj::position_manager, /// lazily created). /// /// Interpolation note: retail's adjust_offset chains /// Interpolation → Sticky → Constraint. acdream's interpolation stage lives in /// (the R5-renamed remote-motion combiner, /// formerly the misnamed Physics.PositionManager) and is NOT chained /// here in V1 — this facade owns only the two R5 targets (Sticky retires TS-39; /// Constraint is structural — see ). Folding the /// combiner in as the interp stage is a wiring-slice cleanup. /// 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)); /// Exposed for wiring/tests — the lazily-created sub-managers /// (null until first use). public StickyManager? Sticky => _sticky; public ConstraintManager? Constraint => _constraint; /// Retail PositionManager::StickTo (0x00555230) — lazily /// create the and begin following /// . public void StickTo(uint objectId, float radius, float height) { _sticky ??= new StickyManager(_host); _sticky.StickTo(objectId, radius, height); } /// Retail PositionManager::UnStick (0x005551e0) — forward /// to the sticky sub-manager if it exists. public void UnStick() => _sticky?.UnStick(); /// Retail PositionManager::GetStickyObjectID /// (0x00555270). public uint GetStickyObjectId() => _sticky?.TargetId ?? 0u; /// Retail PositionManager::ConstrainTo (0x00555280) — /// lazily create the and arm the leash. /// (Unused in acdream — no arming call site; see /// .) public void ConstrainTo(Position anchor, float startDistance, float maxDistance) { _constraint ??= new ConstraintManager(_host); _constraint.ConstrainTo(anchor, startDistance, maxDistance); } /// Retail PositionManager::UnConstrain /// (0x005552b0). public void UnConstrain() => _constraint?.UnConstrain(); /// Retail PositionManager::IsFullyConstrained /// (0x005552c0) — false when no constraint sub-manager exists. public bool IsFullyConstrained() => _constraint?.IsFullyConstrained() ?? false; /// /// Retail PositionManager::HandleUpdateTarget (0x005553d0) — only /// the sticky sub-manager cares about live target positions (interpolation /// and constraint don't). Fanned out from /// CPhysicsObj::HandleUpdateTarget. /// public void HandleUpdateTarget(TargetInfo info) => _sticky?.HandleUpdateTarget(info); /// /// Retail PositionManager::adjust_offset (0x00555190) — chains the /// sub-managers' contributions into the SAME /// 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. /// public void AdjustOffset(MotionDeltaFrame offset, double quantum) { _sticky?.AdjustOffset(offset, quantum); _constraint?.AdjustOffset(offset, quantum); } /// /// Retail PositionManager::UseTime (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). /// public void UseTime() => _sticky?.UseTime(); }