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>
63 lines
2.5 KiB
C#
63 lines
2.5 KiB
C#
using System.Buffers.Binary;
|
|
using System.Numerics;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Pins the shared WorldPosition/Position block byte order used by
|
|
/// <see cref="MoveToState"/>, <see cref="AutonomousPosition"/>, and
|
|
/// <see cref="JumpAction"/>: <c>Position::Pack</c> (0x005a9640) wraps
|
|
/// <c>cellId(u32)</c> then <c>Frame::Pack</c> (0x00535130) =
|
|
/// <c>origin.x/y/z(f32)</c> then <c>qw/qx/qy/qz(f32)</c>. Confirmed verbatim
|
|
/// against the Ghidra decompile-by-address bridge during this slice
|
|
/// (2026-06-30):
|
|
///
|
|
/// <code>
|
|
/// Position::Pack: objcell_id(u32), Frame::Pack(...)
|
|
/// Frame::Pack: m_fOrigin.x/y/z(f32 x3), qw(f32), qx(f32), qy(f32), qz(f32)
|
|
/// </code>
|
|
///
|
|
/// 32 bytes total (4 + 12 + 16). This was already correct pre-slice — these
|
|
/// tests lock the byte order with a golden-value assertion rather than
|
|
/// changing behavior.
|
|
/// </summary>
|
|
public class PositionPackTests
|
|
{
|
|
[Fact]
|
|
public void MoveToState_PositionBlock_OrderIsCellIdThenOriginThenQuaternion()
|
|
{
|
|
var body = MoveToState.Build(
|
|
gameActionSequence: 1,
|
|
rawMotionState: RawMotionState.Default,
|
|
cellId: 0xA9B40001u,
|
|
position: new Vector3(1.5f, 2.5f, 3.5f),
|
|
rotation: new Quaternion(0.1f, 0.2f, 0.3f, 0.9f), // X,Y,Z,W ctor order
|
|
instanceSequence: 0,
|
|
serverControlSequence: 0,
|
|
teleportSequence: 0,
|
|
forcePositionSequence: 0);
|
|
|
|
// No motion state -> flags(4) only before the Position block at offset 16.
|
|
int off = 12 + 4;
|
|
uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(off));
|
|
float x = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 4));
|
|
float y = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 8));
|
|
float z = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 12));
|
|
float qw = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 16));
|
|
float qx = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 20));
|
|
float qy = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 24));
|
|
float qz = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(off + 28));
|
|
|
|
Assert.Equal(0xA9B40001u, cellId);
|
|
Assert.Equal(1.5f, x);
|
|
Assert.Equal(2.5f, y);
|
|
Assert.Equal(3.5f, z);
|
|
Assert.Equal(0.9f, qw);
|
|
Assert.Equal(0.1f, qx);
|
|
Assert.Equal(0.2f, qy);
|
|
Assert.Equal(0.3f, qz);
|
|
}
|
|
}
|