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
|
|
@ -122,6 +122,69 @@ public class AutonomousPositionTests
|
|||
Assert.Equal(0, body[52]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_TimestampOrder_MatchesAutonomousPositionPackPack()
|
||||
{
|
||||
// AutonomousPositionPack::Pack (0x00516af0, confirmed via Ghidra
|
||||
// decompile-by-address during this slice 2026-06-30): after
|
||||
// Position::Pack (32 bytes), four u16 timestamps in order
|
||||
// instance_timestamp, server_control_timestamp, teleport_timestamp,
|
||||
// force_position_ts — then a CONTACT-ONLY byte (no longjump bit;
|
||||
// that bit only exists in MoveToStatePack), then ALIGN_PTR.
|
||||
var body = AutonomousPosition.Build(
|
||||
gameActionSequence: 8,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0x1111,
|
||||
serverControlSequence: 0x2222,
|
||||
teleportSequence: 0x3333,
|
||||
forcePositionSequence: 0x4444);
|
||||
|
||||
// 12 (envelope) + 32 (Position) = 44.
|
||||
int tsOffset = 44;
|
||||
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_ContactByte_IsContactOnly_NoLongjumpBit()
|
||||
{
|
||||
// contact != 0 -> byte = 1 (bool cast, not an OR'd bitmask like MTS).
|
||||
var bodyOnGround = AutonomousPosition.Build(
|
||||
gameActionSequence: 9,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
lastContact: 1);
|
||||
|
||||
Assert.Equal(1, bodyOnGround[52]);
|
||||
|
||||
var bodyAirborne = AutonomousPosition.Build(
|
||||
gameActionSequence: 10,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
lastContact: 0);
|
||||
|
||||
Assert.Equal(0, bodyAirborne[52]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ContainsIdentityRotation_AfterPosition()
|
||||
{
|
||||
|
|
|
|||
170
tests/AcDream.Core.Net.Tests/Messages/JumpActionTests.cs
Normal file
170
tests/AcDream.Core.Net.Tests/Messages/JumpActionTests.cs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-byte tests for the retail-faithful <see cref="JumpAction.Build"/>
|
||||
/// layout, porting <c>JumpPack::Pack</c> (0x00516d10, decomp lines
|
||||
/// ~284934-284963). Confirmed verbatim against the Ghidra decompile-by-address
|
||||
/// bridge (http://127.0.0.1:8081/decompile_function?address=0x00516d10)
|
||||
/// during this slice (2026-06-30):
|
||||
///
|
||||
/// <code>
|
||||
/// extent (f32), velocity.x/y/z (f32 x3), Position::Pack (cellId + Frame),
|
||||
/// instance_timestamp (u16), server_control_timestamp (u16),
|
||||
/// teleport_timestamp (u16), force_position_ts (u16), ALIGN_PTR.
|
||||
/// </code>
|
||||
///
|
||||
/// D4: retail does NOT pack an objectGuid or spellId. The pre-slice acdream
|
||||
/// code wrote two spurious trailing <c>u32 0</c> fields and omitted Position
|
||||
/// entirely — both are fixed here.
|
||||
/// </summary>
|
||||
public class JumpActionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_ProducesValidGameAction()
|
||||
{
|
||||
var body = JumpAction.Build(
|
||||
gameActionSequence: 9,
|
||||
extent: 0.5f,
|
||||
velocity: new Vector3(1f, 2f, 3f),
|
||||
cellId: 0xA9B40001u,
|
||||
position: new Vector3(96f, 96f, 50f),
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0);
|
||||
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0xF7B1u, opcode);
|
||||
|
||||
uint seq = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||||
Assert.Equal(9u, seq);
|
||||
|
||||
uint actionType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
|
||||
Assert.Equal(0xF61Bu, actionType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ExtentAndVelocity_FollowEnvelope()
|
||||
{
|
||||
var body = JumpAction.Build(
|
||||
gameActionSequence: 1,
|
||||
extent: 0.75f,
|
||||
velocity: new Vector3(1.5f, -2.5f, 9.81f),
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0);
|
||||
|
||||
// 12-byte envelope, then extent(4), vx(4), vy(4), vz(4).
|
||||
float extent = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(12));
|
||||
float vx = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(16));
|
||||
float vy = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(20));
|
||||
float vz = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(24));
|
||||
|
||||
Assert.Equal(0.75f, extent);
|
||||
Assert.Equal(1.5f, vx);
|
||||
Assert.Equal(-2.5f, vy);
|
||||
Assert.Equal(9.81f, vz);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PositionFollowsVelocity_CellIdThenOriginThenQuaternion()
|
||||
{
|
||||
var body = JumpAction.Build(
|
||||
gameActionSequence: 2,
|
||||
extent: 0f,
|
||||
velocity: Vector3.Zero,
|
||||
cellId: 0xDEADBEEFu,
|
||||
position: new Vector3(12.5f, 34.0f, 56.75f),
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0);
|
||||
|
||||
// 12 (envelope) + 4 (extent) + 12 (velocity) = offset 28 -> Position::Pack.
|
||||
int positionOffset = 28;
|
||||
uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(positionOffset));
|
||||
float x = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 4));
|
||||
float y = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 8));
|
||||
float z = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 12));
|
||||
float qw = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 16));
|
||||
float qx = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 20));
|
||||
float qy = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 24));
|
||||
float qz = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(positionOffset + 28));
|
||||
|
||||
Assert.Equal(0xDEADBEEFu, cellId);
|
||||
Assert.Equal(12.5f, x);
|
||||
Assert.Equal(34.0f, y);
|
||||
Assert.Equal(56.75f, z);
|
||||
Assert.Equal(1.0f, qw);
|
||||
Assert.Equal(0.0f, qx);
|
||||
Assert.Equal(0.0f, qy);
|
||||
Assert.Equal(0.0f, qz);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_TimestampsFollowPosition_InRetailOrder()
|
||||
{
|
||||
var body = JumpAction.Build(
|
||||
gameActionSequence: 3,
|
||||
extent: 0f,
|
||||
velocity: Vector3.Zero,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0x1111,
|
||||
serverControlSequence: 0x2222,
|
||||
teleportSequence: 0x3333,
|
||||
forcePositionSequence: 0x4444);
|
||||
|
||||
// Position::Pack is 32 bytes (cellId + Frame). Timestamps start at
|
||||
// 28 (extent/velocity/envelope) + 32 = 60.
|
||||
int tsOffset = 60;
|
||||
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_NoObjectGuidOrSpellId_JumpPackBodyLengthIs56()
|
||||
{
|
||||
// JumpPack::Pack body (everything AFTER the 12-byte GameAction
|
||||
// envelope): extent(4) + velocity(12) + Position(32) +
|
||||
// 4x u16 timestamps(8) = 56 bytes, already a multiple of 4 -> no
|
||||
// align padding. Total wire length = 12 (envelope) + 56 = 68.
|
||||
// Retail's JumpPack has NO objectGuid/spellId fields (D4) — the
|
||||
// pre-slice code's two spurious trailing u32 writes are gone.
|
||||
var body = JumpAction.Build(
|
||||
gameActionSequence: 4,
|
||||
extent: 0f,
|
||||
velocity: Vector3.Zero,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0);
|
||||
|
||||
Assert.Equal(56, body.Length - 12);
|
||||
Assert.Equal(68, body.Length);
|
||||
Assert.Equal(0, body.Length % 4);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,26 +2,36 @@ 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>
|
||||
/// Envelope + structural tests for <see cref="MoveToState.Build"/>. Golden
|
||||
/// byte-layout tests for the RawMotionState default-difference packing (D1)
|
||||
/// and the trailing contact/longjump byte (D3) live in
|
||||
/// <see cref="MoveToStateGoldenTests"/> and <see cref="RawMotionStatePackTests"/>.
|
||||
///
|
||||
/// D1/D3 refactor (2026-06-30): <c>Build</c> now takes a
|
||||
/// <see cref="RawMotionState"/> snapshot (matching retail's
|
||||
/// <c>CPhysicsObj::InqRawMotionState()</c>) instead of flat nullable
|
||||
/// per-axis params, plus explicit <c>contact</c>/<c>standingLongjump</c>
|
||||
/// booleans for the trailing byte — see <c>MoveToStatePack::Pack</c>
|
||||
/// (0x005168f0).
|
||||
/// </summary>
|
||||
public class MoveToStateTests
|
||||
{
|
||||
private static readonly Vector3 Pos = new(96f, 96f, 50f);
|
||||
|
||||
[Fact]
|
||||
public void Build_IdleState_ProducesValidGameAction()
|
||||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 1,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: new Vector3(96f, 96f, 50f),
|
||||
position: Pos,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
|
|
@ -42,31 +52,33 @@ public class MoveToStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WalkForward_IncludesForwardCommandInFlags()
|
||||
public void Build_WalkForward_DefaultSpeedOmitted_OnlyCommandAndHoldKeyFlagsSet()
|
||||
{
|
||||
// D1 fix: forward_speed == 1.0 (the retail default) is OMITTED from
|
||||
// the flags, unlike the pre-slice presence-based packer which always
|
||||
// set the ForwardSpeed bit whenever a caller supplied a value.
|
||||
var state = new RawMotionState
|
||||
{
|
||||
ForwardCommand = 0x45000005u, // WalkForward
|
||||
ForwardHoldKey = HoldKey.None, // differs from Invalid -> set
|
||||
ForwardSpeed = 1.0f, // default -> omitted
|
||||
};
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 2,
|
||||
forwardCommand: 0x45000005u, // WalkForward
|
||||
forwardSpeed: 1.0f,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: new Vector3(96f, 96f, 50f),
|
||||
position: Pos,
|
||||
rotation: Quaternion.Identity,
|
||||
instanceSequence: 0,
|
||||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0);
|
||||
|
||||
// After the 12-byte GameAction header comes RawMotionState.
|
||||
// First u32 is the packed flags word. ForwardCommand flag = 0x4.
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
Assert.True((flags & 0x4u) != 0, "ForwardCommand flag (0x4) should be set");
|
||||
// ForwardSpeed flag = 0x10
|
||||
Assert.True((flags & 0x10u) != 0, "ForwardSpeed flag (0x10) should be set");
|
||||
Assert.True((flags & 0x8u) != 0, "ForwardHoldKey flag (0x8) should be set");
|
||||
Assert.False((flags & 0x10u) != 0, "ForwardSpeed flag (0x10) must be OMITTED at the retail default 1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -74,13 +86,7 @@ public class MoveToStateTests
|
|||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 3,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
|
|
@ -94,19 +100,14 @@ public class MoveToStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_IdleState_WorldPositionFollowsMotionState()
|
||||
public void Build_IdleState_WorldPositionFollowsZeroFlagMotionState()
|
||||
{
|
||||
// With no motion state, flags = 0 and no conditional fields are written.
|
||||
// So WorldPosition starts at offset 12 (envelope) + 4 (flags) = 16.
|
||||
// With the default raw motion state, flags = 0 and no conditional
|
||||
// fields are written. So WorldPosition starts at offset 12
|
||||
// (envelope) + 4 (flags) = 16.
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 4,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xDEADBEEFu,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
|
|
@ -124,13 +125,7 @@ public class MoveToStateTests
|
|||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 5,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
|
|
@ -143,17 +138,11 @@ public class MoveToStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UsesExplicitAirborneContactByte()
|
||||
public void Build_UsesExplicitAirborneContact()
|
||||
{
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 7,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: null,
|
||||
rawMotionState: RawMotionState.Default,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
|
|
@ -161,23 +150,20 @@ public class MoveToStateTests
|
|||
serverControlSequence: 0,
|
||||
teleportSequence: 0,
|
||||
forcePositionSequence: 0,
|
||||
contactLongJump: 0);
|
||||
contact: false);
|
||||
|
||||
// flags(4) + Position(32) + timestamps(8) = 44; trailing byte at 12+44=56.
|
||||
Assert.Equal(0, body[56]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithHoldKey_IncludesHoldKeyFlag()
|
||||
{
|
||||
var state = new RawMotionState { CurrentHoldKey = HoldKey.Run };
|
||||
|
||||
var body = MoveToState.Build(
|
||||
gameActionSequence: 6,
|
||||
forwardCommand: null,
|
||||
forwardSpeed: null,
|
||||
sidestepCommand: null,
|
||||
sidestepSpeed: null,
|
||||
turnCommand: null,
|
||||
turnSpeed: null,
|
||||
holdKey: 2u, // Run
|
||||
rawMotionState: state,
|
||||
cellId: 0xA9B40001u,
|
||||
position: Vector3.Zero,
|
||||
rotation: Quaternion.Identity,
|
||||
|
|
|
|||
63
tests/AcDream.Core.Net.Tests/Messages/PositionPackTests.cs
Normal file
63
tests/AcDream.Core.Net.Tests/Messages/PositionPackTests.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
212
tests/AcDream.Core.Net.Tests/Messages/RawMotionStatePackTests.cs
Normal file
212
tests/AcDream.Core.Net.Tests/Messages/RawMotionStatePackTests.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Net.Packets;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-byte tests for <see cref="RawMotionStatePacker.Pack"/>, porting
|
||||
/// retail's <c>RawMotionState::Pack</c> (0x0051ed10, decomp lines
|
||||
/// ~293761-294013; bitfield layout <c>acclient.h RawMotionState::PackBitfield</c>,
|
||||
/// line 46474). Confirmed verbatim against the Ghidra decompile-by-address
|
||||
/// bridge (http://127.0.0.1:8081/decompile_function?address=0x0051ed10)
|
||||
/// during this slice (2026-06-30).
|
||||
///
|
||||
/// <para>
|
||||
/// Retail compares every field against its DEFAULT and only sets the
|
||||
/// matching bit (and emits the field) when the live value DIFFERS. This is
|
||||
/// the D1 fix — the old presence-based packer over-sent defaulted fields.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class RawMotionStatePackTests
|
||||
{
|
||||
private static byte[] Pack(RawMotionState state)
|
||||
{
|
||||
var w = new PacketWriter(64);
|
||||
RawMotionStatePacker.Pack(w, state);
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_DefaultState_EmitsOnlyZeroFlags()
|
||||
{
|
||||
// Every field equals its retail default -> flags dword is 0,
|
||||
// no conditional fields, no actions. 4 bytes total.
|
||||
var body = Pack(RawMotionState.Default);
|
||||
|
||||
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_ShiftWalk_OmitsForwardSpeedAndCurrentHoldKey()
|
||||
{
|
||||
// Shift-walk: current_holdkey stays None (default omitted),
|
||||
// forward_command = WalkForward (0x45000005), forward_holdkey =
|
||||
// None (1) -- DIFFERS from default Invalid(0) so IS sent --
|
||||
// forward_speed stays 1.0 (default, omitted). Rest default.
|
||||
//
|
||||
// Flags expected: ForwardCommand(0x004) | ForwardHoldKey(0x008) = 0x00C.
|
||||
// Body: flags(u32) + forward_command(u32) + forward_holdkey(u32).
|
||||
var state = new RawMotionState
|
||||
{
|
||||
CurrentHoldKey = HoldKey.None, // default -> omitted
|
||||
ForwardCommand = 0x45000005u, // WalkForward -> differs -> set
|
||||
ForwardHoldKey = HoldKey.None, // differs from Invalid -> set
|
||||
ForwardSpeed = 1.0f, // default -> omitted (the D1 fix)
|
||||
};
|
||||
|
||||
var body = Pack(state);
|
||||
|
||||
Assert.Equal(12, body.Length);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0x0000000Cu, flags);
|
||||
|
||||
uint fwdCommand = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||||
Assert.Equal(0x45000005u, fwdCommand);
|
||||
|
||||
uint fwdHoldKey = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
|
||||
Assert.Equal(1u, fwdHoldKey); // HoldKey.None
|
||||
|
||||
byte[] expected =
|
||||
{
|
||||
0x0C, 0x00, 0x00, 0x00,
|
||||
0x05, 0x00, 0x00, 0x45,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
};
|
||||
Assert.Equal(expected, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_RunForward_SetsHoldKeyForwardCommandHoldKeyAndSpeed()
|
||||
{
|
||||
// Run-forward: current_holdkey = Run(2) (differs from None -> set),
|
||||
// forward_command = RunForward-ish 0x44000007 (differs -> set),
|
||||
// forward_holdkey = Run(2) (differs from Invalid -> set),
|
||||
// forward_speed = 3.0 (differs from 1.0 -> set).
|
||||
//
|
||||
// Flags expected: CurrentHoldKey(0x001) | ForwardCommand(0x004) |
|
||||
// ForwardHoldKey(0x008) | ForwardSpeed(0x010) = 0x01D.
|
||||
var state = new RawMotionState
|
||||
{
|
||||
CurrentHoldKey = HoldKey.Run,
|
||||
ForwardCommand = 0x44000007u,
|
||||
ForwardHoldKey = HoldKey.Run,
|
||||
ForwardSpeed = 3.0f,
|
||||
};
|
||||
|
||||
var body = Pack(state);
|
||||
|
||||
Assert.Equal(20, body.Length);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0x0000001Du, flags);
|
||||
|
||||
byte[] expected =
|
||||
{
|
||||
0x1D, 0x00, 0x00, 0x00, // flags
|
||||
0x02, 0x00, 0x00, 0x00, // current_holdkey = Run(2)
|
||||
0x07, 0x00, 0x00, 0x44, // forward_command = 0x44000007
|
||||
0x02, 0x00, 0x00, 0x00, // forward_holdkey = Run(2)
|
||||
0x00, 0x00, 0x40, 0x40, // forward_speed = 3.0f (0x40400000 LE)
|
||||
};
|
||||
Assert.Equal(expected, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_NonDefaultCurrentStyle_SetsStyleBitAndEmitsValue()
|
||||
{
|
||||
// current_style differs from 0x8000003D -> bit 0x002 set, value emitted
|
||||
// immediately after the flags dword (bit order: holdkey, style, ...).
|
||||
var state = new RawMotionState
|
||||
{
|
||||
CurrentStyle = 0x80000042u,
|
||||
};
|
||||
|
||||
var body = Pack(state);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0x002u, flags);
|
||||
|
||||
uint style = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||||
Assert.Equal(0x80000042u, style);
|
||||
|
||||
Assert.Equal(8, body.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_PopulatedActionsList_SetsNumActionsBitsAndEmitsPairs()
|
||||
{
|
||||
// num_actions occupies bits 11-15 (mask 0xF800) of the flags dword.
|
||||
// Two actions -> num_actions = 2 -> bits = 2 << 11 = 0x1000.
|
||||
// Each action emits u16 command then u16 (stamp & 0x7FFF) |
|
||||
// (autonomous ? 0x8000 : 0) (decomp ~293998-294010).
|
||||
var state = new RawMotionState
|
||||
{
|
||||
Actions = new[]
|
||||
{
|
||||
new RawMotionAction(Command: 0x0150, Stamp: 0x0001, Autonomous: false),
|
||||
new RawMotionAction(Command: 0x0163, Stamp: 0x7FFF, Autonomous: true),
|
||||
},
|
||||
};
|
||||
|
||||
var body = Pack(state);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0x1000u, flags); // num_actions=2 << 11, no continuous-axis bits
|
||||
|
||||
// Body: flags(4) + action0(4) + action1(4) = 12 bytes.
|
||||
Assert.Equal(12, body.Length);
|
||||
|
||||
ushort cmd0 = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(4));
|
||||
ushort stamp0 = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(6));
|
||||
Assert.Equal((ushort)0x0150, cmd0);
|
||||
Assert.Equal((ushort)0x0001, stamp0); // autonomous=false -> 0x8000 bit clear
|
||||
|
||||
ushort cmd1 = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(8));
|
||||
ushort stamp1 = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(10));
|
||||
Assert.Equal((ushort)0x0163, cmd1);
|
||||
Assert.Equal((ushort)0xFFFF, stamp1); // (0x7FFF & 0x7FFF) | 0x8000 = 0xFFFF
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_SidestepAndTurnNonDefault_SetExpectedBitsInOrder()
|
||||
{
|
||||
// sidestep_command(0x020), sidestep_holdkey(0x040), sidestep_speed(0x080),
|
||||
// turn_command(0x100), turn_holdkey(0x200), turn_speed(0x400) all non-default.
|
||||
var state = new RawMotionState
|
||||
{
|
||||
SidestepCommand = 0x44000009u,
|
||||
SidestepHoldKey = HoldKey.Run,
|
||||
SidestepSpeed = 1.248f,
|
||||
TurnCommand = 0x4400000Du,
|
||||
TurnHoldKey = HoldKey.Run,
|
||||
TurnSpeed = 1.5f,
|
||||
};
|
||||
|
||||
var body = Pack(state);
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
|
||||
Assert.Equal(0x000007E0u, flags); // 0x20|0x40|0x80|0x100|0x200|0x400
|
||||
|
||||
// Body order after flags: sidestep_command, sidestep_holdkey,
|
||||
// sidestep_speed, turn_command, turn_holdkey, turn_speed.
|
||||
uint ssCmd = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||||
uint ssHold = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
|
||||
float ssSpeed = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(12));
|
||||
uint turnCmd = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16));
|
||||
uint turnHold = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20));
|
||||
float turnSpeed = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(24));
|
||||
|
||||
Assert.Equal(0x44000009u, ssCmd);
|
||||
Assert.Equal(2u, ssHold);
|
||||
Assert.Equal(1.248f, ssSpeed);
|
||||
Assert.Equal(0x4400000Du, turnCmd);
|
||||
Assert.Equal(2u, turnHold);
|
||||
Assert.Equal(1.5f, turnSpeed);
|
||||
|
||||
Assert.Equal(28, body.Length);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue