acdream/docs/research/2026-07-17-local-player-root-motion-start-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

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.