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:
parent
2c8620ea94
commit
78e163a41e
15 changed files with 1173 additions and 212 deletions
254
tests/AcDream.Core.Net.Tests/Messages/MoveToStateGoldenTests.cs
Normal file
254
tests/AcDream.Core.Net.Tests/Messages/MoveToStateGoldenTests.cs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
using System;
|
||||
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>
|
||||
/// Full-message golden-byte tests for the refactored <see cref="MoveToState.Build"/>
|
||||
/// (D1 default-difference RawMotionState packing + D3 trailing byte), porting
|
||||
/// <c>MoveToStatePack::Pack</c> (0x005168f0, decomp lines ~284694-284722).
|
||||
/// Confirmed verbatim against the Ghidra decompile-by-address bridge during
|
||||
/// this slice (2026-06-30):
|
||||
///
|
||||
/// <code>
|
||||
/// MoveToStatePack::Pack:
|
||||
/// RawMotionState::Pack(...)
|
||||
/// Position::Pack(...)
|
||||
/// instance_timestamp(u16), server_control_timestamp(u16),
|
||||
/// teleport_timestamp(u16), force_position_ts(u16)
|
||||
/// trailing byte = ((longjump_mode != 0) ? 0x02 : 0) | (contact != 0 ? 0x01 : 0)
|
||||
/// ALIGN_PTR
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public class MoveToStateGoldenTests
|
||||
{
|
||||
private static readonly Vector3 Pos = new(96f, 96f, 50f);
|
||||
private static readonly Quaternion Rot = Quaternion.Identity;
|
||||
|
||||
[Fact]
|
||||
public void Build_DefaultRawMotionState_FlagsAreZero_EnvelopePlusPositionPlusTimestampsPlusTrailingByte()
|
||||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 1,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
// 12 (envelope) + 4 (flags=0, no fields) + 32 (Position) + 8 (timestamps)
|
||||
// + 1 (trailing byte) = 57, aligned to 60.
|
||||
Assert.Equal(60, body.Length);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.Equal(0u, flags);
|
||||
|
||||
uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16));
|
||||
Assert.Equal(0xA9B40001u, cellId);
|
||||
|
||||
// trailing byte at 12+4+32+8 = 56: contact=true, longjump=false -> 0x01
|
||||
Assert.Equal(0x01, body[56]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WalkForward_OmitsForwardSpeedDefault()
|
||||
{
|
||||
// Walk-forward at default speed 1.0 -> forward_speed bit OMITTED (D1 fix).
|
||||
var state = new RawMotionState
|
||||
{
|
||||
CurrentHoldKey = HoldKey.None, // default, omitted
|
||||
ForwardCommand = 0x45000005u, // WalkForward
|
||||
ForwardHoldKey = HoldKey.None, // differs from Invalid -> set
|
||||
ForwardSpeed = 1.0f, // default, omitted
|
||||
};
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 2,
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.Equal(0x0000000Cu, flags); // ForwardCommand | ForwardHoldKey only
|
||||
|
||||
// RawMotionState body = flags(4) + fwd_cmd(4) + fwd_holdkey(4) = 12.
|
||||
uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12 + 12));
|
||||
Assert.Equal(0xA9B40001u, cellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RunForward_IncludesHoldKeyAndSpeed()
|
||||
{
|
||||
var state = new RawMotionState
|
||||
{
|
||||
CurrentHoldKey = HoldKey.Run,
|
||||
ForwardCommand = 0x44000007u, // RunForward
|
||||
ForwardHoldKey = HoldKey.Run,
|
||||
ForwardSpeed = 2.94f,
|
||||
};
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 3,
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.Equal(0x0000001Du, flags); // holdkey | fwd_cmd | fwd_holdkey | fwd_speed
|
||||
|
||||
// RawMotionState body = flags(4) + holdkey(4) + fwd_cmd(4) + fwd_holdkey(4) + fwd_speed(4) = 20.
|
||||
uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12 + 20));
|
||||
Assert.Equal(0xA9B40001u, cellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Sidestep_SetsSidestepBitsOnly()
|
||||
{
|
||||
var state = new RawMotionState
|
||||
{
|
||||
SidestepCommand = 0x6500000Fu, // SideStepRight
|
||||
SidestepHoldKey = HoldKey.None,
|
||||
SidestepSpeed = 1.0f, // default -> omitted
|
||||
};
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 4,
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.Equal(0x00000060u, flags); // SidestepCommand(0x20) | SidestepHoldKey(0x40)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Turn_SetsTurnBitsOnly()
|
||||
{
|
||||
var state = new RawMotionState
|
||||
{
|
||||
TurnCommand = 0x6500000Du, // TurnRight
|
||||
TurnHoldKey = HoldKey.None,
|
||||
TurnSpeed = 1.0f, // default -> omitted
|
||||
};
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 5,
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.Equal(0x00000300u, flags); // TurnCommand(0x100) | TurnHoldKey(0x200)
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, 0x00)]
|
||||
[InlineData(true, false, 0x01)]
|
||||
[InlineData(false, true, 0x02)]
|
||||
[InlineData(true, true, 0x03)]
|
||||
public void Build_TrailingByte_AllFourContactLongjumpCombinations(bool contact, bool standingLongjump, byte expected)
|
||||
{
|
||||
// MoveToStatePack::Pack trailing byte:
|
||||
// ((longjump_mode != 0) ? 0x02 : 0) | (contact != 0 ? 0x01 : 0)
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 6,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: contact,
|
||||
standingLongjump: standingLongjump);
|
||||
|
||||
// flags(4) + Position(32) + timestamps(8) = 44; trailing byte at 12+44=56.
|
||||
Assert.Equal(expected, body[56]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_TimestampOrder_MatchesMoveToStatePackPack()
|
||||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 7,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0x1111,
|
||||
serverControlSequence: 0x2222,
|
||||
teleportSequence: 0x3333,
|
||||
forcePositionSequence: 0x4444,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
// 12 (envelope) + 4 (flags) + 32 (Position) = 48.
|
||||
int tsOffset = 48;
|
||||
ushort instance = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(tsOffset));
|
||||
ushort serverControl = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(tsOffset + 2));
|
||||
ushort teleport = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(tsOffset + 4));
|
||||
ushort forcePosition = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(tsOffset + 6));
|
||||
|
||||
Assert.Equal((ushort)0x1111, instance);
|
||||
Assert.Equal((ushort)0x2222, serverControl);
|
||||
Assert.Equal((ushort)0x3333, teleport);
|
||||
Assert.Equal((ushort)0x4444, forcePosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_IsAlignedTo4Bytes()
|
||||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 8,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Pos,
|
||||
rotation: Rot,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contact: true,
|
||||
standingLongjump: false);
|
||||
|
||||
Assert.Equal(0, body.Length % 4);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue