acdream/docs/research/2026-07-17-combat-movement-outbound-style-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

84 lines
3.1 KiB
Markdown

# Retail combat-style ownership in outbound movement
## Symptom
The local acdream player remains visually in combat while moving, but a retail
observer sees the player leave the combat stance and locomote in NonCombat.
## Named-retail oracle
`CommandInterpreter::SendMovementEvent @ 0x006B4680` does not construct a new
axis-only motion object. It obtains the player's canonical raw state and passes
that exact object to the packet constructor:
```text
player = CommandInterpreter.player
raw = player.InqRawMotionState()
if player != null and smartbox != null and raw != null and autonomy_level != 0:
packet = MoveToStatePack(
raw,
player.position,
player is Contact+OnWalkable,
player.minterp.standing_longjump,
player movement timestamps)
SendMoveToStateEvent(packet)
```
`CPhysicsObj::InqRawMotionState @ 0x0050FDE0` returns
`MovementManager::InqRawMotionState`; stance and movement axes therefore have
one canonical owner.
`RawMotionState::Pack @ 0x0051ED10` compares `current_style` with the retail
default `MotionStance_NonCombat` (`0x8000003D`):
```text
flags.bit1 = current_style != NonCombat
write flags
if flags.bit0: write current_holdkey
if flags.bit1: write current_style as uint32
if flags.bit2: write forward_command
... write the remaining axes and action queue ...
```
`RawMotionState::UnPack @ 0x0051EFC0` restores NonCombat when bit 1 is absent.
Omitting the bit is therefore an explicit NonCombat value, not “preserve the
receiver's previous stance.”
## Reference cross-checks
- ACE `Network/Motion/RawMotionState.cs` reads `RawMotionFlags.CurrentStyle`
(`0x2`) as a 32-bit stance.
- ACE `Network/Motion/MovementData.cs` copies that field into the interpreted
state relayed to observers only when the flag is present.
- acclientlib `UtilityBelt.Common/DataTypes.cs` independently documents that an
absent raw `CurrentStyle` defaults to `0x3D` NonCombat.
Thus an axis-only acdream packet necessarily makes ACE/retail reconstruct
NonCombat movement.
## acdream root cause and port
`PlayerMovementController` already owns retail's canonical
`MotionInterpreter.RawState`, and inbound server stance changes correctly
update `RawState.CurrentStyle`. However, `MovementResult` omitted that field and
`LocalPlayerOutboundController.BuildRawMotionState` rebuilt only the hold key
and three axes. Its new `RawMotionState` retained the constructor default
NonCombat, so `RawMotionStatePacker` correctly omitted the CurrentStyle bit.
Port the missing canonical field through the existing immutable frame result:
```text
MovementResult.CurrentStyle = MotionInterpreter.RawState.CurrentStyle
BuildRawMotionState(result):
raw.CurrentStyle = result.CurrentStyle
raw.CurrentHoldKey = result current hold level
raw.forward/side/turn = result axes
return raw
```
This preserves the existing update-thread ownership and packet scheduler. It
does not infer a stance from `CombatMode` or equipped items; the raw motion
state remains the oracle, exactly as retail. The still-unwired raw action queue
is separate and remains tracked by divergence TS-24.