acdream/src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs
Erik 78e163a41e feat(L.2b): outbound movement wire parity — RawMotionState default-difference, JumpPack, contact byte
Ports the three retail outbound-movement packers verbatim (decomp-derived
golden bytes, confirmed via the Ghidra bridge, cross-checked vs holtburger):

- D1 — RawMotionState::Pack (0x0051ed10): new AcDream.Core.Physics.RawMotionState
  data type (11 fields + actions, retail defaults) + RawMotionStatePacker that
  sets a flag bit only when the field DIFFERS from its default. MoveToState.Build
  now takes a RawMotionState instead of presence-based nullable params, so the
  over-sent forwardSpeed=1.0 / currentHoldKey=None / default per-axis holdkeys are
  no longer emitted. num_actions packs into bits 11-15 (not "bits 11-31").
- D3 — MoveToStatePack::Pack (0x005168f0) trailing byte =
  (standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0); explicit contact/
  standingLongjump params (standingLongjump=false honestly until the feature lands).
- D4 — JumpAction rewritten to retail JumpPack::Pack (0x00516d10): extent,
  velocity, full Position, four u16 timestamps, align. Removed the spurious
  objectGuid/spellId u32s; Position is now packed (it was absent). Body 56 bytes.
- Position::Pack (0x005a9640) / Frame::Pack (0x00535130) verified already-correct
  (cellId, origin xyz, quaternion wxyz); locked with a golden test, no change.

GameWindow callers adapted minimally: build the RawMotionState from the existing
MovementResult values (behavior preserved except the intended D1 omissions) and
pass cellId/position/rotation to the Jump send. Pre-existing MotionInterpreter
placeholder struct RawMotionState renamed LegacyRawMotionState (D6/Phase-2 scope,
pure rename) to free the name for the retail-faithful type.

D5 audit: confirmed a real divergence — retail SendMovementEvent (0x006b4680)
stamps only last_sent_position_time after an MTS while SendPositionEvent
(0x006b4770) stamps all three; acdream's NotePositionSent stamps all three on
both paths. Left unchanged (comments added at both call sites), recorded as
register TS-33, deferred to a dedicated cadence-port slice.

Tests: RawMotionStatePackTests / MoveToStateGoldenTests / JumpActionTests /
PositionPackTests + updated MoveToStateTests / AutonomousPositionTests. Full
suite green (Core.Net.Tests 372, full solution 3228 passed / 4 pre-existing skips).

Register: TS-24/TS-25 refreshed (packer now supports actions/style; runtime
emission still deferred), TS-33 added. Roadmap L.2b shipped note added.
Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md (2-6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:30:01 +02:00

98 lines
5.2 KiB
C#

using AcDream.Core.Net.Packets;
using AcDream.Core.Physics;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Ports retail's <c>RawMotionState::Pack</c> verbatim
/// (<c>0x0051ed10</c>, decomp lines ~293761-294013; bitfield layout
/// <c>acclient.h RawMotionState::PackBitfield</c>, line 46474). Confirmed
/// against the Ghidra decompile-by-address bridge during this slice
/// (2026-06-30).
///
/// <para>
/// <b>D1 fix.</b> Retail does NOT set a field's bit merely because the
/// caller supplied a value — it compares every field against its retail
/// DEFAULT (<see cref="RawMotionState.Default"/>) and sets the bit (and
/// emits the field) only when the live value DIFFERS. This packer mirrors
/// that default-difference comparison exactly, field by field, in bit
/// order.
/// </para>
///
/// <para>
/// Flags dword layout (bits 0-15 only; bits 16-31 are unused — retail's
/// <c>num_actions</c> is a 5-bit field occupying bits 11-15, not "the rest
/// of the dword"):
/// </para>
/// <list type="table">
/// <item><term>0x001</term><description>current_holdkey != HoldKey.None</description></item>
/// <item><term>0x002</term><description>current_style != 0x8000003D</description></item>
/// <item><term>0x004</term><description>forward_command != 0x41000003</description></item>
/// <item><term>0x008</term><description>forward_holdkey != HoldKey.Invalid</description></item>
/// <item><term>0x010</term><description>forward_speed != 1.0f</description></item>
/// <item><term>0x020</term><description>sidestep_command != 0</description></item>
/// <item><term>0x040</term><description>sidestep_holdkey != HoldKey.Invalid</description></item>
/// <item><term>0x080</term><description>sidestep_speed != 1.0f</description></item>
/// <item><term>0x100</term><description>turn_command != 0</description></item>
/// <item><term>0x200</term><description>turn_holdkey != HoldKey.Invalid</description></item>
/// <item><term>0x400</term><description>turn_speed != 1.0f</description></item>
/// <item><term>0xF800</term><description>num_actions (bits 11-15, count not values)</description></item>
/// </list>
/// </summary>
public static class RawMotionStatePacker
{
private const uint FlagCurrentHoldKey = 0x001u;
private const uint FlagCurrentStyle = 0x002u;
private const uint FlagForwardCommand = 0x004u;
private const uint FlagForwardHoldKey = 0x008u;
private const uint FlagForwardSpeed = 0x010u;
private const uint FlagSidestepCommand = 0x020u;
private const uint FlagSidestepHoldKey = 0x040u;
private const uint FlagSidestepSpeed = 0x080u;
private const uint FlagTurnCommand = 0x100u;
private const uint FlagTurnHoldKey = 0x200u;
private const uint FlagTurnSpeed = 0x400u;
private const int NumActionsShift = 11;
public static void Pack(PacketWriter w, RawMotionState state)
{
var defaults = RawMotionState.Default;
uint flags = 0u;
if (state.CurrentHoldKey != defaults.CurrentHoldKey) flags |= FlagCurrentHoldKey;
if (state.CurrentStyle != defaults.CurrentStyle) flags |= FlagCurrentStyle;
if (state.ForwardCommand != defaults.ForwardCommand) flags |= FlagForwardCommand;
if (state.ForwardHoldKey != defaults.ForwardHoldKey) flags |= FlagForwardHoldKey;
if (state.ForwardSpeed != defaults.ForwardSpeed) flags |= FlagForwardSpeed;
if (state.SidestepCommand != defaults.SidestepCommand) flags |= FlagSidestepCommand;
if (state.SidestepHoldKey != defaults.SidestepHoldKey) flags |= FlagSidestepHoldKey;
if (state.SidestepSpeed != defaults.SidestepSpeed) flags |= FlagSidestepSpeed;
if (state.TurnCommand != defaults.TurnCommand) flags |= FlagTurnCommand;
if (state.TurnHoldKey != defaults.TurnHoldKey) flags |= FlagTurnHoldKey;
if (state.TurnSpeed != defaults.TurnSpeed) flags |= FlagTurnSpeed;
int numActions = state.Actions.Count;
flags |= (uint)(numActions << NumActionsShift);
w.WriteUInt32(flags);
if ((flags & FlagCurrentHoldKey) != 0) w.WriteUInt32((uint)state.CurrentHoldKey);
if ((flags & FlagCurrentStyle) != 0) w.WriteUInt32(state.CurrentStyle);
if ((flags & FlagForwardCommand) != 0) w.WriteUInt32(state.ForwardCommand);
if ((flags & FlagForwardHoldKey) != 0) w.WriteUInt32((uint)state.ForwardHoldKey);
if ((flags & FlagForwardSpeed) != 0) w.WriteFloat(state.ForwardSpeed);
if ((flags & FlagSidestepCommand) != 0) w.WriteUInt32(state.SidestepCommand);
if ((flags & FlagSidestepHoldKey) != 0) w.WriteUInt32((uint)state.SidestepHoldKey);
if ((flags & FlagSidestepSpeed) != 0) w.WriteFloat(state.SidestepSpeed);
if ((flags & FlagTurnCommand) != 0) w.WriteUInt32(state.TurnCommand);
if ((flags & FlagTurnHoldKey) != 0) w.WriteUInt32((uint)state.TurnHoldKey);
if ((flags & FlagTurnSpeed) != 0) w.WriteFloat(state.TurnSpeed);
foreach (var action in state.Actions)
{
w.WriteUInt16(action.Command);
ushort stampWord = (ushort)((action.Stamp & 0x7FFF) | (action.Autonomous ? 0x8000 : 0));
w.WriteUInt16(stampWord);
}
}
}