using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics.Motion; /// /// R4-V2 — HandleUpdateTarget (0052a7d0, 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. /// 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()); } }