# Remote root-motion reconciliation — retail pseudocode **Date:** 2026-07-17 **Symptom:** an observed character walks forward, blips backward, then repeats. **Oracle:** Sept 2013 named retail decomp, installed retail DAT, ACE physics port. ## Named retail path ### `CPhysicsObj::UpdatePositionInternal` — `0x00512C30` ```text offset = identity Frame if object is visible: if partArray exists: partArray.Update(dt, offset) // CPartArray::Update 0x00517DB0 calls // CSequence::update(sequence, dt, offset) if OnWalkable: offset.origin *= objectScale else: offset.origin = zero if positionManager exists: positionManager.adjust_offset(offset, dt) // InterpolationManager may REPLACE offset.origin with the // queue-head catch-up delta. newFrame = currentWorldFrame combined with offset if object is visible: UpdatePhysicsInternal(dt, newFrame) process_hooks() ``` ### `add_motion` — `0x005224B0` ```text if motionData exists: sequence.velocity = motionData.velocity * speed sequence.omega = motionData.omega * speed append every authored AnimData * speed ``` There is no substitution of `CMotionInterp::get_state_velocity` constants into `CSequence::velocity`. Those constants drive body physics in the interpreted movement path; they are not animation-root data. ### `InterpolationManager::adjust_offset` — `0x00555D30` ```text if queue empty, no physics object, or object lacks Contact: leave offset unchanged if queue head is within 0.05 m: complete the node leave offset unchanged maxSpeed = minterp.adjustedMaxSpeed * 2 if maxSpeed is effectively zero: maxSpeed = 7.5 m/s offset.origin = directionToHead * min(maxSpeed * dt, distanceToHead) ``` The queue catch-up replaces the CSequence displacement while active. Once the head is reached, the literal CSequence displacement remains. ## Installed-DAT result `client_portal.dat` MotionTable `0x09000001` (Humanoid), NonCombat style `0x8000003D`: - WalkForward `0x40000005`: `MotionData.Velocity == (0,0,0)` - RunForward `0x40000007`: `MotionData.Velocity == (0,0,0)` Pinned by `HumanoidMotionTableRootMotionTests`. ## acdream correction Previous behavior synthesized `WalkAnimSpeed × speed` or `RunAnimSpeed × speed` into `CSequence.Velocity`, then used that guessed value after the interpolation queue reached its latest waypoint. The body could run past server truth; the next UpdatePosition queued a backward correction, creating the repeating blip. Correct frame order: ```text rootDelta = identity Frame partTransforms = sequencer.Advance(dt, rootDelta) worldDelta = RemoteMotionCombiner( rootDelta.origin, interpolationQueue, positionManager) sweep and commit worldDelta publish root and part transforms ``` The client now consumes the complete root delta produced by the already-ported `CSequence` (authored PosFrames plus literal MotionData velocity). It does not derive remote translation from a command name or packet cadence. ## Cross-reference - ACE `PhysicsObj.UpdatePositionInternal`: PartArray update → PositionManager adjustment → world-frame combine, matching retail ordering. - ACE `InterpolationManager.adjust_offset`: queue correction replaces the supplied frame origin and uses the same 0.05 m completion radius and 2× speed. - Earlier live retail cdb trace: `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` — walking remotes use queued position catch-up; their body velocity is not synthesized from UpdatePosition.