MovementParameters gains the verbatim selection family: GetCommand 0x0052aa00 (the walk-vs-run cascade INCLUDING the CanCharge 0x10 fast-path ACE dropped - retail's default can_charge=false + the fast-path present, the A13+A15 canceling-pair trap avoided; inclusive threshold edge per the raw), TowardsAndAway, GetDesiredHeading per the live Ghidra decompile (fwd-towards 0 / fwd-away 180 / back-towards 180 / back-away 0), FromWire/FromWireTurnTo (UnPackNet semantics, all 18 A4 masks round-tripped). New MoveToMath: HeadingDiff per the live Ghidra decompile of 0x00528fb0 (the 360-diff NOT-TurnRight mirror + F_EPSILON 0.000199999995f - the BN "arg unused" artifact corrected), HeadingGreater (the visible TurnRight idiom), PositionHeading/Get/SetHeading reusing the codebase's single yaw-heading convention (P5), CylinderDistance (PDB arg order; planar-minus-radii shape documented as the interpretation - the raw's x87 body is garbled; seam noted). MovementType gains Invalid + retail 6/7/8/9; MovementStruct widened (ObjectId/TopLevelId/Pos/Radius/Height/Params, additive); WeenieError += 0x0B/0x36/0x37/0x38/0x3D with retail-meaning doc comments. 148 new conformance tests. Full suite: 3,860 passed. Implemented by a dedicated agent against the V0-pinned spec; scope + suite independently verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
6.4 KiB
C#
186 lines
6.4 KiB
C#
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R4-V1 — <c>MovementParameters::UnPackNet</c> (<c>0x0052ac50</c>, raw
|
|
/// 308118-308205) factory semantics, per
|
|
/// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §2g: the 7-dword
|
|
/// MoveTo wire form is <c>bitfield, distance_to_object, min_distance,
|
|
/// fail_distance, speed, walk_run_threshhold, desired_heading</c> — the SAME
|
|
/// field order <c>UpdateMotion.TryParseMoveToPayload</c> already reads off
|
|
/// the wire (UpdateMotion.cs:328-341). The A4 bitfield masks
|
|
/// (W0-pins.md §A4) decode into the named bool properties; every bit not
|
|
/// present on the wire bitfield resolves to false (UnPackNet fully
|
|
/// overwrites the bitfield — no ctor-default bits survive).
|
|
/// </summary>
|
|
public sealed class MovementParametersFromWireTests
|
|
{
|
|
[Fact]
|
|
public void FromWire_AllBitsSet_EveryFlagTrue()
|
|
{
|
|
var p = MovementParameters.FromWire(
|
|
bitfield: 0x3FFFFu, // every A4 bit through 0x20000
|
|
distanceToObject: 1f,
|
|
minDistance: 2f,
|
|
failDistance: 3f,
|
|
speed: 4f,
|
|
walkRunThreshhold: 5f,
|
|
desiredHeading: 6f);
|
|
|
|
Assert.True(p.CanWalk);
|
|
Assert.True(p.CanRun);
|
|
Assert.True(p.CanSidestep);
|
|
Assert.True(p.CanWalkBackwards);
|
|
Assert.True(p.CanCharge);
|
|
Assert.True(p.FailWalk);
|
|
Assert.True(p.UseFinalHeading);
|
|
Assert.True(p.Sticky);
|
|
Assert.True(p.MoveAway);
|
|
Assert.True(p.MoveTowards);
|
|
Assert.True(p.UseSpheres);
|
|
Assert.True(p.SetHoldKey);
|
|
Assert.True(p.Autonomous);
|
|
Assert.True(p.ModifyRawState);
|
|
Assert.True(p.ModifyInterpretedState);
|
|
Assert.True(p.CancelMoveTo);
|
|
Assert.True(p.StopCompletelyFlag);
|
|
Assert.True(p.DisableJumpDuringLink);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromWire_ZeroBitfield_EveryFlagFalse_NoCtorDefaultsSurvive()
|
|
{
|
|
var p = MovementParameters.FromWire(
|
|
bitfield: 0u,
|
|
distanceToObject: 1f, minDistance: 2f, failDistance: 3f,
|
|
speed: 4f, walkRunThreshhold: 5f, desiredHeading: 6f);
|
|
|
|
Assert.False(p.CanWalk);
|
|
Assert.False(p.CanRun);
|
|
Assert.False(p.CanSidestep);
|
|
Assert.False(p.CanWalkBackwards);
|
|
Assert.False(p.CanCharge);
|
|
Assert.False(p.MoveTowards);
|
|
Assert.False(p.UseSpheres);
|
|
Assert.False(p.SetHoldKey);
|
|
Assert.False(p.ModifyRawState);
|
|
Assert.False(p.ModifyInterpretedState);
|
|
Assert.False(p.CancelMoveTo);
|
|
Assert.False(p.StopCompletelyFlag);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromWire_CanChargeBit_DecodesIndependently()
|
|
{
|
|
// The wire bitfield carries can_charge 0x10 — the walk-vs-run answer
|
|
// (feedback_autowalk_cancharge_bit). Verify it round-trips on its own.
|
|
var p = MovementParameters.FromWire(
|
|
bitfield: 0x10u,
|
|
distanceToObject: 0f, minDistance: 0f, failDistance: 0f,
|
|
speed: 0f, walkRunThreshhold: 0f, desiredHeading: 0f);
|
|
|
|
Assert.True(p.CanCharge);
|
|
Assert.False(p.CanWalk);
|
|
Assert.False(p.CanRun);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x1u)]
|
|
[InlineData(0x2u)]
|
|
[InlineData(0x4u)]
|
|
[InlineData(0x8u)]
|
|
[InlineData(0x10u)]
|
|
[InlineData(0x20u)]
|
|
[InlineData(0x40u)]
|
|
[InlineData(0x80u)]
|
|
[InlineData(0x100u)]
|
|
[InlineData(0x200u)]
|
|
[InlineData(0x400u)]
|
|
[InlineData(0x800u)]
|
|
[InlineData(0x1000u)]
|
|
[InlineData(0x2000u)]
|
|
[InlineData(0x4000u)]
|
|
[InlineData(0x8000u)]
|
|
[InlineData(0x10000u)]
|
|
[InlineData(0x20000u)]
|
|
public void FromWire_SingleBitMaskRoundTrips(uint mask)
|
|
{
|
|
var p = MovementParameters.FromWire(
|
|
bitfield: mask,
|
|
distanceToObject: 0f, minDistance: 0f, failDistance: 0f,
|
|
speed: 0f, walkRunThreshhold: 0f, desiredHeading: 0f);
|
|
|
|
Assert.Equal(mask, ToBitfield(p));
|
|
}
|
|
|
|
[Fact]
|
|
public void FromWire_ScalarFields_CopiedInWireOrder()
|
|
{
|
|
var p = MovementParameters.FromWire(
|
|
bitfield: 0u,
|
|
distanceToObject: 1.5f,
|
|
minDistance: 2.5f,
|
|
failDistance: 3.5f,
|
|
speed: 4.5f,
|
|
walkRunThreshhold: 5.5f,
|
|
desiredHeading: 6.5f);
|
|
|
|
Assert.Equal(1.5f, p.DistanceToObject);
|
|
Assert.Equal(2.5f, p.MinDistance);
|
|
Assert.Equal(3.5f, p.FailDistance);
|
|
Assert.Equal(4.5f, p.Speed);
|
|
Assert.Equal(5.5f, p.WalkRunThreshhold);
|
|
Assert.Equal(6.5f, p.DesiredHeading);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromWireTurnTo_ThreeDwordForm_LeavesDistanceFieldsAtDefault()
|
|
{
|
|
// TurnToObject/TurnToHeading wire form (0xc bytes, 3 dwords):
|
|
// bitfield, speed, desired_heading only. distance_to_object /
|
|
// min_distance / fail_distance / walk_run_threshhold are NOT on
|
|
// this wire form — the factory overload must not touch them
|
|
// (they keep the MovementParameters ctor defaults).
|
|
var p = MovementParameters.FromWireTurnTo(
|
|
bitfield: 0x2u, // can_run
|
|
speed: 2f,
|
|
desiredHeading: 90f);
|
|
|
|
Assert.True(p.CanRun);
|
|
Assert.Equal(2f, p.Speed);
|
|
Assert.Equal(90f, p.DesiredHeading);
|
|
|
|
// ctor defaults, untouched by the 3-dword form:
|
|
Assert.Equal(0.6f, p.DistanceToObject);
|
|
Assert.Equal(0f, p.MinDistance);
|
|
Assert.Equal(float.MaxValue, p.FailDistance);
|
|
Assert.Equal(15f, p.WalkRunThreshhold);
|
|
}
|
|
|
|
private static uint ToBitfield(MovementParameters p)
|
|
{
|
|
uint bitfield = 0;
|
|
if (p.CanWalk) bitfield |= 0x1;
|
|
if (p.CanRun) bitfield |= 0x2;
|
|
if (p.CanSidestep) bitfield |= 0x4;
|
|
if (p.CanWalkBackwards) bitfield |= 0x8;
|
|
if (p.CanCharge) bitfield |= 0x10;
|
|
if (p.FailWalk) bitfield |= 0x20;
|
|
if (p.UseFinalHeading) bitfield |= 0x40;
|
|
if (p.Sticky) bitfield |= 0x80;
|
|
if (p.MoveAway) bitfield |= 0x100;
|
|
if (p.MoveTowards) bitfield |= 0x200;
|
|
if (p.UseSpheres) bitfield |= 0x400;
|
|
if (p.SetHoldKey) bitfield |= 0x800;
|
|
if (p.Autonomous) bitfield |= 0x1000;
|
|
if (p.ModifyRawState) bitfield |= 0x2000;
|
|
if (p.ModifyInterpretedState) bitfield |= 0x4000;
|
|
if (p.CancelMoveTo) bitfield |= 0x8000;
|
|
if (p.StopCompletelyFlag) bitfield |= 0x10000;
|
|
if (p.DisableJumpDuringLink) bitfield |= 0x20000;
|
|
return bitfield;
|
|
}
|
|
}
|