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>
115 lines
3.5 KiB
Markdown
115 lines
3.5 KiB
Markdown
# 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.
|