feat(R4-V2): MoveToManager verbatim - all 33 members + conformance harness (closes M1/M3/M4/M5/M6/M10/M14-core)
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>
This commit is contained in:
parent
e0d2492cbb
commit
addc8e97a8
13 changed files with 3779 additions and 0 deletions
|
|
@ -0,0 +1,150 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V2 — <c>UseTime</c> (<c>0052a780</c>, raw 307776-307798, decomp §6a):
|
||||
/// the three-gate tick matrix (grounded / node-exists / object-move
|
||||
/// initialized), including the uninitialized type-6 stall case from the
|
||||
/// port plan's V2 test list.
|
||||
/// </summary>
|
||||
public sealed class MoveToManagerUseTimeGateTests
|
||||
{
|
||||
[Fact]
|
||||
public void NoNodeQueued_UseTimeIsANoOp()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
// Fresh manager: no active move, no nodes.
|
||||
h.Manager.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotGrounded_ContactFalse_UseTimeDoesNothing_EvenWithNodesQueued()
|
||||
{
|
||||
var h = new MoveToManagerHarness { ContactValue = false };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f; // face the target so BeginMoveForward runs (no turn-to-face node needed)
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters());
|
||||
h.DrainPendingMotions();
|
||||
uint commandBefore = h.Manager.CurrentCommand;
|
||||
|
||||
// Move the mover without letting UseTime process it (Contact=false blocks the gate).
|
||||
h.WorldPosition = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
h.Advance(5.0);
|
||||
h.Manager.UseTime();
|
||||
|
||||
// State machine did not advance -- still the same command, same type.
|
||||
Assert.Equal(commandBefore, h.Manager.CurrentCommand);
|
||||
Assert.Equal(MovementType.MoveToPosition, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Grounded_MoveToPositionNode_DispatchesToHandleMoveToPosition()
|
||||
{
|
||||
var h = new MoveToManagerHarness { ContactValue = true };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f; // face the target so BeginMoveForward runs (no turn-to-face node needed)
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false });
|
||||
h.DrainPendingMotions();
|
||||
|
||||
// Arrived: move the mover close to the TARGET (20,0,0), well within
|
||||
// DistanceToObject, and advance time so CheckProgressMade evaluates
|
||||
// true and the arrival branch pops.
|
||||
h.WorldPosition = new Position(1u, new Vector3(19.7f, 0f, 0f), Quaternion.Identity);
|
||||
h.Advance(2.0);
|
||||
|
||||
h.Manager.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // HandleMoveToPosition ran and completed the move.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Grounded_TurnToHeadingNode_DispatchesToHandleTurnToHeading()
|
||||
{
|
||||
var h = new MoveToManagerHarness { ContactValue = true };
|
||||
h.Heading = 0f;
|
||||
h.Manager.TurnToHeading(new MovementParameters { DesiredHeading = 90f });
|
||||
h.DrainPendingMotions();
|
||||
Assert.Equal(MotionCommand.TurnRight, h.Manager.CurrentCommand);
|
||||
|
||||
h.Heading = 91f; // "passed" the target
|
||||
h.Manager.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // HandleTurnToHeading ran and completed the turn.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectMove_UninitializedType6_StallsUntilFirstTargetCallback()
|
||||
{
|
||||
// The port plan's named "uninitialized type-6 stall" case: a
|
||||
// MoveToObject manager with TopLevelObjectId != 0 and
|
||||
// MovementTypeState != Invalid, but Initialized still false (no
|
||||
// HandleUpdateTarget callback has arrived yet) -- and CRITICALLY,
|
||||
// no node is queued yet either (MoveToObject defers node-building
|
||||
// to the first callback, §3b), so UseTime's node-exists gate (gate
|
||||
// 2) already blocks it. This test proves the stall holds even if a
|
||||
// node WERE somehow present (defense in depth for gate 3).
|
||||
var h = new MoveToManagerHarness { ContactValue = true };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Manager.MoveToObject(0x5000AAAAu, 0x5000AAAAu, 1f, 2f, new MovementParameters());
|
||||
|
||||
Assert.False(h.Manager.Initialized);
|
||||
Assert.Empty(h.Manager.PendingActions); // gate 2 alone already stalls it
|
||||
|
||||
h.Manager.UseTime();
|
||||
|
||||
// No crash, no state change -- the manager is still waiting.
|
||||
Assert.Equal(MovementType.MoveToObject, h.Manager.MovementTypeState);
|
||||
Assert.False(h.Manager.Initialized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectMove_Initialized_PassesGate3_ProcessesNormally()
|
||||
{
|
||||
var h = new MoveToManagerHarness { ContactValue = true };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f; // face the target so the internal node plan skips the turn-to-face step
|
||||
h.Manager.MoveToObject(0x5000BBBBu, 0x5000BBBBu, radius: 0.5f, height: 2f, new MovementParameters { UseSpheres = false });
|
||||
|
||||
var target = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(0x5000BBBBu, TargetStatus.Ok, target, target));
|
||||
Assert.True(h.Manager.Initialized);
|
||||
h.DrainPendingMotions();
|
||||
|
||||
h.WorldPosition = new Position(1u, new Vector3(19.5f, 0f, 0f), Quaternion.Identity); // within DistanceToObject default 0.6
|
||||
h.Advance(2.0);
|
||||
|
||||
h.Manager.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // completed via UseTime -> HandleMoveToPosition.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonObjectMove_TopLevelIdZero_Gate3AlwaysPasses_RegardlessOfInitialized()
|
||||
{
|
||||
// Gate 3: (top_level_object_id == 0 || movement_type == Invalid) ||
|
||||
// initialized. Position/heading moves never set TopLevelObjectId,
|
||||
// so the FIRST disjunct alone always satisfies gate 3 -- Initialized
|
||||
// staying false (as it does for MoveToPosition/TurnToHeading, per
|
||||
// §3c/§3e's notes) never blocks them.
|
||||
var h = new MoveToManagerHarness { ContactValue = true };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false });
|
||||
h.DrainPendingMotions();
|
||||
|
||||
Assert.Equal(0u, h.Manager.TopLevelObjectId);
|
||||
Assert.False(h.Manager.Initialized);
|
||||
|
||||
h.WorldPosition = new Position(1u, new Vector3(19.7f, 0f, 0f), Quaternion.Identity);
|
||||
h.Advance(2.0);
|
||||
h.Manager.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // gate 3 passed via the first disjunct.
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue