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>
98 lines
5.2 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|