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>
3.5 KiB
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
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
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
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.