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,158 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V2 — <c>HandleUpdateTarget</c> (<c>0052a7d0</c>, raw 307802-307867,
|
||||
/// decomp §6d): the P4 TargetTracker feed's deferred-start lifecycle
|
||||
/// (Initialized=false = the first callback vs true = an in-flight retarget),
|
||||
/// context/target-id filtering, self-target instant success, NoObject vs
|
||||
/// ObjectGone status handling, and the retarget progress-clock reset.
|
||||
/// </summary>
|
||||
public sealed class MoveToManagerHandleUpdateTargetTests
|
||||
{
|
||||
private const uint TargetId = 0x50004444u;
|
||||
|
||||
private static MoveToManagerHarness ArmMoveToObject(float ownRadius = 0.5f, float ownHeight = 2f)
|
||||
{
|
||||
var h = new MoveToManagerHarness { OwnRadius = ownRadius, OwnHeight = ownHeight };
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 0f;
|
||||
h.Manager.MoveToObject(TargetId, TargetId, radius: 1f, height: 2f, new MovementParameters());
|
||||
return h;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUpdate_ForADifferentTarget()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
var wrongTargetPos = new Position(1u, new Vector3(5f, 5f, 0f), Quaternion.Identity);
|
||||
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(0x59999999u, TargetStatus.Ok, wrongTargetPos, wrongTargetPos));
|
||||
|
||||
Assert.False(h.Manager.Initialized);
|
||||
Assert.Empty(h.Manager.PendingActions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstCallback_NonOkStatus_CancelsAsNoObject()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
var pos = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.ExitWorld, pos, pos));
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstCallback_OkStatus_BuildsNodePlan_SetsInitialized()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
var target = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target, target));
|
||||
|
||||
Assert.True(h.Manager.Initialized);
|
||||
Assert.NotEmpty(h.Manager.PendingActions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstCallback_OrdinaryTarget_DoesNotFireMoveToComplete()
|
||||
{
|
||||
// MoveToObject's OWN self-target branch (§3b) already short-circuits
|
||||
// via CleanUp+StopCompletely BEFORE any HandleUpdateTarget ever
|
||||
// fires for a same-id target — so HandleUpdateTarget's self-target
|
||||
// instant-success path (§6d: "top_level_object_id ==
|
||||
// physics_obj->id") is reachable only in the deferred-start window,
|
||||
// and is covered by construction in
|
||||
// MoveToManagerNodePlanTests.MoveToObject_SelfTarget_*
|
||||
// (MoveToObject never even reaches SetTarget for a self-id, so the
|
||||
// callback path is dead in practice — retail's redundant guard).
|
||||
// This test isolates the ORDINARY path: MoveToComplete's only
|
||||
// trigger is CleanUpAndCallWeenie, never a plain node-plan build.
|
||||
var h = ArmMoveToObject();
|
||||
var target = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target, target));
|
||||
|
||||
Assert.Empty(h.MoveToCompleteCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retarget_NonOkStatus_CancelsAsObjectGone()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
var target = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target, target));
|
||||
Assert.True(h.Manager.Initialized);
|
||||
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.ExitWorld, target, target));
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retarget_UpdatesPositions_ResetsProgressClock_DoesNotRequeueNodes()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
var target1 = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target1, target1));
|
||||
int nodeCountAfterFirst = System.Linq.Enumerable.Count(h.Manager.PendingActions);
|
||||
Assert.True(nodeCountAfterFirst > 0);
|
||||
|
||||
h.Advance(3.0); // simulate time passing, progress clock advanced by BeginMoveForward
|
||||
|
||||
var target2 = new Position(1u, new Vector3(20f, 5f, 0f), Quaternion.Identity);
|
||||
var interp2 = new Position(1u, new Vector3(19f, 5f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target2, interp2));
|
||||
|
||||
Assert.Equal(target2, h.Manager.CurrentTargetPosition);
|
||||
Assert.Equal(interp2, h.Manager.SoughtPosition);
|
||||
Assert.Equal(float.MaxValue, h.Manager.PreviousDistance);
|
||||
Assert.Equal(float.MaxValue, h.Manager.OriginalDistance);
|
||||
|
||||
// Node count unchanged by the retarget itself (no requeue) — the
|
||||
// running MoveToPosition node keeps steering toward the moved
|
||||
// CurrentTargetPosition on its own next tick.
|
||||
Assert.Equal(nodeCountAfterFirst, System.Linq.Enumerable.Count(h.Manager.PendingActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retarget_TurnToObject_GetsNoRetargetHandling_HeadingFrozen()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 0f;
|
||||
h.Manager.TurnToObject(TargetId, TargetId, new MovementParameters());
|
||||
|
||||
var target1 = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity); // heading 90
|
||||
h.Manager.TurnToObject_Internal(target1); // first callback (direct, mirrors deferred-start call shape)
|
||||
Assert.True(h.Manager.Initialized);
|
||||
var soughtAfterFirst = h.Manager.SoughtPosition;
|
||||
|
||||
// A retarget-shaped HandleUpdateTarget call for a TurnToObject
|
||||
// manager: since Initialized is already true, this takes the
|
||||
// "retarget" branch, which only updates state for MoveToObject —
|
||||
// TurnToObject gets NO handling at all (decomp §6d note).
|
||||
var target2 = new Position(1u, new Vector3(0f, 10f, 0f), Quaternion.Identity); // heading 0
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target2, target2));
|
||||
|
||||
Assert.Equal(soughtAfterFirst, h.Manager.SoughtPosition); // untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoPhysicsObj_CancelsWithNoPhysicsObjectCode()
|
||||
{
|
||||
var h = ArmMoveToObject();
|
||||
h.Manager.HasPhysicsObj = false;
|
||||
|
||||
var target = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(new TargetInfo(TargetId, TargetStatus.Ok, target, target));
|
||||
|
||||
Assert.False(h.Manager.IsMovingTo());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue