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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue