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>
This commit is contained in:
Erik 2026-06-30 22:30:01 +02:00
parent 2c8620ea94
commit 78e163a41e
15 changed files with 1173 additions and 212 deletions

View file

@ -129,11 +129,23 @@ public enum WeenieError : uint
// ── Motion state structs ───────────────────────────────────────────────────────
/// <summary>
/// Raw (network-derived) motion state for the local player.
/// Raw (network-derived) motion state for the local player, as consumed by
/// the (still-approximate) <see cref="MotionInterpreter"/> input-state path.
/// Struct layout in chunk_00520000 starts at offset +0x14 (struct field +0x20 =
/// ForwardCommand, +0x28 = ForwardSpeed, etc.).
///
/// <para>
/// Renamed from <c>RawMotionState</c> during the L.2b wire-parity slice
/// (2026-06-30) to free that name for the retail-faithful, full-field
/// <c>AcDream.Core.Physics.RawMotionState</c> (mirrors
/// <c>acclient.h RawMotionState::PackBitfield</c>) used by the outbound
/// wire packer. This partial type is D6/Phase-2 territory (the
/// <c>adjust_motion</c>/<c>apply_raw_movement</c> input-state port) —
/// out of scope for this slice. It is expected to be folded into or
/// replaced by the retail-faithful type when D6 lands.
/// </para>
/// </summary>
public struct RawMotionState
public struct LegacyRawMotionState
{
/// <summary>Forward/backward motion command (offset +0x20).</summary>
public uint ForwardCommand;
@ -149,7 +161,7 @@ public struct RawMotionState
public float TurnSpeed;
/// <summary>Initialize to the idle/ready state (1.0 speed, Ready command).</summary>
public static RawMotionState Default() => new()
public static LegacyRawMotionState Default() => new()
{
ForwardCommand = MotionCommand.Ready,
ForwardSpeed = 1.0f,
@ -261,7 +273,7 @@ public sealed class MotionInterpreter
public IWeenieObject? WeenieObj { get; set; }
/// <summary>Raw (network-derived) motion state (struct offsets +0x14..+0x44).</summary>
public RawMotionState RawState;
public LegacyRawMotionState RawState;
/// <summary>Interpreted motion state derived from raw (struct offsets +0x44..+0x7C).</summary>
public InterpretedMotionState InterpretedState;
@ -318,7 +330,7 @@ public sealed class MotionInterpreter
public MotionInterpreter()
{
RawState = RawMotionState.Default();
RawState = LegacyRawMotionState.Default();
InterpretedState = InterpretedMotionState.Default();
}
@ -326,7 +338,7 @@ public sealed class MotionInterpreter
{
PhysicsObj = physicsObj;
WeenieObj = weenieObj;
RawState = RawMotionState.Default();
RawState = LegacyRawMotionState.Default();
InterpretedState = InterpretedMotionState.Default();
}