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); } }