The retail server-directed-movement brain (0x00529010-0x0052a987), Core-only with every App dependency as a ctor/property seam for the V4/V5 cutovers: node-plan builders for all four movement types (TurnToObject's desired-heading clobber quirk VERBATIM; TurnToHeading's immediate BeginNextNode - ACE's one-tick-late gap not copied), PerformMovement (cancel 0x36 + unstick first), BeginNextNode with the sticky handoff order (radius/height/tlid read BEFORE CleanUp), BeginMoveForward (GetCommand walk/run cascade + stored-params write-back + progress-clock seed), HandleMoveToPosition (chase arrival dist <= distance_to_object per the adjudicated BN inversion; fail distance -> 0x3D; progress >= 0.25 units/s over >= 1 s, incremental AND overall; fail_progress_count write-only - retail has NO give-up threshold and none was invented), HandleTurnToHeading (20/340 aux deadband; the Ghidra-confirmed heading_diff mirror), HandleUpdateTarget 0x0052a7d0 (deferred-start: object moves wait for the first Ok callback; retargets reset the progress clock without requeueing), UseTime's initialized gate, InitializeLocalVariables per retail (flags word + context_id zeroed, floats stale, FLT_MAX resets - not ACE's transpositions). TDD catch: default(Quaternion) is the ZERO quaternion, not identity - a fresh manager's heading computations would silently read 90 degrees; explicit IdentityPosition resets match the decomp's identity-Frame semantics. Also pinned: retail's explicit double adjust_motion in _DoMotion/_StopMotion; entry points never drain pending_actions (only PerformMovement's cancel does) - re-issues must route through PerformMovement, documented + tested. 101 new conformance tests incl. three end-to-end scripted drives (chase turn->run->walk-demote->arrive; flee; frozen-heading TurnToObject through retarget). Full suite: 3,961 passed. Implemented by a dedicated agent against the V0-pinned spec; scope + suite independently verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
3.9 KiB
C#
100 lines
3.9 KiB
C#
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R4-V2 — <c>MoveToManager</c> construction / <c>InitializeLocalVariables</c>
|
|
/// (<c>00529250</c>, raw 306490-306534) / <c>Destroy</c> (<c>005294b0</c>) /
|
|
/// <c>is_moving_to</c> (<c>00529220</c>). Per r4-moveto-decomp.md §1: the ctor
|
|
/// zeroes the FLAGS WORD + context_id only (NOT ACE's A2/A3 full-struct-reset
|
|
/// transposition — scalar param fields keep stale values since every entry
|
|
/// point copies all ten fields anyway), resets both progress-clock pairs to
|
|
/// FLT_MAX/now, and resets Sought/CurrentTarget positions but NOT
|
|
/// StartingPosition.
|
|
/// </summary>
|
|
public sealed class MoveToManagerLifecycleTests
|
|
{
|
|
[Fact]
|
|
public void FreshManager_MovementTypeIsInvalid()
|
|
{
|
|
var h = new MoveToManagerHarness();
|
|
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
|
Assert.False(h.Manager.IsMovingTo());
|
|
}
|
|
|
|
[Fact]
|
|
public void FreshManager_ProgressClocksAreFltMax()
|
|
{
|
|
var h = new MoveToManagerHarness();
|
|
Assert.Equal(float.MaxValue, h.Manager.PreviousDistance);
|
|
Assert.Equal(float.MaxValue, h.Manager.OriginalDistance);
|
|
}
|
|
|
|
[Fact]
|
|
public void FreshManager_BitfieldFlagsAllClear_ScalarsUntouchedByCtorReset()
|
|
{
|
|
// InitializeLocalVariables clears ONLY the bitfield + context_id.
|
|
// The scalar fields (DistanceToObject etc.) are NOT part of that
|
|
// clear — but since Params starts as `new MovementParameters()`
|
|
// (retail ctor defaults), the scalars already hold their defaults
|
|
// here; the "stale values survive InitializeLocalVariables" claim is
|
|
// exercised by MoveToManagerCancelAndCleanupTests (a scalar surviving
|
|
// a CleanUp/InitializeLocalVariables round-trip after being changed
|
|
// by an entry point).
|
|
var h = new MoveToManagerHarness();
|
|
Assert.False(h.Manager.Params.CanWalk);
|
|
Assert.False(h.Manager.Params.CanRun);
|
|
Assert.False(h.Manager.Params.MoveTowards);
|
|
Assert.False(h.Manager.Params.CancelMoveTo);
|
|
Assert.Equal(0u, h.Manager.Params.ContextId);
|
|
}
|
|
|
|
[Fact]
|
|
public void FreshManager_SoughtPositionAndCurrentTargetAreIdentityFrameAtCellZero()
|
|
{
|
|
// NOT default(Position) — default(Quaternion) is the ZERO
|
|
// quaternion, not identity. Retail resets to a genuine identity
|
|
// frame (decomp §1c) at cell id 0.
|
|
var h = new MoveToManagerHarness();
|
|
var expected = new Position(0u, System.Numerics.Vector3.Zero, System.Numerics.Quaternion.Identity);
|
|
Assert.Equal(expected, h.Manager.SoughtPosition);
|
|
Assert.Equal(expected, h.Manager.CurrentTargetPosition);
|
|
Assert.Equal(0f, MoveToMath.GetHeading(h.Manager.SoughtPosition.Frame.Orientation));
|
|
}
|
|
|
|
[Fact]
|
|
public void FreshManager_PendingActionsEmpty()
|
|
{
|
|
var h = new MoveToManagerHarness();
|
|
Assert.Empty(h.Manager.PendingActions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Destroy_DrainsPendingActions_ThenReInitializes()
|
|
{
|
|
var h = new MoveToManagerHarness();
|
|
h.Manager.AddMoveToPositionNode();
|
|
h.Manager.AddTurnToHeadingNode(90f);
|
|
Assert.Equal(2, System.Linq.Enumerable.Count(h.Manager.PendingActions));
|
|
|
|
h.Manager.Destroy();
|
|
|
|
Assert.Empty(h.Manager.PendingActions);
|
|
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsMovingTo_TrueAfterMoveToPosition_FalseAfterCancel()
|
|
{
|
|
var h = new MoveToManagerHarness();
|
|
h.Manager.MoveToPosition(new Position(1u, new System.Numerics.Vector3(10f, 0f, 0f), System.Numerics.Quaternion.Identity), new MovementParameters());
|
|
|
|
Assert.True(h.Manager.IsMovingTo());
|
|
|
|
h.Manager.CancelMoveTo(WeenieError.ActionCancelled);
|
|
|
|
Assert.False(h.Manager.IsMovingTo());
|
|
}
|
|
}
|