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>
95 lines
3.7 KiB
Markdown
95 lines
3.7 KiB
Markdown
# Local-player root-motion start — retail pseudocode (2026-07-17)
|
|
|
|
## Symptom
|
|
|
|
A short forward-key tap moved the local acdream body immediately while the
|
|
Ready-to-locomotion animation was still starting. The character therefore
|
|
glided before its legs began the corresponding movement.
|
|
|
|
## Retail oracle
|
|
|
|
Named Sept-2013 client:
|
|
|
|
- `CMotionInterp::DoInterpretedMotion @ 0x00528360`
|
|
- `CMotionInterp::apply_interpreted_movement @ 0x00528600`
|
|
- `CMotionInterp::apply_current_movement @ 0x00528870`
|
|
- `CMotionInterp::get_state_velocity @ 0x00527D50`
|
|
- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`
|
|
- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`
|
|
- `CPhysicsObj::get_velocity @ 0x005113C0`
|
|
|
|
Cross-checks:
|
|
|
|
- `docs/research/2026-07-02-inbound-motion-maps/map-ace-port.md`, which records
|
|
ACE's `PartArray.Update` into the shared offset frame before
|
|
`PositionManager.AdjustOffset`.
|
|
- `docs/research/2026-07-02-r3-motioninterp/r3-ace-motioninterp.md`, which
|
|
independently shows `apply_interpreted_movement` dispatching motions rather
|
|
than installing ordinary grounded velocity.
|
|
|
|
## Pseudocode
|
|
|
|
```text
|
|
on input motion edge(command, params):
|
|
CMotionInterp updates raw/interpreted state
|
|
apply_interpreted_movement dispatches style, forward, sidestep, and turn
|
|
CPhysicsObj::DoInterpretedMotion forwards those operations to MotionTableManager
|
|
// No ordinary grounded set_local_velocity occurs here.
|
|
|
|
CPhysicsObj::UpdatePositionInternal(dt):
|
|
offset = identity Frame
|
|
|
|
if visible and part_array exists:
|
|
part_array.Update(dt, offset)
|
|
|
|
if OnWalkable:
|
|
offset.origin *= object_scale
|
|
else:
|
|
offset.origin = zero
|
|
|
|
position_manager.adjust_offset(offset, dt)
|
|
|
|
candidate = current_position.frame combined with offset
|
|
candidate.origin += m_velocityVector * dt + acceleration * dt^2 / 2
|
|
m_velocityVector += acceleration * dt
|
|
return candidate
|
|
|
|
CPhysicsObj::UpdateObjectInternal(dt):
|
|
old = current_position
|
|
candidate = UpdatePositionInternal(dt)
|
|
transition = sweep(old, candidate)
|
|
if transition succeeds:
|
|
cached_velocity = offset(old, resolved_position) / dt
|
|
SetPositionInternal(transition)
|
|
else:
|
|
cached_velocity = zero
|
|
```
|
|
|
|
`get_state_velocity` is not the ordinary grounded locomotion driver. Retail
|
|
uses it for leave-ground/jump velocity and related state queries. Grounded
|
|
translation comes from the complete `CSequence::update` Frame, including the
|
|
authored Ready-to-Walk/Run link timing. `CPhysicsObj::get_velocity` returns the
|
|
resolved `cached_velocity`, distinct from the integrator's
|
|
`m_velocityVector`.
|
|
|
|
## Port shape
|
|
|
|
1. The local player's input edge must reach `MotionTableManager` before the
|
|
current object's animation advance.
|
|
2. `AnimationSequencer.Advance(dt, rootFrame)` runs exactly once for the local
|
|
owner and publishes both the pose and `rootFrame.Origin`.
|
|
3. `PlayerMovementController` accumulates that local displacement until the
|
|
existing 30 Hz physics quantum runs.
|
|
4. While grounded, scale it by the server object scale and seed the same
|
|
`MotionDeltaFrame` passed through `PositionManager.AdjustOffset`.
|
|
5. The transition sweep resolves the combined delta and computes cached
|
|
velocity from the realized displacement.
|
|
6. Do not install `get_state_velocity()` as ordinary grounded body velocity
|
|
when a retail root-motion source is attached. Keep that function for
|
|
leave-ground/jump behavior.
|
|
7. The animation pass consumes the already-advanced pose instead of advancing
|
|
the local sequence a second time.
|
|
|
|
The existing manual yaw path remains separate; this slice consumes the root
|
|
Frame's translation only. Full root-orientation ownership remains part of the
|
|
registered turn/omega divergence and is not guessed here.
|