acdream/docs/research/2026-07-17-remote-root-motion-reconciliation-pseudocode.md
Erik 124e046976 fix(runtime): align portal and movement presentation
Port retail portal viewport projection and reveal behavior, preserve outbound combat style, drive remote and local grounded movement from authored CSequence root frames, and reuse the local prepared pose so animation hooks advance once.

User-verified portal, observer movement, combat stance, and short-tap locomotion gates. Release build passed with 5,767 tests and five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-17 08:48:27 +02:00

3.5 KiB
Raw Blame History

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::UpdatePositionInternal0x00512C30

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_motion0x005224B0

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_offset0x00555D30

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:

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.