using System.Numerics;
using AcDream.Core.Physics.Motion;
namespace AcDream.Core.Physics;
///
/// Per-frame combiner for remote-entity motion: the root-motion delta emitted
/// by CSequence::update + InterpolationManager catch-up correction.
/// Pure function — no side effects or hidden state.
///
/// Mirrors retail CPhysicsObj::UpdatePositionInternal (0x00512C30):
/// CPartArray writes one complete local Frame, then PositionManager mutates
/// that same Frame. Active interpolation replaces it with
/// Position::subtract2; later managers receive the result.
///
/// The animation root motion is the complete body-local Frame.Origin
/// accumulated by CPartArray::Update: authored PosFrames plus the
/// literal sequence velocity. We rotate that delta by the body's orientation
/// to get world space. It must not be reconstructed from the interpreted
/// command; the retail Humanoid table carries zero sequence velocity for Walk
/// and Run, and retail moves observed characters through the interpolation
/// queue instead.
///
/// Renamed R5 (was PositionManager): this class is only the
/// InterpolationManager-composition portion of retail's
/// PositionManager::adjust_offset — NOT the retail PositionManager
/// facade. The faithful facade (Sticky/Constraint, owned per entity) is
/// . The name was freed to remove the
/// ambiguity that broke every file importing both
/// AcDream.Core.Physics and AcDream.Core.Physics.Motion.
///
public sealed class RemoteMotionCombiner
{
///
/// Compose retail's complete per-object delta frame. Interpolation, when
/// active, replaces the PartArray frame via
/// Position::subtract2; otherwise the authored root frame remains.
///
/// AD-10, retired 2026-08-06. This method used to accept a
/// terrainNormal and project the composed world-space root motion
/// onto it whenever interpolation did not overwrite. That was an EXTRA
/// projection: retail projects the per-sub-step offset onto
/// collision_info.contact_plane INSIDE the sweep
/// (CTransition::adjust_offset 0x0050a370,
/// pc:272271-272393), acdream ports that in
/// Transition.AdjustOffset (structurally exact, with exactly two
/// filed divergences — AD-65 and AD-66), and remote bodies do run that sweep.
/// The extra copy also sampled the wrong surface — a single-point
/// XY-only terrain lookup, blind to buildings, EnvCells and statics — so
/// on a walkable NON-terrain surface it applied the plane of the ground
/// far below. Removing it left the measured trajectory unchanged.
///
/// true when interpolation replaced the root frame.
public bool ComposeOffset(
double dt,
Vector3 currentBodyPosition,
Quaternion ori,
MotionDeltaFrame rootMotionLocalFrame,
InterpolationManager interp,
float maxSpeed,
MotionDeltaFrame output,
bool inContact = true)
{
ArgumentNullException.ThrowIfNull(rootMotionLocalFrame);
ArgumentNullException.ThrowIfNull(interp);
ArgumentNullException.ThrowIfNull(output);
output.Origin = rootMotionLocalFrame.Origin;
output.Orientation = rootMotionLocalFrame.Orientation;
bool interpolationOverwrote = interp.AdjustOffset(
dt,
currentBodyPosition,
ori,
maxSpeed,
output,
inContact);
return interpolationOverwrote;
}
///
/// Compute the per-frame world-space delta to add to body.Position.
///
/// Per-frame delta time, seconds.
/// Body's current world-space position.
///
/// Complete body-local displacement accumulated by this frame's
/// CSequence::update. This is already a delta for the current
/// quantum, not a velocity to multiply by .
///
/// Body orientation; used to rotate root motion from body-local to world.
/// The remote's InterpolationManager (for AdjustOffset call).
/// From MotionInterpreter.GetMaxSpeed() — passed to AdjustOffset for the catch-up clamp.
public Vector3 ComputeOffset(
double dt,
Vector3 currentBodyPosition,
Vector3 rootMotionLocalDelta,
Quaternion ori,
InterpolationManager interp,
float maxSpeed)
{
// Retail-faithful per-frame combiner. Mirrors
// CPhysicsObj::UpdatePositionInternal (acclient @ 0x00512c30) +
// InterpolationManager::adjust_offset (@ 0x00555d30):
//
// 1. CPartArray::Update writes rootOffset (animation root motion)
// into the per-tick Frame.
// 2. PositionManager::adjust_offset → InterpolationManager::adjust_offset
// either:
// a) RETURNS EARLY when distance(body, head) < 0.05m
// (NodeCompleted; arg2 unmodified) — body uses root motion.
// b) OVERWRITES arg2 with `direction × min(catchUpSpeed × dt,
// distance)` when body is far from head — catch-up REPLACES
// root motion for this frame.
//
// It is NOT additive. Our prior port added rootMotion + correction
// every frame, which stacked the animation push (≈ RunAnimSpeed ×
// speedMod, ≈ 11.7 m/s) on top of the queue catch-up (capped at
// ≈ 23.5 m/s) so the body advanced at up to ~3× the server's
// broadcast pace and the head-behind-body case produced a backward
// correction every UP — the visible 1-Hz blip the user reported.
//
// AdjustOffset returns Vector3.Zero in two cases mapped to retail's
// early-return: empty queue OR distance < DesiredDistance (0.05m).
// In both, body falls back to animation root motion.
var root = new MotionDeltaFrame
{
Origin = rootMotionLocalDelta,
};
var output = new MotionDeltaFrame();
ComposeOffset(
dt,
currentBodyPosition,
ori,
root,
interp,
maxSpeed,
output);
// AD-10 (retired 2026-08-06): a second copy of the deleted terrain
// projection used to run here. See ComposeOffset's summary.
return Vector3.Transform(output.Origin, ori);
}
}