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,157 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V2 — <c>MoveToManager::BeginMoveForward</c> (<c>00529a00</c>, raw
|
||||
/// 306957-307042) per r4-moveto-decomp.md §4c: dispatched motion id / hold
|
||||
/// key, the write-back to the STORED params, and the progress-clock seed.
|
||||
/// Also exercises the run→walk demote inside <c>WalkRunThreshhold</c> (the
|
||||
/// R3 visual-pass expected-diff this closes).
|
||||
/// </summary>
|
||||
public sealed class MoveToManagerBeginMoveForwardTests
|
||||
{
|
||||
[Fact]
|
||||
public void FarFromTarget_CanRunCanWalk_DispatchesRunForward()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f; // already facing target — no aux turn needed
|
||||
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f, WalkRunThreshhold = 15f };
|
||||
// Distance far beyond threshold+dto -> Run.
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(100f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
// MoveToPosition's node plan queues [TurnToHeading(face)] first since
|
||||
// heading(0->target)=90 != current heading is not tested here (we
|
||||
// set Heading=90 already so diff=0, GetCommand still picks motion
|
||||
// because distance is huge, so a turn node is queued anyway — but
|
||||
// since diff==0 the queued turn will complete immediately in
|
||||
// BeginNextNode's synchronous dispatch, landing directly on
|
||||
// BeginMoveForward).
|
||||
// ForwardCommand (post-adjust_motion, dispatched to the interp) is
|
||||
// RunForward; CurrentCommand (the manager's OWN field) stores the
|
||||
// PRE-adjust command GetCommand chose — get_command's own body only
|
||||
// ever returns WalkForward/WalkBackward/0 (§5c) — the Run promotion
|
||||
// happens downstream, inside adjust_motion (_DoMotion §7a), and is
|
||||
// never written back into CurrentCommand.
|
||||
Assert.Equal(MotionCommand.RunForward, h.ForwardCommand);
|
||||
Assert.Equal(HoldKey.Run, h.Manager.Params.HoldKeyToApply);
|
||||
Assert.Equal(MotionCommand.WalkForward, h.Manager.CurrentCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithinWalkRunThreshold_DemotesToWalk()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f, WalkRunThreshhold = 15f };
|
||||
// dist - dto = 10 - 0.6 = 9.4 <= 15 -> walk.
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
Assert.Equal(MotionCommand.WalkForward, h.ForwardCommand);
|
||||
Assert.Equal(HoldKey.None, h.Manager.Params.HoldKeyToApply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCharge_FastPathWins_RunsEvenWhenClose()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f, WalkRunThreshhold = 15f, CanCharge = true };
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(2f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
Assert.Equal(MotionCommand.RunForward, h.ForwardCommand);
|
||||
Assert.Equal(HoldKey.Run, h.Manager.Params.HoldKeyToApply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoldKeyWriteBack_ToStoredParams_NotJustLocalCopy()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f, WalkRunThreshhold = 15f, HoldKeyToApply = HoldKey.Invalid };
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(100f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
Assert.Equal(HoldKey.Run, h.Manager.Params.HoldKeyToApply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProgressClockSeeded_PreviousAndOriginalEqualCurrentDistance()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.CurTime = 5.0;
|
||||
|
||||
// UseSpheres defaults true on a fresh MovementParameters, and
|
||||
// MoveToPosition's own params (copied into Params BEFORE
|
||||
// BeginMoveForward runs) drive GetCurrentDistance's use_spheres
|
||||
// branch: cylinder distance = center distance - ownRadius(0.5) -
|
||||
// targetRadius(0, position moves always zero SoughtObjectRadius).
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false };
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
Assert.Equal(20f, h.Manager.PreviousDistance, 2);
|
||||
Assert.Equal(20f, h.Manager.OriginalDistance, 2);
|
||||
Assert.Equal(5.0, h.Manager.PreviousDistanceTime, 3);
|
||||
Assert.Equal(5.0, h.Manager.OriginalDistanceTime, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProgressClockSeeded_UseSpheresDefault_UsesCylinderDistance()
|
||||
{
|
||||
var h = new MoveToManagerHarness { OwnRadius = 0.5f };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.CurTime = 5.0;
|
||||
|
||||
var p = new MovementParameters { DistanceToObject = 0.6f }; // UseSpheres=true (default)
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), p);
|
||||
|
||||
// center distance 20 - ownRadius 0.5 - targetRadius 0 (position
|
||||
// moves zero SoughtObjectRadius, §3c) = 19.5.
|
||||
Assert.Equal(19.5f, h.Manager.PreviousDistance, 2);
|
||||
Assert.Equal(19.5f, h.Manager.OriginalDistance, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelMoveToBit_ClearedOnLocalParams_DoesNotSelfCancel()
|
||||
{
|
||||
// If the 0x8000 CancelMoveTo bit were NOT cleared on the local
|
||||
// params passed into _DoMotion, InterruptCurrentMovement-style
|
||||
// cancellation logic downstream could tear down THIS moveto before
|
||||
// it starts. We assert the observable effect: the manager is still
|
||||
// MovingTo after BeginMoveForward dispatches.
|
||||
var h = new MoveToManagerHarness();
|
||||
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());
|
||||
|
||||
Assert.True(h.Manager.IsMovingTo());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoPhysicsObj_CancelsWithNoPhysicsObjectCode()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters());
|
||||
Assert.True(h.Manager.IsMovingTo());
|
||||
|
||||
h.Manager.HasPhysicsObj = false;
|
||||
h.Manager.BeginMoveForward();
|
||||
|
||||
Assert.False(h.Manager.IsMovingTo());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue