feat(R4-V3): wire completion - mt 8/9 parsing + full params exposure + the mt-0 sticky trailer (closes M7, M13, M14-wire-note)
UpdateMotion now parses TurnToObject (mt 8: guid, standalone wire heading, 3-dword UnPackNet) and TurnToHeading (mt 9: 3-dword UnPackNet) into a new TurnToPathData sibling record (the two wire forms genuinely diverge - 7-dword move UnPackNet with Origin head vs 3-dword turn UnPackNet with guid+heading head; every consumer switches on MovementType first, so no polymorphic shape was invented). mt 6/7 exposure widened additively so ALL UnPackNet fields reach MovementParameters.FromWire. The mt=0 motionFlags sticky-guid trailer (bit 0x1) is parsed for cursor honesty and carried unconsumed until R5 - scoped to mt=0 ONLY per both ACE's writer (MovementInvalid.Write) and the decomp's case-0 read, tighter than the plan sketched; the StandingLongJump bit (0x2) doc-noted as the R5 unpack_movement item. MoveToRunRate doc-pointered as the V4/V5 MyRunRate write. 11 new golden-byte tests hand-assembled from ACE's writers (MovementData/TurnToObject/TurnToParameters/TurnToHeading/ MoveToParameters/MovementInvalid) incl. flag-permutation round-trips and the trailer cursor-honesty case; the existing 12 mt 6/7 fixtures pass unchanged. Full suite: 3,972 passed. Implemented by a dedicated agent against the V0-pinned spec (P6 order confirmed exactly); scope + suite independently verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
addc8e97a8
commit
a144e87318
3 changed files with 542 additions and 5 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
|
@ -382,4 +383,336 @@ public class UpdateMotionTests
|
|||
Assert.Equal(7f, path.OriginZ);
|
||||
Assert.Equal(1.25f, result.Value.MotionState.MoveToRunRate);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// R4-V3 (closes M7): mt 8 (TurnToObject) / mt 9 (TurnToHeading).
|
||||
//
|
||||
// Golden bytes assembled from ACE's own writers (V0-pins.md P6):
|
||||
// MovementDataExtensions.Write (references/ACE/Source/ACE.Server/
|
||||
// Network/Motion/MovementData.cs:184-229) writes the common header
|
||||
// (movementType u8, motionFlags u8, currentStyle u16) then dispatches
|
||||
// on MovementType to:
|
||||
// TurnToObjectExtensions.Write (TurnToObject.cs:25-30):
|
||||
// writer.WriteGuid(Target); // u32
|
||||
// writer.Write(DesiredHeading); // f32 — the STANDALONE
|
||||
// // "wire_heading" field
|
||||
// writer.Write(TurnToParameters); // 3-dword UnPackNet form
|
||||
// TurnToParametersExtensions.Write (TurnToParameters.cs:23-28):
|
||||
// writer.Write((uint)MovementParams); // u32 bitfield
|
||||
// writer.Write(Speed); // f32
|
||||
// writer.Write(DesiredHeading); // f32 — TurnToParameters'
|
||||
// // OWN desired_heading
|
||||
// TurnToHeadingExtensions.Write (TurnToHeading.cs:18-21):
|
||||
// writer.Write(TurnToParameters); // 3-dword UnPackNet form only
|
||||
//
|
||||
// P6's fixture caveat: ACE always populates field2 (TurnToObject.
|
||||
// DesiredHeading) and field5 (TurnToParameters.DesiredHeading) from the
|
||||
// SAME motion.DesiredHeading source, so a byte-faithful ACE capture
|
||||
// would have field2 == field5. To prove the parser distinguishes the
|
||||
// two fields by OFFSET (not by coincidentally-equal value), these
|
||||
// fixtures hand-vary the two headings.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ParsesTurnToObject_GuidWireHeadingAndParams()
|
||||
{
|
||||
// Header (20 bytes) + guid (4) + wireHeading (4) + TurnToParameters (12) = 40.
|
||||
var body = new byte[20 + 4 + 4 + 12];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80005678u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6; // MovementData header padding
|
||||
|
||||
body[p++] = 8; // TurnToObject
|
||||
body[p++] = 0; // motionFlags
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80009999u); p += 4; // target guid
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 42.0f); p += 4; // standalone wire_heading (field2)
|
||||
|
||||
const uint flags = 0x1u | 0x2u | 0x200u; // can_walk | can_run | move_towards
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), flags); p += 4; // TurnToParameters.bitfield
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.5f); p += 4; // TurnToParameters.speed
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 199.0f); p += 4; // TurnToParameters.desired_heading (field5) — DELIBERATELY != field2
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal((byte)8, result!.Value.MotionState.MovementType);
|
||||
Assert.True(result.Value.MotionState.IsServerControlledTurnTo);
|
||||
Assert.False(result.Value.MotionState.IsServerControlledMoveTo);
|
||||
Assert.NotNull(result.Value.MotionState.TurnToPath);
|
||||
|
||||
var path = result.Value.MotionState.TurnToPath!.Value;
|
||||
Assert.Equal(0x80009999u, path.TargetGuid);
|
||||
Assert.Equal(42.0f, path.WireHeading); // field2 — distinguished by OFFSET
|
||||
Assert.Equal(flags, path.Bitfield);
|
||||
Assert.Equal(1.5f, path.Speed);
|
||||
Assert.Equal(199.0f, path.DesiredHeading); // field5 — distinct from field2
|
||||
|
||||
// The consumer feeds this straight into FromWireTurnTo (App-layer,
|
||||
// out of scope here) — verify the fixture is round-trippable.
|
||||
var mp = MovementParameters.FromWireTurnTo(path.Bitfield, path.Speed, path.DesiredHeading);
|
||||
Assert.True(mp.CanRun);
|
||||
Assert.Equal(1.5f, mp.Speed);
|
||||
Assert.Equal(199.0f, mp.DesiredHeading);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesTurnToObject_UnresolvableFallback_BothHeadingsSurvivedDistinctly()
|
||||
{
|
||||
// Retail's degrade-to-TurnToHeading fallback (decomp §2f case 8) only
|
||||
// fires when GetObjectA(object_id) == 0 — a runtime/consumer-side
|
||||
// resolution the wire parser has no visibility into. The parser's
|
||||
// job is just to expose BOTH heading fields so the (future) V4/V5
|
||||
// consumer can implement: "if unresolvable, params.DesiredHeading =
|
||||
// wireHeading, then degrade to TurnToHeading". Confirm both survive
|
||||
// even when they'd trigger the fallback (i.e. even when they differ).
|
||||
var body = new byte[20 + 4 + 4 + 12];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x8000AAAAu); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 8;
|
||||
body[p++] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xDEADBEEFu); p += 4; // unresolvable guid
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 270.0f); p += 4; // wire_heading — the fallback source
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x1u); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.0f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.0f); p += 4; // params.desired_heading (would be overwritten by wire_heading on fallback)
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var path = result!.Value.MotionState.TurnToPath!.Value;
|
||||
Assert.Equal(0xDEADBEEFu, path.TargetGuid);
|
||||
Assert.Equal(270.0f, path.WireHeading);
|
||||
Assert.Equal(0.0f, path.DesiredHeading);
|
||||
Assert.NotEqual(path.WireHeading, path.DesiredHeading);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesTurnToHeading_ThreeDwordFormOnly_NoGuidOrWireHeading()
|
||||
{
|
||||
// Header (20 bytes) + TurnToParameters (12) = 32. No guid, no
|
||||
// standalone heading field — TurnToHeadingExtensions.Write emits
|
||||
// ONLY the 3-dword UnPackNet form (TurnToHeading.cs:18-21).
|
||||
var body = new byte[20 + 12];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x8000BBBBu); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 9; // TurnToHeading
|
||||
body[p++] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||||
|
||||
const uint flags = 0x2u | 0x800u; // can_run | set_hold_key
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), flags); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 2.0f); p += 4; // speed
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 315.0f); p += 4; // desired_heading
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal((byte)9, result!.Value.MotionState.MovementType);
|
||||
Assert.True(result.Value.MotionState.IsServerControlledTurnTo);
|
||||
Assert.NotNull(result.Value.MotionState.TurnToPath);
|
||||
|
||||
var path = result.Value.MotionState.TurnToPath!.Value;
|
||||
Assert.Null(path.TargetGuid);
|
||||
Assert.Null(path.WireHeading);
|
||||
Assert.Equal(flags, path.Bitfield);
|
||||
Assert.Equal(2.0f, path.Speed);
|
||||
Assert.Equal(315.0f, path.DesiredHeading);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u)] // no flags
|
||||
[InlineData(0x1u | 0x2u)] // can_walk | can_run
|
||||
[InlineData(0x10u)] // can_charge (fast-path bit)
|
||||
[InlineData(0x3FFFFu)] // every A4 bit through 0x20000
|
||||
public void ParsesTurnToHeading_FlagPermutations_BitfieldRoundTrips(uint bitfield)
|
||||
{
|
||||
var body = new byte[20 + 12];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80001111u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 9;
|
||||
body[p++] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), bitfield); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.0f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.0f); p += 4;
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(bitfield, result!.Value.MotionState.TurnToPath!.Value.Bitfield);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// R4-V3 deliverable B: mt 6/7 widened exposure. MoveToPathData already
|
||||
// carries every UnPackNet field (V0/V1 shipped that); this proves the
|
||||
// fixture round-trips end-to-end through MovementParameters.FromWire —
|
||||
// i.e. that ALL seven UnPackNet fields (not just the three ad-hoc bool
|
||||
// properties MoveToCanRun/MoveTowards/CanCharge) reach a consumer.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MoveToPositionPath_FeedsFromWire_AllSevenFieldsSurvive()
|
||||
{
|
||||
var body = new byte[20 + 16 + 28 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80002222u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 7; // MoveToPosition
|
||||
body[p++] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xA8B4000Eu); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 11f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 22f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 33f); p += 4;
|
||||
|
||||
const uint flags = 0x1u | 0x2u | 0x4u | 0x8u | 0x10u | 0x200u | 0x400u; // incl. can_charge + use_spheres
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), flags); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.6f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.1f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 50.0f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.25f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 15.0f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 123.0f); p += 4;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 2.75f); p += 4; // runRate
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
Assert.NotNull(result);
|
||||
var path = result!.Value.MotionState.MoveToPath!.Value;
|
||||
|
||||
var mp = MovementParameters.FromWire(
|
||||
flags,
|
||||
path.DistanceToObject,
|
||||
path.MinDistance,
|
||||
path.FailDistance,
|
||||
result.Value.MotionState.MoveToSpeed!.Value,
|
||||
path.WalkRunThreshold,
|
||||
path.DesiredHeading);
|
||||
|
||||
Assert.True(mp.CanWalk);
|
||||
Assert.True(mp.CanRun);
|
||||
Assert.True(mp.CanSidestep);
|
||||
Assert.True(mp.CanWalkBackwards);
|
||||
Assert.True(mp.CanCharge);
|
||||
Assert.True(mp.MoveTowards);
|
||||
Assert.True(mp.UseSpheres);
|
||||
Assert.Equal(0.6f, mp.DistanceToObject);
|
||||
Assert.Equal(0.1f, mp.MinDistance);
|
||||
Assert.Equal(50.0f, mp.FailDistance);
|
||||
Assert.Equal(1.25f, mp.Speed);
|
||||
Assert.Equal(15.0f, mp.WalkRunThreshhold);
|
||||
Assert.Equal(123.0f, mp.DesiredHeading);
|
||||
Assert.Equal(2.75f, result.Value.MotionState.MoveToRunRate);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// R4-V3 deliverable C: the 0xF74C motionFlags sticky-guid trailer
|
||||
// (mt=0/Invalid only — ACE MovementInvalid.Write gates the trailing
|
||||
// guid on MotionFlags.StickToObject 0x1; decomp §2f case 0
|
||||
// @0052455d). Cursor-honesty test: bytes AFTER the trailer must still
|
||||
// parse correctly (i.e. the trailer's 4 bytes were actually consumed,
|
||||
// not left dangling / double-read).
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ParsesStickyGuidTrailer_WhenMotionFlagsBitSet()
|
||||
{
|
||||
// motionFlags byte1&0x1 (StickToObject) set; InterpretedMotionState
|
||||
// flags = 0 (no fields), so the sticky guid dword immediately
|
||||
// follows the packed flags dword.
|
||||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80003333u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 0; // movementType = Invalid
|
||||
body[p++] = 0x1; // motionFlags = StickToObject
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // no IMS flags
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80004444u); p += 4; // sticky object guid
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0x80004444u, result!.Value.MotionState.StickyObjectGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipsStickyGuidTrailer_WhenMotionFlagsBitClear()
|
||||
{
|
||||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80005555u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 0; // movementType = Invalid
|
||||
body[p++] = 0; // motionFlags = none
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // no IMS flags
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result!.Value.MotionState.StickyObjectGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesStickyGuidTrailer_CursorHonesty_BytesAfterTrailerStillParseCorrectly()
|
||||
{
|
||||
// Sticky trailer with the ForwardCommand flag ALSO set, so there are
|
||||
// bytes both BEFORE (forwardCommand u16) and the sticky dword AFTER
|
||||
// the flags dword — the trailer must be read at the right offset
|
||||
// (after ForwardCommand + its own 2-byte read), not glued onto the
|
||||
// packed-flags dword itself. Cross-checks against ACE's actual field
|
||||
// order: MovementInvalid.Write emits `State` (the whole
|
||||
// InterpretedMotionState, incl. Commands list) THEN the sticky guid
|
||||
// — decomp confirms the same order (UnPack first, sticky guid read
|
||||
// after, r4-moveto-decomp.md:274-275).
|
||||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80006666u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
|
||||
body[p++] = 0; // movementType = Invalid
|
||||
body[p++] = 0x1; // motionFlags = StickToObject
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2; // outer stance
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x2u); p += 4; // IMS flags = ForwardCommand only
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0007); p += 2; // ForwardCommand = RunForward
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80007777u); p += 4; // sticky object guid — AFTER ForwardCommand
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal((ushort)0x0007, result!.Value.MotionState.ForwardCommand);
|
||||
Assert.Equal(0x80007777u, result.Value.MotionState.StickyObjectGuid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue