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

3.1 KiB

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:

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):

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:

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.