Gate result: user side-by-side vs retail — "looks good, as close to retail now as I can see". Telemetry from the gate session (launch-170-gate2.log): BeginMoveForward ~1:1 with MoveToObject arms for every chasing creature (pre-fix capture was 16:1), ZERO "SERVERVEL skips UseTime while armed" occurrences, pending_motions depth 1 at last sample. Stripped per the #170 close-out plan: s_mvtoDiag + all [mvto] prints (MoveToManager), s_drainDiag + [drain]/[drainq] (MotionInterpreter), the [npc-tick] branch prints and the "UM actions" inbound-action dump (GameWindow). The durable ACDREAM_DUMP_MOTION family stays. The RemoteChaseEndToEndHarnessTests + RemoteChaseDrainBisectTests conformance suites stay as the permanent regression net for this pipeline. ISSUES: #170 -> DONE + Recently-closed entry (fix SHAs427332ac,d2ccc80e,1051fc83). Memory topic + index flipped to CLOSED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1634 lines
70 KiB
C#
1634 lines
70 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.Core.Physics.Motion;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// MoveToManager — R4-V2 verbatim port of retail's MoveToManager
|
||
// (acclient.h:31473, struct #3462; docs/research/2026-07-03-r4-moveto/
|
||
// r4-moveto-decomp.md, all 33 members, addresses 0x00529010-0x0052a987).
|
||
//
|
||
// Retail chain this class stands in for (r4-port-plan.md §4):
|
||
// MovementManager::PerformMovement (types 6-9) → MoveToManager::PerformMovement
|
||
// → MoveToManager entry points (MoveToObject/MoveToPosition/TurnToObject/
|
||
// TurnToHeading) → node plan (pending_actions) → BeginNextNode →
|
||
// BeginMoveForward/BeginTurnToHeading → _DoMotion/_StopMotion →
|
||
// CMotionInterp.adjust_motion → DoInterpretedMotion/StopInterpretedMotion.
|
||
//
|
||
// MovementManager itself is R5 scope (r4-port-plan.md §5 "do-not-invent") —
|
||
// this class is bound DIRECTLY to the owning entity's MotionInterpreter by the
|
||
// (future) V4/V5 orchestrator; the type-6..9 dispatch stays at the existing
|
||
// GameWindow/controller call sites until R5 grows the relay.
|
||
//
|
||
// TargetManager / StickyManager / ConstraintManager / PositionManager::StickTo
|
||
// bodies were NOT extracted (call shapes only, decomp §9f/§9g) — R5 scope.
|
||
// This class exposes them as ctor-injected seams (Action/Func delegates),
|
||
// matching the MotionInterpreter Action? seam convention
|
||
// (UnstickFromObject/InterruptCurrentMovement/RemoveLinkAnimations/
|
||
// InitializeMotionTables/CheckForCompletedMotions).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// R4-V2 — verbatim port of retail's <c>MoveToManager</c> (acclient.h:31473,
|
||
/// struct #3462). Drives an entity's chase/flee/turn-to/turn-to-heading
|
||
/// state machine: builds a small node plan (<see cref="MoveToNode"/>,
|
||
/// <c>pending_actions</c>), steps it every tick via <see cref="UseTime"/>,
|
||
/// and issues the resulting motion commands through the SAME
|
||
/// <see cref="MotionInterpreter.DoInterpretedMotion(uint,MovementParameters)"/>/
|
||
/// <see cref="MotionInterpreter.StopInterpretedMotion(uint,MovementParameters)"/>
|
||
/// seam every other interpreted motion uses (<c>_DoMotion</c>/<c>_StopMotion</c>,
|
||
/// §7a/§7b — MoveToManager NEVER calls <c>DoMotion</c>, <c>set_hold_run</c>, or
|
||
/// any raw-state API directly).
|
||
///
|
||
/// <para>
|
||
/// <b>Construction contract (r4-port-plan.md §4):</b> ctor-injected with the
|
||
/// entity's <see cref="MotionInterpreter"/> (the <c>_DoMotion</c> target) plus
|
||
/// a set of seam delegates standing in for retail's <c>CPhysicsObj</c>
|
||
/// position/heading/body-shape accessors and the not-yet-ported
|
||
/// TargetManager/StickyManager (R5). All seam delegates are REQUIRED
|
||
/// (non-nullable) except the three explicitly retail-optional ones
|
||
/// (<see cref="Unstick"/>, <see cref="StickTo"/>, <see cref="MoveToComplete"/>)
|
||
/// which mirror the no-op-until-bound convention already used by
|
||
/// <see cref="MotionInterpreter.UnstickFromObject"/> /
|
||
/// <see cref="MotionInterpreter.InterruptCurrentMovement"/>.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>The "no physics_obj" guard.</b> Retail gates almost every member on
|
||
/// <c>this->physics_obj != 0</c> (a raw pointer that can be null before
|
||
/// <c>SetPhysicsObject</c> is called, or after the owning <c>CPhysicsObj</c>
|
||
/// is destroyed). This port models the SAME guard via <see cref="HasPhysicsObj"/>
|
||
/// (settable, defaults true) rather than requiring callers to pass a nullable
|
||
/// body reference through every seam — the seam delegates themselves are
|
||
/// assumed valid whenever <see cref="HasPhysicsObj"/> is true (matching
|
||
/// retail's single non-null pointer covering the whole entry-point family).
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class MoveToManager
|
||
{
|
||
private readonly MotionInterpreter _interp;
|
||
|
||
// ── seam delegates (ctor-injected; r4-port-plan.md §4) ─────────────────
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::StopCompletely</c> — routes through
|
||
/// <c>MovementStruct{type=5}</c> to the SAME interp
|
||
/// (<see cref="MotionInterpreter.StopCompletely"/> pre-R5; direct call is
|
||
/// the same body).</summary>
|
||
private readonly Action _stopCompletely;
|
||
|
||
/// <summary>Retail <c>physics_obj->m_position</c> read (world position
|
||
/// + cell id) — the CURRENT position, sampled fresh every call site that
|
||
/// reads <c>myPos</c>/<c>curPos</c>.</summary>
|
||
private readonly Func<Position> _getPosition;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::get_heading</c> — current compass
|
||
/// heading, degrees (P5 convention).</summary>
|
||
private readonly Func<float> _getHeading;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::set_heading(heading, send)</c> — the
|
||
/// ONE heading snap in the whole family
|
||
/// (<see cref="HandleTurnToHeading"/>'s arrival snap, decomp §6c
|
||
/// @0052a146). <c>send</c> flags the outbound network echo (remotes:
|
||
/// no-op send per the wiring contract).</summary>
|
||
private readonly Action<float, bool> _setHeading;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::GetRadius</c> — the MOVER's own
|
||
/// radius (feeds <see cref="GetCurrentDistance"/>'s cylinder-distance
|
||
/// own-side argument).</summary>
|
||
private readonly Func<float> _getOwnRadius;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::GetHeight</c> — the MOVER's own
|
||
/// height.</summary>
|
||
private readonly Func<float> _getOwnHeight;
|
||
|
||
/// <summary>Retail <c>physics_obj->transient_state & 1</c> —
|
||
/// CONTACT bit (<see cref="UseTime"/>'s tick gate, decomp §6a).</summary>
|
||
private readonly Func<bool> _contact;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::IsInterpolating</c> →
|
||
/// <c>PositionManager::IsInterpolating</c> — consumed by the
|
||
/// fail-progress stall tests (decomp §6b/§6c).</summary>
|
||
private readonly Func<bool> _isInterpolating;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::get_velocity</c> — feeds the Phase-3
|
||
/// TargetManager quantum retune (decomp §6b Phase 3).</summary>
|
||
private readonly Func<Vector3> _getVelocity;
|
||
|
||
/// <summary>Retail <c>physics_obj->id</c> — the mover's own object id
|
||
/// (self-target detection in MoveToObject/TurnToObject, §3b/§3d).</summary>
|
||
private readonly Func<uint> _getSelfId;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::set_target(context_id, object_id,
|
||
/// radius, quantum)</c> → <c>TargetManager::SetTarget</c> (call shape
|
||
/// only, decomp §9f — R5 owns the body). MoveToManager always passes
|
||
/// <c>(0, top_level_object_id, 0.5f, 0.0)</c>.</summary>
|
||
private readonly Action<uint, uint, float, double> _setTarget;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::clear_target</c> →
|
||
/// <c>TargetManager::ClearTarget</c> (call shape only).</summary>
|
||
private readonly Action _clearTarget;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::get_target_quantum</c> (call shape
|
||
/// only; ACE: returns <c>TargetInfo.Quantum</c>, default 0).</summary>
|
||
private readonly Func<double> _getTargetQuantum;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::set_target_quantum</c> →
|
||
/// <c>TargetManager::SetTargetQuantum</c> (call shape only).</summary>
|
||
private readonly Action<double> _setTargetQuantum;
|
||
|
||
/// <summary>Retail <c>CPhysicsObj::unstick_from_object</c> →
|
||
/// <c>PositionManager::UnStick</c> (call shape only, R5 body). Called at
|
||
/// the head of every <see cref="PerformMovement"/> (§3a). Optional —
|
||
/// null is a silent no-op, matching
|
||
/// <see cref="MotionInterpreter.UnstickFromObject"/>'s convention.</summary>
|
||
public Action? Unstick { get; set; }
|
||
|
||
/// <summary>Retail <c>PositionManager::StickTo(object_id, radius,
|
||
/// height)</c> (call shape only, R5 StickyManager body) — the sticky
|
||
/// arrival handoff in <see cref="BeginNextNode"/> (§4b). Optional —
|
||
/// null is a silent no-op.</summary>
|
||
public Action<uint, float, float>? StickTo { get; set; }
|
||
|
||
/// <summary>
|
||
/// CLIENT ADDITION — NOT retail (decomp §7e / do-not-invent list):
|
||
/// <c>CleanUpAndCallWeenie</c> contains no weenie call in this build
|
||
/// (the name is vestigial; body ≡ CleanUp + StopCompletely), and retail
|
||
/// notifies NOTHING on arrival (§4b's empty-queue completion is inline
|
||
/// CleanUp + StopCompletely). This seam stands in for ACE's server-side
|
||
/// <c>OnMoveComplete</c> notification so the App layer can re-anchor
|
||
/// AD-27 (Use/PickUp re-send on arrival). Fires with
|
||
/// <see cref="WeenieError.None"/> on NATURAL COMPLETION — the
|
||
/// <see cref="BeginNextNode"/> empty-queue exits (both sticky and
|
||
/// non-sticky) and <see cref="CleanUpAndCallWeenie"/>'s instant-success
|
||
/// path — and NEVER from <see cref="CancelMoveTo"/>/plain
|
||
/// <see cref="CleanUp"/> (a cancel is not an arrival; AD-27's re-send
|
||
/// must not fire on user interrupt). Optional — null is a silent no-op.
|
||
/// </summary>
|
||
public Action<WeenieError>? MoveToComplete { get; set; }
|
||
|
||
/// <summary>Retail <c>Timer::cur_time</c> — injectable clock (tests drive
|
||
/// this explicitly; production binds to the real wall/game clock).
|
||
/// Defaults to a monotonically-increasing stub if not overridden via the
|
||
/// ctor (every call site needs SOME value; production always supplies
|
||
/// one).</summary>
|
||
private readonly Func<double> _curTime;
|
||
|
||
/// <summary>
|
||
/// Retail's <c>physics_obj != 0</c> guard, modeled as a settable flag
|
||
/// (see the class-level "no physics_obj" doc). Defaults true — most
|
||
/// production entities always have a body by construction time.
|
||
/// </summary>
|
||
public bool HasPhysicsObj { get; set; } = true;
|
||
|
||
public MoveToManager(
|
||
MotionInterpreter interp,
|
||
Action stopCompletely,
|
||
Func<Position> getPosition,
|
||
Func<float> getHeading,
|
||
Action<float, bool> setHeading,
|
||
Func<float> getOwnRadius,
|
||
Func<float> getOwnHeight,
|
||
Func<bool> contact,
|
||
Func<bool> isInterpolating,
|
||
Func<Vector3> getVelocity,
|
||
Func<uint> getSelfId,
|
||
Action<uint, uint, float, double> setTarget,
|
||
Action clearTarget,
|
||
Func<double> getTargetQuantum,
|
||
Action<double> setTargetQuantum,
|
||
Func<double>? curTime = null)
|
||
{
|
||
_interp = interp ?? throw new ArgumentNullException(nameof(interp));
|
||
_stopCompletely = stopCompletely ?? throw new ArgumentNullException(nameof(stopCompletely));
|
||
_getPosition = getPosition ?? throw new ArgumentNullException(nameof(getPosition));
|
||
_getHeading = getHeading ?? throw new ArgumentNullException(nameof(getHeading));
|
||
_setHeading = setHeading ?? throw new ArgumentNullException(nameof(setHeading));
|
||
_getOwnRadius = getOwnRadius ?? throw new ArgumentNullException(nameof(getOwnRadius));
|
||
_getOwnHeight = getOwnHeight ?? throw new ArgumentNullException(nameof(getOwnHeight));
|
||
_contact = contact ?? throw new ArgumentNullException(nameof(contact));
|
||
_isInterpolating = isInterpolating ?? throw new ArgumentNullException(nameof(isInterpolating));
|
||
_getVelocity = getVelocity ?? throw new ArgumentNullException(nameof(getVelocity));
|
||
_getSelfId = getSelfId ?? throw new ArgumentNullException(nameof(getSelfId));
|
||
_setTarget = setTarget ?? throw new ArgumentNullException(nameof(setTarget));
|
||
_clearTarget = clearTarget ?? throw new ArgumentNullException(nameof(clearTarget));
|
||
_getTargetQuantum = getTargetQuantum ?? throw new ArgumentNullException(nameof(getTargetQuantum));
|
||
_setTargetQuantum = setTargetQuantum ?? throw new ArgumentNullException(nameof(setTargetQuantum));
|
||
|
||
double t = 0.0;
|
||
_curTime = curTime ?? (() => t += 1.0 / 30.0);
|
||
|
||
InitializeLocalVariables();
|
||
}
|
||
|
||
// ── state block (retail acclient.h:31473 field-for-field) ──────────────
|
||
|
||
/// <summary>+0x00 <c>movement_type</c>.</summary>
|
||
public MovementType MovementTypeState { get; private set; } = MovementType.Invalid;
|
||
|
||
/// <summary>
|
||
/// Retail's default-constructed <c>Position</c> (identity
|
||
/// <c>CellFrame</c>, cell id 0) — NOT C#'s <c>default(Position)</c>,
|
||
/// whose <c>default(Quaternion)</c> is the ZERO quaternion (0,0,0,0),
|
||
/// not the identity rotation (0,0,0,1). <c>GetHeading</c> on a zero
|
||
/// quaternion is degenerate (does not read as "heading 0" — the
|
||
/// underlying <c>Vector3.Transform</c> collapses to zero). Retail's
|
||
/// ctor (§1a) and <see cref="InitializeLocalVariables"/> (§1c) both
|
||
/// reset these fields to a genuine identity frame.
|
||
/// </summary>
|
||
private static readonly Position IdentityPosition = new(0u, Vector3.Zero, Quaternion.Identity);
|
||
|
||
/// <summary>+0x04 <c>sought_position</c>.</summary>
|
||
public Position SoughtPosition { get; private set; } = IdentityPosition;
|
||
|
||
/// <summary>+0x4C <c>current_target_position</c>.</summary>
|
||
public Position CurrentTargetPosition { get; private set; } = IdentityPosition;
|
||
|
||
/// <summary>+0x94 <c>starting_position</c>.</summary>
|
||
public Position StartingPosition { get; private set; } = IdentityPosition;
|
||
|
||
/// <summary>+0xDC <c>movement_params</c> (the 10-field copy every entry
|
||
/// point performs).</summary>
|
||
public MovementParameters Params { get; private set; } = new();
|
||
|
||
/// <summary>+0x108 <c>previous_heading</c>.</summary>
|
||
public float PreviousHeading { get; private set; }
|
||
|
||
/// <summary>+0x10C <c>previous_distance</c>.</summary>
|
||
public float PreviousDistance { get; private set; }
|
||
|
||
/// <summary>+0x110 <c>previous_distance_time</c>.</summary>
|
||
public double PreviousDistanceTime { get; private set; }
|
||
|
||
/// <summary>+0x118 <c>original_distance</c>.</summary>
|
||
public float OriginalDistance { get; private set; }
|
||
|
||
/// <summary>+0x120 <c>original_distance_time</c>.</summary>
|
||
public double OriginalDistanceTime { get; private set; }
|
||
|
||
/// <summary>+0x128 <c>fail_progress_count</c> — WRITE-ONLY (decomp §8;
|
||
/// do-not-invent: no give-up threshold exists in retail). Exposed for
|
||
/// conformance-test inspection only.</summary>
|
||
public uint FailProgressCount { get; private set; }
|
||
|
||
/// <summary>+0x12C <c>sought_object_id</c>.</summary>
|
||
public uint SoughtObjectId { get; private set; }
|
||
|
||
/// <summary>+0x130 <c>top_level_object_id</c>.</summary>
|
||
public uint TopLevelObjectId { get; private set; }
|
||
|
||
/// <summary>+0x134 <c>sought_object_radius</c>.</summary>
|
||
public float SoughtObjectRadius { get; private set; }
|
||
|
||
/// <summary>+0x138 <c>sought_object_height</c>.</summary>
|
||
public float SoughtObjectHeight { get; private set; }
|
||
|
||
/// <summary>+0x13C <c>current_command</c>.</summary>
|
||
public uint CurrentCommand { get; private set; }
|
||
|
||
/// <summary>+0x140 <c>aux_command</c>.</summary>
|
||
public uint AuxCommand { get; private set; }
|
||
|
||
/// <summary>+0x144 <c>moving_away</c>.</summary>
|
||
public bool MovingAway { get; private set; }
|
||
|
||
/// <summary>+0x148 <c>initialized</c>.</summary>
|
||
public bool Initialized { get; private set; }
|
||
|
||
/// <summary>+0x14C/+0x150 <c>pending_actions</c> (DLList) — ported as a
|
||
/// managed <see cref="LinkedList{T}"/> of <see cref="MoveToNode"/>, tail-append
|
||
/// / head-pop (matching <c>InsertAfter</c>/<c>RemovePendingActionsHead</c>
|
||
/// shape). Exposed read-only for conformance tests.</summary>
|
||
private readonly LinkedList<MoveToNode> _pendingActions = new();
|
||
|
||
/// <summary>Read-only inspection surface (tests): the node queue in
|
||
/// head-to-tail order.</summary>
|
||
public IEnumerable<MoveToNode> PendingActions => _pendingActions;
|
||
|
||
// ── 1. Creation / lifecycle ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::InitializeLocalVariables</c>
|
||
/// (<c>00529250</c>, raw 306490-306534). VERBATIM per decomp §1c: zeroes
|
||
/// <see cref="MovementTypeState"/> (Invalid) and the params BITFIELD +
|
||
/// CONTEXT ID ONLY (<c>movement_params.__inner0 = 0;
|
||
/// movement_params.context_id = 0;</c> — NOT ACE's A2/A3
|
||
/// full-struct-reset transposition; the scalar param fields keep their
|
||
/// STALE values because every entry point copies all ten fields anyway),
|
||
/// resets both progress-clock pairs to <c>FLT_MAX</c>/now (
|
||
/// <see cref="PreviousDistance"/>/<see cref="OriginalDistance"/>),
|
||
/// <see cref="PreviousHeading"/>=0, <see cref="FailProgressCount"/>=0,
|
||
/// <see cref="CurrentCommand"/>/<see cref="AuxCommand"/>=0,
|
||
/// <see cref="MovingAway"/>/<see cref="Initialized"/>=false, resets
|
||
/// <see cref="SoughtPosition"/>/<see cref="CurrentTargetPosition"/> to
|
||
/// cell 0 + identity frame (<see cref="StartingPosition"/> is NOT
|
||
/// touched here), and zeroes <see cref="SoughtObjectId"/>/
|
||
/// <see cref="TopLevelObjectId"/>/<see cref="SoughtObjectRadius"/>/
|
||
/// <see cref="SoughtObjectHeight"/>. Does NOT drain
|
||
/// <see cref="_pendingActions"/> — callers (<see cref="Destroy"/>/
|
||
/// <see cref="CancelMoveTo"/>) drain first.
|
||
/// </summary>
|
||
public void InitializeLocalVariables()
|
||
{
|
||
MovementTypeState = MovementType.Invalid;
|
||
|
||
// movement_params.__inner0 = 0 (bitfield CLEARED, not re-defaulted);
|
||
// movement_params.context_id = 0. Scalars stay whatever they were —
|
||
// model this by clearing only the bitfield-backed bools + ContextId
|
||
// on the EXISTING Params instance (do not replace it — a replace
|
||
// would also reset the scalars, diverging from retail's field-level
|
||
// clear).
|
||
Params.CanWalk = false;
|
||
Params.CanRun = false;
|
||
Params.CanSidestep = false;
|
||
Params.CanWalkBackwards = false;
|
||
Params.CanCharge = false;
|
||
Params.FailWalk = false;
|
||
Params.UseFinalHeading = false;
|
||
Params.Sticky = false;
|
||
Params.MoveAway = false;
|
||
Params.MoveTowards = false;
|
||
Params.UseSpheres = false;
|
||
Params.SetHoldKey = false;
|
||
Params.Autonomous = false;
|
||
Params.ModifyRawState = false;
|
||
Params.ModifyInterpretedState = false;
|
||
Params.CancelMoveTo = false;
|
||
Params.StopCompletelyFlag = false;
|
||
Params.DisableJumpDuringLink = false;
|
||
Params.ContextId = 0;
|
||
|
||
PreviousDistance = float.MaxValue;
|
||
PreviousDistanceTime = _curTime();
|
||
OriginalDistance = float.MaxValue;
|
||
OriginalDistanceTime = _curTime();
|
||
|
||
PreviousHeading = 0f;
|
||
FailProgressCount = 0;
|
||
CurrentCommand = 0;
|
||
AuxCommand = 0;
|
||
MovingAway = false;
|
||
Initialized = false;
|
||
|
||
SoughtPosition = IdentityPosition;
|
||
CurrentTargetPosition = IdentityPosition;
|
||
|
||
SoughtObjectId = 0;
|
||
TopLevelObjectId = 0;
|
||
SoughtObjectRadius = 0f;
|
||
SoughtObjectHeight = 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::Destroy</c> (<c>005294b0</c>, raw
|
||
/// 306618-306663): drains <see cref="_pendingActions"/> then tailcalls
|
||
/// <see cref="InitializeLocalVariables"/>.
|
||
/// </summary>
|
||
public void Destroy()
|
||
{
|
||
_pendingActions.Clear();
|
||
InitializeLocalVariables();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::is_moving_to</c> (<c>00529220</c>, raw
|
||
/// 306464-306470): <c>movement_type != Invalid</c>.
|
||
/// </summary>
|
||
public bool IsMovingTo() => MovementTypeState != MovementType.Invalid;
|
||
|
||
// ── 3. Entry points (movement_type setters) ─────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::PerformMovement</c> (<c>0052a900</c>, raw
|
||
/// 307871-307904). VERBATIM per decomp §3a: cancels any in-flight moveto
|
||
/// (<see cref="CancelMoveTo"/> with <see cref="WeenieError.ActionCancelled"/>
|
||
/// 0x36) and unsticks FIRST, unconditionally, before the 4-way type
|
||
/// dispatch (MoveToObject/MoveToPosition/TurnToObject/TurnToHeading).
|
||
/// Always returns <see cref="WeenieError.None"/> — moveto errors surface
|
||
/// via <see cref="CancelMoveTo"/>, never via this return value (decomp
|
||
/// §2b note, carried through §3a).
|
||
/// </summary>
|
||
public WeenieError PerformMovement(MovementStruct mvs)
|
||
{
|
||
CancelMoveTo(WeenieError.ActionCancelled);
|
||
Unstick?.Invoke();
|
||
|
||
switch (mvs.Type)
|
||
{
|
||
case MovementType.MoveToObject:
|
||
MoveToObject(mvs.ObjectId, mvs.TopLevelId, mvs.Radius, mvs.Height, mvs.Params ?? new MovementParameters());
|
||
break;
|
||
case MovementType.MoveToPosition:
|
||
MoveToPosition(mvs.Pos, mvs.Params ?? new MovementParameters());
|
||
break;
|
||
case MovementType.TurnToObject:
|
||
TurnToObject(mvs.ObjectId, mvs.TopLevelId, mvs.Params ?? new MovementParameters());
|
||
break;
|
||
case MovementType.TurnToHeading:
|
||
TurnToHeading(mvs.Params ?? new MovementParameters());
|
||
break;
|
||
}
|
||
|
||
return WeenieError.None;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::MoveToObject</c> (<c>00529680</c>, raw
|
||
/// 306756-306817). VERBATIM per decomp §3b: StopCompletely, snapshot
|
||
/// <see cref="StartingPosition"/>, store sought object id/radius/height,
|
||
/// set <see cref="MovementTypeState"/>=MoveToObject,
|
||
/// <see cref="TopLevelObjectId"/>, copy all ten <paramref name="p"/>
|
||
/// fields into <see cref="Params"/>, <see cref="Initialized"/>=false.
|
||
/// Self-target (<paramref name="topLevelId"/> == the mover's own id) →
|
||
/// <see cref="CleanUp"/> + StopCompletely (degenerate instant no-op).
|
||
/// Otherwise → <see cref="_setTarget"/>(0, topLevelId, 0.5f, 0.0) — the
|
||
/// P4 TargetTracker seam. NO nodes are queued yet — object moves are
|
||
/// deferred until the first <see cref="HandleUpdateTarget"/> callback
|
||
/// (§6). NOTE: <see cref="MovementParameters.Sticky"/> is PRESERVED here
|
||
/// (unlike position/turn-heading moves, which force it off — §3c/§3e).
|
||
/// </summary>
|
||
public void MoveToObject(uint objectId, uint topLevelId, float radius, float height, MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return;
|
||
|
||
_stopCompletely();
|
||
StartingPosition = _getPosition();
|
||
SoughtObjectId = objectId;
|
||
SoughtObjectRadius = radius;
|
||
SoughtObjectHeight = height;
|
||
MovementTypeState = MovementType.MoveToObject;
|
||
TopLevelObjectId = topLevelId;
|
||
CopyParams(p);
|
||
Initialized = false;
|
||
|
||
if (topLevelId == _getSelfId())
|
||
{
|
||
CleanUp();
|
||
_stopCompletely();
|
||
}
|
||
else
|
||
{
|
||
_setTarget(0, TopLevelObjectId, 0.5f, 0.0);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::MoveToPosition</c> (<c>0052a240</c>, raw
|
||
/// 307521-307593). VERBATIM per decomp §3c: StopCompletely, snapshot
|
||
/// <see cref="CurrentTargetPosition"/>=<paramref name="target"/>,
|
||
/// <see cref="SoughtObjectRadius"/>=0, compute
|
||
/// <see cref="GetCurrentDistance"/>, compute the heading-to-target minus
|
||
/// current heading (epsilon-snapped/wrapped, same [0,360) normalization
|
||
/// used throughout), <see cref="MovementParameters.GetCommand"/> to see
|
||
/// if ANY motion is needed — if so, queue
|
||
/// <c>[TurnToHeading(face target)] → [MoveToPosition]</c>. If
|
||
/// <see cref="MovementParameters.UseFinalHeading"/> (0x40) is set, ALSO
|
||
/// queue a final <c>TurnToHeading(desired_heading)</c> (absolute, unlike
|
||
/// <see cref="MoveToObject_Internal"/>'s relative form). Then snapshot
|
||
/// <see cref="SoughtPosition"/>/<see cref="StartingPosition"/>, set
|
||
/// <see cref="MovementTypeState"/>=MoveToPosition, copy all ten
|
||
/// <paramref name="p"/> fields into <see cref="Params"/>, CLEAR the
|
||
/// sticky bit (0x80 — position moves and heading turns can never stick;
|
||
/// only object moves preserve it), then <see cref="BeginNextNode"/>.
|
||
///
|
||
/// <para>
|
||
/// Reads the ARGUMENT's <see cref="MovementParameters.UseFinalHeading"/>
|
||
/// (<paramref name="p"/>), NOT a stale member — the do-not-invent list's
|
||
/// "A1 stale-member UseFinalHeading read" ACE-ism is explicitly NOT
|
||
/// copied here.
|
||
/// </para>
|
||
/// </summary>
|
||
public void MoveToPosition(Position target, MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return;
|
||
|
||
_stopCompletely();
|
||
CurrentTargetPosition = target;
|
||
SoughtObjectRadius = 0f;
|
||
|
||
float dist = GetCurrentDistance();
|
||
|
||
Vector3 myPos = _getPosition().Frame.Origin;
|
||
Vector3 targetPos = target.Frame.Origin;
|
||
float toTargetHeading = MoveToMath.PositionHeading(myPos, targetPos);
|
||
float headingDiff = toTargetHeading - _getHeading();
|
||
if (MathF.Abs(headingDiff) < MoveToMath.Epsilon) headingDiff = 0f;
|
||
if (headingDiff < -MoveToMath.Epsilon) headingDiff += 360f;
|
||
|
||
p.GetCommand(dist, headingDiff, out uint cmd, out _, out _);
|
||
if (cmd != 0)
|
||
{
|
||
AddTurnToHeadingNode(toTargetHeading);
|
||
AddMoveToPositionNode();
|
||
}
|
||
|
||
if (p.UseFinalHeading)
|
||
{
|
||
AddTurnToHeadingNode(p.DesiredHeading);
|
||
}
|
||
|
||
SoughtPosition = target;
|
||
StartingPosition = _getPosition();
|
||
MovementTypeState = MovementType.MoveToPosition;
|
||
CopyParams(p);
|
||
Params.Sticky = false; // __inner0 &= 0xffffff7f
|
||
|
||
BeginNextNode();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::TurnToObject</c> (<c>005297d0</c>, raw
|
||
/// 306820-306882). VERBATIM per decomp §3d, INCLUDING the desired-heading
|
||
/// clobber quirk: <paramref name="p"/>.StopCompletelyFlag's underlying bit
|
||
/// (0x10000) — here, <see cref="MovementParameters.StopCompletelyFlag"/> —
|
||
/// gates the StopCompletely call CONDITIONALLY (unlike MoveToObject/
|
||
/// MoveToPosition's UNCONDITIONAL stop). Seeds
|
||
/// <c>current_target_position.frame</c>'s heading from
|
||
/// <paramref name="p"/>.DesiredHeading — but this write is DISCARDED:
|
||
/// <see cref="TurnToObject_Internal"/> later overwrites
|
||
/// <see cref="CurrentTargetPosition"/> wholesale and reads
|
||
/// <see cref="SoughtPosition"/>'s heading instead (which is 0 after
|
||
/// <see cref="InitializeLocalVariables"/> for a fresh manager). This is a
|
||
/// RETAIL QUIRK (ACE MoveToManager.cs:246 matches verbatim) — do NOT
|
||
/// "fix" it; the effective final heading is simply "face the object".
|
||
/// Self-target → <see cref="CleanUp"/> + StopCompletely. Otherwise →
|
||
/// <see cref="Initialized"/>=false + <see cref="_setTarget"/>(0,
|
||
/// topLevelId, 0.5f, 0.0). Deferred like MoveToObject — no nodes queued
|
||
/// here; <see cref="TurnToObject_Internal"/> queues on the first target
|
||
/// callback.
|
||
/// </summary>
|
||
public void TurnToObject(uint objectId, uint topLevelId, MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return;
|
||
|
||
if (p.StopCompletelyFlag)
|
||
{
|
||
_stopCompletely();
|
||
}
|
||
|
||
MovementTypeState = MovementType.TurnToObject;
|
||
SoughtObjectId = objectId;
|
||
|
||
// Frame::set_heading(¤t_target_position.frame, desired_heading)
|
||
// — clobbered before any read; see the quirk note above.
|
||
CurrentTargetPosition = CurrentTargetPosition with
|
||
{
|
||
Frame = new CellFrame(
|
||
CurrentTargetPosition.Frame.Origin,
|
||
MoveToMath.SetHeading(CurrentTargetPosition.Frame.Orientation, p.DesiredHeading)),
|
||
};
|
||
|
||
TopLevelObjectId = topLevelId;
|
||
CopyParams(p);
|
||
|
||
if (topLevelId == _getSelfId())
|
||
{
|
||
CleanUp();
|
||
_stopCompletely();
|
||
}
|
||
else
|
||
{
|
||
Initialized = false;
|
||
_setTarget(0, topLevelId, 0.5f, 0.0);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::TurnToHeading</c> (<c>0052a630</c>, raw
|
||
/// 307706-307772). VERBATIM per decomp §3e: conditional StopCompletely
|
||
/// (0x10000 stop_completely bit, same shape as
|
||
/// <see cref="TurnToObject"/>), copy all ten <paramref name="p"/> fields,
|
||
/// CLEAR sticky (0x80), seed <see cref="SoughtPosition"/>'s frame heading
|
||
/// from <paramref name="p"/>.DesiredHeading, set
|
||
/// <see cref="MovementTypeState"/>=TurnToHeading, queue ONE
|
||
/// <see cref="MoveToNode"/> (type TurnToHeading, heading=DesiredHeading)
|
||
/// directly (inlined <c>AddTurnToHeadingNode</c> shape — same effect),
|
||
/// then IMMEDIATE <see cref="BeginNextNode"/> — unlike ACE's A4 gap
|
||
/// (one-tick-late), retail calls <c>BeginNextNode</c> synchronously
|
||
/// inside this entry point. <see cref="Initialized"/> is NOT set here
|
||
/// (stays whatever it was) — the <see cref="UseTime"/> gate (§6a) passes
|
||
/// anyway because <see cref="TopLevelObjectId"/> is 0 for non-object
|
||
/// moves (TurnToHeading never sets it).
|
||
/// </summary>
|
||
public void TurnToHeading(MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return;
|
||
|
||
if (p.StopCompletelyFlag)
|
||
{
|
||
_stopCompletely();
|
||
}
|
||
|
||
CopyParams(p);
|
||
Params.Sticky = false; // __inner0 &= 0xffffff7f
|
||
|
||
SoughtPosition = SoughtPosition with
|
||
{
|
||
Frame = new CellFrame(
|
||
SoughtPosition.Frame.Origin,
|
||
MoveToMath.SetHeading(SoughtPosition.Frame.Orientation, p.DesiredHeading)),
|
||
};
|
||
|
||
MovementTypeState = MovementType.TurnToHeading;
|
||
|
||
_pendingActions.AddLast(new MoveToNode(MovementType.TurnToHeading, p.DesiredHeading));
|
||
|
||
BeginNextNode();
|
||
}
|
||
|
||
// ── 4. Node stepping ─────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::AddTurnToHeadingNode</c> (<c>00529530</c>,
|
||
/// raw 306667-306685): tail-append a TurnToHeading node.
|
||
/// </summary>
|
||
public void AddTurnToHeadingNode(float heading)
|
||
=> _pendingActions.AddLast(new MoveToNode(MovementType.TurnToHeading, heading));
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::AddMoveToPositionNode</c> (<c>00529580</c>,
|
||
/// raw 306689-306706): tail-append a MoveToPosition node (heading
|
||
/// unused).
|
||
/// </summary>
|
||
public void AddMoveToPositionNode()
|
||
=> _pendingActions.AddLast(new MoveToNode(MovementType.MoveToPosition, 0f));
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::RemovePendingActionsHead</c>
|
||
/// (<c>00529380</c>, raw 306538-306550): unlink + delete the head node.
|
||
/// </summary>
|
||
public void RemovePendingActionsHead()
|
||
{
|
||
if (_pendingActions.First is not null)
|
||
_pendingActions.RemoveFirst();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::BeginNextNode</c> (<c>00529cb0</c>, raw
|
||
/// 307123-307171). VERBATIM per decomp §4b — THE STICKY HANDOFF: if a
|
||
/// node exists, tailcall <see cref="BeginMoveForward"/> (type
|
||
/// MoveToPosition) or <see cref="BeginTurnToHeading"/> (type
|
||
/// TurnToHeading); an unknown node type stalls (defensive, matches the
|
||
/// raw's <c>if/if</c> shape — no <c>else</c> fallthrough). Empty queue =
|
||
/// moveto complete: if <see cref="MovementParameters.Sticky"/> (byte0
|
||
/// sign bit 0x80) is set, READ <see cref="SoughtObjectRadius"/>/
|
||
/// <see cref="SoughtObjectHeight"/>/<see cref="TopLevelObjectId"/>
|
||
/// BEFORE <see cref="CleanUp"/> (which zeroes them), THEN CleanUp, THEN
|
||
/// StopCompletely, THEN hand off to <see cref="StickTo"/> (R5
|
||
/// StickyManager seam) with the pre-CleanUp values. Non-sticky empty
|
||
/// queue: CleanUp + StopCompletely only.
|
||
/// </summary>
|
||
public void BeginNextNode()
|
||
{
|
||
if (_pendingActions.First is not null)
|
||
{
|
||
MovementType type = _pendingActions.First.Value.Type;
|
||
if (type == MovementType.MoveToPosition)
|
||
{
|
||
BeginMoveForward();
|
||
return;
|
||
}
|
||
if (type == MovementType.TurnToHeading)
|
||
{
|
||
BeginTurnToHeading();
|
||
return;
|
||
}
|
||
return; // unknown node type: stall (defensive)
|
||
}
|
||
|
||
if (Params.Sticky)
|
||
{
|
||
float height = SoughtObjectHeight;
|
||
float radius = SoughtObjectRadius;
|
||
uint tlid = TopLevelObjectId;
|
||
|
||
CleanUp();
|
||
if (HasPhysicsObj) _stopCompletely();
|
||
|
||
StickTo?.Invoke(tlid, radius, height);
|
||
// CLIENT ADDITION (see MoveToComplete doc): natural completion.
|
||
// Reentrancy-safe: CleanUp reset movement_type to Invalid BEFORE
|
||
// the stop, so the stop's interrupt→CancelMoveTo chain no-oped.
|
||
MoveToComplete?.Invoke(WeenieError.None);
|
||
return;
|
||
}
|
||
|
||
CleanUp();
|
||
if (HasPhysicsObj) _stopCompletely();
|
||
// CLIENT ADDITION (see MoveToComplete doc): natural completion.
|
||
MoveToComplete?.Invoke(WeenieError.None);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::BeginMoveForward</c> (<c>00529a00</c>, raw
|
||
/// 306957-307042). VERBATIM per decomp §4c: no physics_obj →
|
||
/// <see cref="CancelMoveTo"/>(<see cref="WeenieError.NoPhysicsObject"/>).
|
||
/// Compute distance + heading-diff-to-target (same epsilon
|
||
/// snap/normalize as <see cref="MoveToPosition"/>), then
|
||
/// <see cref="MovementParameters.GetCommand"/>. If no motion is needed
|
||
/// (already in range) — pop the head node and
|
||
/// <see cref="BeginNextNode"/>. Otherwise build a fresh LOCAL params
|
||
/// (defaults; <see cref="MovementParameters.CancelMoveTo"/> CLEARED so
|
||
/// <c>_DoMotion</c> doesn't cancel THIS moveto; speed copied from
|
||
/// <see cref="Params"/>), issue via <see cref="_DoMotion"/> — on error,
|
||
/// <see cref="CancelMoveTo"/> with that error and return. On success,
|
||
/// record <see cref="CurrentCommand"/>/<see cref="MovingAway"/>, write
|
||
/// the CHOSEN hold key BACK to the stored <see cref="Params"/>
|
||
/// (<c>HoldKeyToApply</c>), and seed BOTH progress-clock pairs
|
||
/// (<see cref="PreviousDistance"/>/<see cref="OriginalDistance"/> = dist,
|
||
/// both times = now).
|
||
/// </summary>
|
||
public void BeginMoveForward()
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
float dist = GetCurrentDistance();
|
||
|
||
Vector3 myPos = _getPosition().Frame.Origin;
|
||
Vector3 targetPos = CurrentTargetPosition.Frame.Origin;
|
||
float heading = MoveToMath.PositionHeading(myPos, targetPos) - _getHeading();
|
||
if (MathF.Abs(heading) < MoveToMath.Epsilon) heading = 0f;
|
||
if (heading < -MoveToMath.Epsilon) heading += 360f;
|
||
|
||
Params.GetCommand(dist, heading, out uint cmd, out HoldKey holdKey, out bool movingAway);
|
||
|
||
if (cmd == 0)
|
||
{
|
||
RemovePendingActionsHead();
|
||
BeginNextNode();
|
||
return;
|
||
}
|
||
|
||
var localParams = new MovementParameters
|
||
{
|
||
HoldKeyToApply = holdKey,
|
||
CancelMoveTo = false, // bitfield &= 0xffff7fff
|
||
Speed = Params.Speed,
|
||
};
|
||
|
||
WeenieError err = _DoMotion(cmd, localParams);
|
||
if (err != WeenieError.None)
|
||
{
|
||
CancelMoveTo(err);
|
||
return;
|
||
}
|
||
|
||
CurrentCommand = cmd;
|
||
MovingAway = movingAway;
|
||
Params.HoldKeyToApply = holdKey;
|
||
PreviousDistance = dist;
|
||
PreviousDistanceTime = _curTime();
|
||
OriginalDistance = dist;
|
||
OriginalDistanceTime = _curTime();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::BeginTurnToHeading</c> (<c>00529b90</c>, raw
|
||
/// 307046-307120). VERBATIM per decomp §4d: empty queue OR no
|
||
/// physics_obj → <see cref="CancelMoveTo"/>(NoPhysicsObject, per A10 —
|
||
/// NOT ACE's throw). If animations are still pending
|
||
/// (<see cref="MotionInterpreter.MotionsPending"/>), WAIT (return — do
|
||
/// not start the turn yet). Otherwise read the head node's heading, call
|
||
/// <see cref="MoveToMath.HeadingDiff"/> with the CONSTANT
|
||
/// <see cref="MotionCommand.TurnRight"/> (mirror explicitly disabled —
|
||
/// direction pick uses the raw ≤180 test, not the mirrored value):
|
||
/// <c>diff > 180</c> → check "already there" (<c>diff + eps >=
|
||
/// 360</c>) and pop+advance if so, else TurnLeft; <c>diff <= 180</c>
|
||
/// → check "already there" (<c>diff <= eps</c>) and pop+advance if
|
||
/// so, else TurnRight. Issue the turn via <see cref="_DoMotion"/> with a
|
||
/// fresh local params (CancelMoveTo cleared, speed + hold_key copied
|
||
/// from <see cref="Params"/>) — on error, <see cref="CancelMoveTo"/>. On
|
||
/// success, record <see cref="CurrentCommand"/> and store the REMAINING
|
||
/// DIFF (not a heading!) into <see cref="PreviousHeading"/> — THE QUIRK:
|
||
/// <see cref="HandleTurnToHeading"/>'s progress test then compares a
|
||
/// live HEADING against this DIFF-shaped seed on its first tick. Keep
|
||
/// verbatim (ACE matches: <c>PreviousHeading = diff</c>).
|
||
/// </summary>
|
||
public void BeginTurnToHeading()
|
||
{
|
||
if (_pendingActions.First is null || !HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
if (_interp.MotionsPending())
|
||
{
|
||
return;
|
||
}
|
||
|
||
float targetHeading = _pendingActions.First.Value.Heading;
|
||
float diff = MoveToMath.HeadingDiff(targetHeading, _getHeading(), MotionCommand.TurnRight);
|
||
|
||
uint turn;
|
||
if (diff > 180f)
|
||
{
|
||
if (diff + MoveToMath.Epsilon >= 360f)
|
||
{
|
||
RemovePendingActionsHead();
|
||
BeginNextNode();
|
||
return;
|
||
}
|
||
turn = MotionCommand.TurnLeft;
|
||
}
|
||
else
|
||
{
|
||
if (diff <= MoveToMath.Epsilon)
|
||
{
|
||
RemovePendingActionsHead();
|
||
BeginNextNode();
|
||
return;
|
||
}
|
||
turn = MotionCommand.TurnRight;
|
||
}
|
||
|
||
var localParams = new MovementParameters
|
||
{
|
||
CancelMoveTo = false,
|
||
Speed = Params.Speed,
|
||
HoldKeyToApply = Params.HoldKeyToApply,
|
||
};
|
||
|
||
WeenieError err = _DoMotion(turn, localParams);
|
||
if (err != WeenieError.None)
|
||
{
|
||
CancelMoveTo(err);
|
||
return;
|
||
}
|
||
|
||
CurrentCommand = turn;
|
||
PreviousHeading = diff; // NOTE: the remaining DIFF, not a heading — retail quirk, keep verbatim.
|
||
}
|
||
|
||
// ── 5. Distance / progress / command-selection helpers ─────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::GetCurrentDistance</c> (<c>005291b0</c>, raw
|
||
/// 306435-306460). VERBATIM per decomp §5a: no physics_obj → 0 (retail's
|
||
/// x87-garbled void return; modeled as 0 since every caller only uses
|
||
/// this when <see cref="HasPhysicsObj"/> is already known true). When
|
||
/// <see cref="MovementParameters.UseSpheres"/> (0x400) is NOT set → plain
|
||
/// center (Euclidean) distance to <see cref="CurrentTargetPosition"/>.
|
||
/// When SET → <see cref="MoveToMath.CylinderDistance"/> using the
|
||
/// mover's own radius/height (<see cref="_getOwnRadius"/>/
|
||
/// <see cref="_getOwnHeight"/>) vs the stored
|
||
/// <see cref="SoughtObjectRadius"/>/<see cref="SoughtObjectHeight"/> —
|
||
/// object moves (which set sought radius/height and get use_spheres on
|
||
/// the wire) use edge-to-edge distance; position moves use center
|
||
/// distance.
|
||
/// </summary>
|
||
public float GetCurrentDistance()
|
||
{
|
||
if (!HasPhysicsObj) return 0f;
|
||
|
||
Vector3 myPos = _getPosition().Frame.Origin;
|
||
Vector3 targetPos = CurrentTargetPosition.Frame.Origin;
|
||
|
||
if (!Params.UseSpheres)
|
||
{
|
||
return Vector3.Distance(myPos, targetPos);
|
||
}
|
||
|
||
return MoveToMath.CylinderDistance(
|
||
_getOwnRadius(), _getOwnHeight(), myPos,
|
||
SoughtObjectRadius, SoughtObjectHeight, targetPos);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::CheckProgressMade</c> (<c>005290f0</c>, raw
|
||
/// 306385-306431). VERBATIM per decomp §5b: evaluated only after a
|
||
/// 1-SECOND window since <see cref="PreviousDistanceTime"/> (before that,
|
||
/// returns true — "OK, too soon to judge"). Progress = distance closed
|
||
/// (or opened, when <see cref="MovingAway"/>) since the last checkpoint;
|
||
/// requires BOTH the incremental rate (since
|
||
/// <see cref="PreviousDistanceTime"/>) AND the overall rate (since
|
||
/// <see cref="OriginalDistanceTime"/>) to be ≥ 0.25 units/second. The
|
||
/// incremental checkpoint (<see cref="PreviousDistance"/>/
|
||
/// <see cref="PreviousDistanceTime"/>) only advances when the
|
||
/// incremental rate passes.
|
||
/// </summary>
|
||
public bool CheckProgressMade(float currentDistance)
|
||
{
|
||
double elapsed = _curTime() - PreviousDistanceTime;
|
||
if (elapsed <= 1.0) return true;
|
||
|
||
float progress = MovingAway
|
||
? currentDistance - PreviousDistance
|
||
: PreviousDistance - currentDistance;
|
||
|
||
if (progress / (float)elapsed >= 0.25f)
|
||
{
|
||
PreviousDistance = currentDistance;
|
||
PreviousDistanceTime = _curTime();
|
||
|
||
float total = MovingAway
|
||
? currentDistance - OriginalDistance
|
||
: OriginalDistance - currentDistance;
|
||
float totalRate = total / (float)(_curTime() - OriginalDistanceTime);
|
||
|
||
if (totalRate >= 0.25f) return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// ── 6. Per-tick drivers + target updates ────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::UseTime</c> (<c>0052a780</c>, raw
|
||
/// 307776-307798) — THE TICK GATE. VERBATIM per decomp §6a: three gates,
|
||
/// ALL must pass: (1) grounded — <see cref="_contact"/>() (CONTACT
|
||
/// transient-state bit; no moveto progress mid-air, <see cref="HitGround"/>
|
||
/// resumes on landing); (2) a pending node exists; (3) object-moves
|
||
/// (<see cref="TopLevelObjectId"/> != 0 AND
|
||
/// <see cref="MovementTypeState"/> != Invalid) must be
|
||
/// <see cref="Initialized"/> — i.e. the first target update has arrived.
|
||
/// Dispatches to <see cref="HandleMoveToPosition"/> (node type 7) or
|
||
/// <see cref="HandleTurnToHeading"/> (node type 9).
|
||
/// </summary>
|
||
public void UseTime()
|
||
{
|
||
if (!HasPhysicsObj || !_contact()) return;
|
||
|
||
if (_pendingActions.First is null) return;
|
||
|
||
bool objectMoveGate = TopLevelObjectId == 0 || MovementTypeState == MovementType.Invalid || Initialized;
|
||
if (!objectMoveGate) return;
|
||
|
||
MovementType type = _pendingActions.First.Value.Type;
|
||
if (type == MovementType.MoveToPosition)
|
||
{
|
||
HandleMoveToPosition();
|
||
}
|
||
else if (type == MovementType.TurnToHeading)
|
||
{
|
||
HandleTurnToHeading();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::HandleMoveToPosition</c> (<c>00529d80</c>,
|
||
/// raw 307187-307438) — the big per-tick driver. VERBATIM per decomp
|
||
/// §6b, three phases:
|
||
/// <list type="number">
|
||
/// <item><description><b>Phase 1 — aux turn steering</b> (only when NOT
|
||
/// animating, i.e. <see cref="MotionInterpreter.MotionsPending"/> is
|
||
/// false): compute the desired heading
|
||
/// (<see cref="MovementParameters.GetDesiredHeading"/> offset +
|
||
/// heading-to-target, wrapped [0,360)) minus current heading
|
||
/// (epsilon-snapped/normalized). If inside the [0,20]∪[340,360) deadband,
|
||
/// stop any active aux turn. Otherwise pick TurnLeft (diff ≥ 180) or
|
||
/// TurnRight, and issue it via <see cref="_DoMotion"/> ONLY if it differs
|
||
/// from the currently-running <see cref="AuxCommand"/> (no redundant
|
||
/// re-issue). While animating, stop any active aux turn instead (can't
|
||
/// steer mid-animation).</description></item>
|
||
/// <item><description><b>Phase 2 — arrival / progress</b>: compute
|
||
/// distance, run <see cref="CheckProgressMade"/>. On NO progress
|
||
/// (returns false): increment <see cref="FailProgressCount"/> (write-only,
|
||
/// §8) ONLY if neither interpolating nor animating (a stall is only
|
||
/// "real" when nothing else explains the lack of motion). On progress
|
||
/// (true): reset the fail counter, then test arrival —
|
||
/// <c>moving_away ? dist >= MinDistance : dist <= DistanceToObject</c>.
|
||
/// NOT arrived: check <see cref="MovementParameters.FailDistance"/> against
|
||
/// the distance traveled from <see cref="StartingPosition"/> —
|
||
/// <see cref="WeenieError.YouChargedTooFar"/> (0x3D) if exceeded. ARRIVED:
|
||
/// pop the head node, stop the current+aux motions, clear both command
|
||
/// fields, <see cref="BeginNextNode"/>.</description></item>
|
||
/// <item><description><b>Phase 3 — TargetManager quantum retune</b>
|
||
/// (object moves only, gated the SAME way as the UseTime object-move
|
||
/// gate): while chasing an object faster than 0.1 units/s, retune the
|
||
/// tracker's update quantum to the estimated time-to-arrival
|
||
/// (distance/speed) whenever it drifts ≥1 second from the current
|
||
/// quantum.</description></item>
|
||
/// </list>
|
||
/// <b>ACE-divergence trap (do-not-invent):</b> retail has NO
|
||
/// <c>set_heading</c> call anywhere in this method (ACE's "custom: sync
|
||
/// for server ticrate" addition is NOT ported).
|
||
/// </summary>
|
||
public void HandleMoveToPosition()
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
Vector3 curPos = _getPosition().Frame.Origin;
|
||
|
||
var localParams = new MovementParameters
|
||
{
|
||
Speed = Params.Speed,
|
||
CancelMoveTo = false,
|
||
HoldKeyToApply = Params.HoldKeyToApply,
|
||
};
|
||
|
||
// ---- PHASE 1: aux turn steering ----
|
||
if (_interp.MotionsPending())
|
||
{
|
||
if (AuxCommand != 0)
|
||
{
|
||
_StopMotion(AuxCommand, localParams);
|
||
AuxCommand = 0;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Vector3 targetPos = CurrentTargetPosition.Frame.Origin;
|
||
float toTargetHeading = MoveToMath.PositionHeading(curPos, targetPos);
|
||
float heading = Params.GetDesiredHeading(CurrentCommand, MovingAway) + toTargetHeading;
|
||
if (heading >= 360f) heading -= 360f;
|
||
|
||
float diff = heading - _getHeading();
|
||
if (MathF.Abs(diff) < MoveToMath.Epsilon) diff = 0f;
|
||
if (diff < -MoveToMath.Epsilon) diff += 360f;
|
||
|
||
if (diff <= 20f || diff >= 340f)
|
||
{
|
||
if (AuxCommand != 0)
|
||
{
|
||
_StopMotion(AuxCommand, localParams);
|
||
AuxCommand = 0;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
uint turn = diff >= 180f ? MotionCommand.TurnLeft : MotionCommand.TurnRight;
|
||
if (turn != AuxCommand)
|
||
{
|
||
_DoMotion(turn, localParams);
|
||
AuxCommand = turn;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- PHASE 2: arrival / progress ----
|
||
float dist = GetCurrentDistance();
|
||
if (!CheckProgressMade(dist))
|
||
{
|
||
if (!_isInterpolating() && !_interp.MotionsPending())
|
||
{
|
||
FailProgressCount += 1;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
FailProgressCount = 0;
|
||
|
||
bool arrived = MovingAway
|
||
? dist >= Params.MinDistance
|
||
: dist <= Params.DistanceToObject;
|
||
|
||
if (!arrived)
|
||
{
|
||
float startDist = Vector3.Distance(StartingPosition.Frame.Origin, _getPosition().Frame.Origin);
|
||
if (startDist > Params.FailDistance)
|
||
{
|
||
CancelMoveTo(WeenieError.YouChargedTooFar);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
RemovePendingActionsHead();
|
||
_StopMotion(CurrentCommand, localParams);
|
||
CurrentCommand = 0;
|
||
if (AuxCommand != 0)
|
||
{
|
||
_StopMotion(AuxCommand, localParams);
|
||
AuxCommand = 0;
|
||
}
|
||
BeginNextNode();
|
||
}
|
||
}
|
||
|
||
// ---- PHASE 3: TargetManager quantum retune (object moves only) ----
|
||
if (TopLevelObjectId != 0 && MovementTypeState != MovementType.Invalid)
|
||
{
|
||
Vector3 v = _getVelocity();
|
||
float speed = v.Length();
|
||
if (speed > 0.1)
|
||
{
|
||
float eta = dist / speed;
|
||
if (MathF.Abs(eta - (float)_getTargetQuantum()) >= 1.0f)
|
||
{
|
||
_setTargetQuantum(eta);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::HandleTurnToHeading</c> (<c>0052a0c0</c>, raw
|
||
/// 307442-307517). VERBATIM per decomp §6c: no physics_obj →
|
||
/// <see cref="CancelMoveTo"/>(NoPhysicsObject). If NOT currently turning
|
||
/// (<see cref="CurrentCommand"/> isn't TurnLeft/TurnRight) — re-enter
|
||
/// <see cref="BeginTurnToHeading"/> (arms the turn). Otherwise: if
|
||
/// <see cref="MoveToMath.HeadingGreater"/> says the current heading has
|
||
/// PASSED the node's target heading (direction-aware per
|
||
/// <see cref="CurrentCommand"/>) — reset the fail counter, SNAP the
|
||
/// heading to the node's exact value via <see cref="_setHeading"/>
|
||
/// (<c>send:true</c> — THE ONE heading snap in the whole family, decomp
|
||
/// §6c @0052a146), pop the node, stop the current motion, clear
|
||
/// <see cref="CurrentCommand"/>, <see cref="BeginNextNode"/>. Otherwise —
|
||
/// still turning: compute <see cref="MoveToMath.HeadingDiff"/> between
|
||
/// the LIVE current heading and <see cref="PreviousHeading"/> (the
|
||
/// quirk-seeded DIFF from <see cref="BeginTurnToHeading"/>), passing the
|
||
/// LIVE <see cref="CurrentCommand"/> (can be TurnLeft — the mirror DOES
|
||
/// apply here, P3). If the result is inside <c>(eps, 180)</c> — rotational
|
||
/// progress was made: reset fail counter, update
|
||
/// <see cref="PreviousHeading"/> to the LIVE heading. Otherwise — no
|
||
/// progress: update <see cref="PreviousHeading"/> anyway, then increment
|
||
/// <see cref="FailProgressCount"/> ONLY if neither interpolating nor
|
||
/// animating.
|
||
/// </summary>
|
||
public void HandleTurnToHeading()
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
uint cmd = CurrentCommand;
|
||
if (cmd != MotionCommand.TurnLeft && cmd != MotionCommand.TurnRight)
|
||
{
|
||
BeginTurnToHeading();
|
||
return;
|
||
}
|
||
|
||
MoveToNode head = _pendingActions.First!.Value;
|
||
float heading = _getHeading();
|
||
|
||
if (MoveToMath.HeadingGreater(heading, head.Heading, cmd))
|
||
{
|
||
FailProgressCount = 0;
|
||
_setHeading(head.Heading, true);
|
||
RemovePendingActionsHead();
|
||
|
||
var localParams = new MovementParameters
|
||
{
|
||
CancelMoveTo = false,
|
||
HoldKeyToApply = Params.HoldKeyToApply,
|
||
};
|
||
_StopMotion(CurrentCommand, localParams);
|
||
CurrentCommand = 0;
|
||
BeginNextNode();
|
||
return;
|
||
}
|
||
|
||
float diff = MoveToMath.HeadingDiff(heading, PreviousHeading, cmd);
|
||
if (diff < 180f && diff > MoveToMath.Epsilon)
|
||
{
|
||
FailProgressCount = 0;
|
||
PreviousHeading = heading;
|
||
return;
|
||
}
|
||
|
||
PreviousHeading = heading;
|
||
if (!_isInterpolating() && !_interp.MotionsPending())
|
||
{
|
||
FailProgressCount += 1;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::HandleUpdateTarget</c> (<c>0052a7d0</c>, raw
|
||
/// 307802-307867). VERBATIM per decomp §6d — the P4 TargetTracker feed
|
||
/// point: <paramref name="info"/> arrives at CONTEXT 0 only (the caller's
|
||
/// responsibility — <c>CPhysicsObj::HandleUpdateTarget</c> gates on
|
||
/// <c>context_id == 0</c> before relaying, decomp §9f; not re-checked
|
||
/// here). No physics_obj → <see cref="CancelMoveTo"/>(NoPhysicsObject).
|
||
/// Ignore updates for any target other than
|
||
/// <see cref="TopLevelObjectId"/>. TWO paths:
|
||
/// <list type="bullet">
|
||
/// <item><description><b>Deferred start</b> (<see cref="Initialized"/>
|
||
/// false — the FIRST callback): self-target → snapshot both positions to
|
||
/// the mover's own current position and
|
||
/// <see cref="CleanUpAndCallWeenie"/>(None) — instant success. Non-OK
|
||
/// status → <see cref="CancelMoveTo"/>(<see cref="WeenieError.NoObject"/>
|
||
/// 0x38 — the target never resolved). Otherwise build the node plan via
|
||
/// <see cref="MoveToObject_Internal"/> (MoveToObject) or
|
||
/// <see cref="TurnToObject_Internal"/> (TurnToObject).</description></item>
|
||
/// <item><description><b>Retarget while running</b>
|
||
/// (<see cref="Initialized"/> true): non-OK status →
|
||
/// <see cref="CancelMoveTo"/>(<see cref="WeenieError.ObjectGone"/> 0x37 —
|
||
/// was tracked, now gone). Otherwise, for MoveToObject ONLY: update
|
||
/// <see cref="SoughtPosition"/>=interpolated,
|
||
/// <see cref="CurrentTargetPosition"/>=target, and RESET both
|
||
/// progress-clock pairs to FLT_MAX/now. Does NOT requeue nodes — the
|
||
/// running MoveToPosition node keeps steering toward the moved
|
||
/// <see cref="CurrentTargetPosition"/> each tick (Phase 1 of
|
||
/// <see cref="HandleMoveToPosition"/>). TurnToObject gets NO retarget
|
||
/// handling (heading was frozen at the initial
|
||
/// callback).</description></item>
|
||
/// </list>
|
||
/// </summary>
|
||
public void HandleUpdateTarget(TargetInfo info)
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
if (TopLevelObjectId != info.ObjectId) return;
|
||
|
||
if (!Initialized)
|
||
{
|
||
if (TopLevelObjectId == _getSelfId())
|
||
{
|
||
Position selfPos = _getPosition();
|
||
SoughtPosition = selfPos;
|
||
CurrentTargetPosition = selfPos;
|
||
CleanUpAndCallWeenie(WeenieError.None);
|
||
return;
|
||
}
|
||
|
||
if (info.Status != TargetStatus.Ok)
|
||
{
|
||
CancelMoveTo(WeenieError.NoObject);
|
||
return;
|
||
}
|
||
|
||
if (MovementTypeState == MovementType.MoveToObject)
|
||
{
|
||
MoveToObject_Internal(info.TargetPosition, info.InterpolatedPosition);
|
||
}
|
||
else if (MovementTypeState == MovementType.TurnToObject)
|
||
{
|
||
TurnToObject_Internal(info.TargetPosition);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (info.Status != TargetStatus.Ok)
|
||
{
|
||
CancelMoveTo(WeenieError.ObjectGone);
|
||
return;
|
||
}
|
||
|
||
if (MovementTypeState == MovementType.MoveToObject)
|
||
{
|
||
SoughtPosition = info.InterpolatedPosition;
|
||
CurrentTargetPosition = info.TargetPosition;
|
||
PreviousDistance = float.MaxValue;
|
||
PreviousDistanceTime = _curTime();
|
||
OriginalDistance = float.MaxValue;
|
||
OriginalDistanceTime = _curTime();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::HitGround</c> (<c>00529d70</c>, raw
|
||
/// 307175-307183): no-op when <see cref="MovementTypeState"/> is Invalid;
|
||
/// otherwise tailcall <see cref="BeginNextNode"/> — re-arm the moveto
|
||
/// state machine after landing (mirrors the UseTime CONTACT gate, which
|
||
/// blocks all progress while airborne).
|
||
/// </summary>
|
||
public void HitGround()
|
||
{
|
||
if (MovementTypeState == MovementType.Invalid) return;
|
||
BeginNextNode();
|
||
}
|
||
|
||
// ── 6f/6g. Deferred-start internals (first HandleUpdateTarget callback) ─
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::MoveToObject_Internal</c> (<c>0052a400</c>,
|
||
/// raw 307597-307663). VERBATIM per decomp §6f: no physics_obj →
|
||
/// <see cref="CancelMoveTo"/>(NoPhysicsObject). Snapshot
|
||
/// <see cref="SoughtPosition"/>=<paramref name="interpolated"/>,
|
||
/// <see cref="CurrentTargetPosition"/>=<paramref name="target"/>. Compute
|
||
/// heading toward the INTERPOLATED position (not the raw target — the
|
||
/// smoother tracking point) minus current heading, epsilon-snapped/
|
||
/// normalized. <see cref="MovementParameters.GetCommand"/> against
|
||
/// <see cref="GetCurrentDistance"/> (now valid — CurrentTargetPosition is
|
||
/// set) — if motion is needed, queue
|
||
/// <c>[TurnToHeading(face interpolated)] → [MoveToPosition]</c> (SAME
|
||
/// node plan shape as <see cref="MoveToPosition"/>, but aimed at the
|
||
/// interpolated point). If <see cref="MovementParameters.UseFinalHeading"/>
|
||
/// — queue a FINAL turn to <c>heading-to-interpolated +
|
||
/// DesiredHeading</c> (RELATIVE, unlike <see cref="MoveToPosition"/>'s
|
||
/// absolute final heading). Set <see cref="Initialized"/>=true, then
|
||
/// <see cref="BeginNextNode"/>.
|
||
/// </summary>
|
||
public void MoveToObject_Internal(Position target, Position interpolated)
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
SoughtPosition = interpolated;
|
||
CurrentTargetPosition = target;
|
||
|
||
Vector3 myPos = _getPosition().Frame.Origin;
|
||
Vector3 interpPos = interpolated.Frame.Origin;
|
||
float iHeading = MoveToMath.PositionHeading(myPos, interpPos);
|
||
|
||
float dist = GetCurrentDistance();
|
||
|
||
float diff = iHeading - _getHeading();
|
||
if (MathF.Abs(diff) < MoveToMath.Epsilon) diff = 0f;
|
||
if (diff < -MoveToMath.Epsilon) diff += 360f;
|
||
|
||
Params.GetCommand(dist, diff, out uint cmd, out _, out _);
|
||
if (cmd != 0)
|
||
{
|
||
AddTurnToHeadingNode(iHeading);
|
||
AddMoveToPositionNode();
|
||
}
|
||
|
||
if (Params.UseFinalHeading)
|
||
{
|
||
float final = iHeading + Params.DesiredHeading;
|
||
if (final >= 360f) final -= 360f;
|
||
AddTurnToHeadingNode(final);
|
||
}
|
||
|
||
Initialized = true;
|
||
BeginNextNode();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::TurnToObject_Internal</c> (<c>0052a550</c>,
|
||
/// raw 307667-307702). VERBATIM per decomp §6g: no physics_obj →
|
||
/// <see cref="CancelMoveTo"/>(NoPhysicsObject). Snapshot
|
||
/// <see cref="CurrentTargetPosition"/>=<paramref name="target"/>. Read
|
||
/// <see cref="SoughtPosition"/>'s CURRENT heading (with §3d's quirk, this
|
||
/// is 0 for a fresh manager — the desired-heading write TurnToObject
|
||
/// seeded landed on <see cref="CurrentTargetPosition"/> instead and was
|
||
/// overwritten just above). Compute heading-to-target, add the sought
|
||
/// heading, <c>fmod 360</c> — the final heading. Write it back into
|
||
/// <see cref="SoughtPosition"/>'s frame, queue ONE TurnToHeading node
|
||
/// (inlined <c>AddTurnToHeadingNode</c> shape), set
|
||
/// <see cref="Initialized"/>=true, <see cref="BeginNextNode"/>. With the
|
||
/// §3d quirk, the effective final heading is simply "face the object" —
|
||
/// ACE matches verbatim (MoveToManager.cs:246 + 271-276).
|
||
/// </summary>
|
||
public void TurnToObject_Internal(Position target)
|
||
{
|
||
if (!HasPhysicsObj)
|
||
{
|
||
CancelMoveTo(WeenieError.NoPhysicsObject);
|
||
return;
|
||
}
|
||
|
||
CurrentTargetPosition = target;
|
||
|
||
float soughtHeading = MoveToMath.GetHeading(SoughtPosition.Frame.Orientation);
|
||
|
||
Vector3 myPos = _getPosition().Frame.Origin;
|
||
Vector3 targetPos = CurrentTargetPosition.Frame.Origin;
|
||
float targetHeading = MoveToMath.PositionHeading(myPos, targetPos);
|
||
|
||
float final = (targetHeading + soughtHeading) % 360f;
|
||
if (final < 0f) final += 360f;
|
||
|
||
SoughtPosition = SoughtPosition with
|
||
{
|
||
Frame = new CellFrame(SoughtPosition.Frame.Origin, MoveToMath.SetHeading(SoughtPosition.Frame.Orientation, final)),
|
||
};
|
||
|
||
_pendingActions.AddLast(new MoveToNode(MovementType.TurnToHeading, final));
|
||
|
||
Initialized = true;
|
||
BeginNextNode();
|
||
}
|
||
|
||
// ── 7. Motion issuing + cleanup family ──────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::_DoMotion</c> (<c>00529010</c>, raw
|
||
/// 306351-306364). VERBATIM per decomp §7a: no physics_obj → 0x08
|
||
/// (NoPhysicsObject); no motion interpreter → 0x0B
|
||
/// (NoMotionInterpreter — modeled as always-present since
|
||
/// <see cref="_interp"/> is a required ctor field, so this branch is
|
||
/// unreachable in this port; kept in the doc for parity). Calls
|
||
/// <see cref="MotionInterpreter.adjust_motion"/> EXPLICITLY (mutates
|
||
/// <paramref name="motion"/>/<paramref name="p"/>.Speed in place — e.g.
|
||
/// promotes WalkForward+HoldKey.Run → RunForward×runRate) THEN tailcalls
|
||
/// <see cref="MotionInterpreter.DoInterpretedMotion(uint,MovementParameters)"/>
|
||
/// with the ADJUSTED motion id. THIS IS THE ENTIRE CMotionInterp SEAM —
|
||
/// MoveToManager never calls the raw-command <c>DoMotion</c>,
|
||
/// <c>set_hold_run</c>, or any raw-state API directly.
|
||
/// </summary>
|
||
private WeenieError _DoMotion(uint motion, MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return WeenieError.NoPhysicsObject;
|
||
|
||
float speed = p.Speed;
|
||
_interp.adjust_motion(ref motion, ref speed, p.HoldKeyToApply);
|
||
p.Speed = speed;
|
||
|
||
return _interp.DoInterpretedMotion(motion, p);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::_StopMotion</c> (<c>00529080</c>, raw
|
||
/// 306368-306381). Identical shape to <see cref="_DoMotion"/>: calls
|
||
/// <see cref="MotionInterpreter.adjust_motion"/> then tailcalls
|
||
/// <see cref="MotionInterpreter.StopInterpretedMotion(uint,MovementParameters)"/>
|
||
/// with the ADJUSTED motion id.
|
||
/// </summary>
|
||
private WeenieError _StopMotion(uint motion, MovementParameters p)
|
||
{
|
||
if (!HasPhysicsObj) return WeenieError.NoPhysicsObject;
|
||
|
||
float speed = p.Speed;
|
||
_interp.adjust_motion(ref motion, ref speed, p.HoldKeyToApply);
|
||
p.Speed = speed;
|
||
|
||
return _interp.StopInterpretedMotion(motion, p);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::CancelMoveTo</c> (<c>00529930</c>, raw
|
||
/// 306886-306940). VERBATIM per decomp §7c: no-op when
|
||
/// <see cref="MovementTypeState"/> is already Invalid (reentrancy guard —
|
||
/// see the class doc's reentrancy invariant). Otherwise: drain
|
||
/// <see cref="_pendingActions"/>, <see cref="CleanUp"/>, then
|
||
/// StopCompletely. The <paramref name="error"/> argument is NEVER READ
|
||
/// in retail's body (every call site's error code is dropped in this
|
||
/// build, decomp §7c) — kept for parity/logging/tests only; NO behavior
|
||
/// depends on its value.
|
||
/// </summary>
|
||
public void CancelMoveTo(WeenieError error)
|
||
{
|
||
_ = error; // retail: never read in the body (decomp §7c) — kept for parity/logging.
|
||
|
||
if (MovementTypeState == MovementType.Invalid) return;
|
||
|
||
_pendingActions.Clear();
|
||
CleanUp();
|
||
if (HasPhysicsObj) _stopCompletely();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::CleanUp</c> (<c>005295c0</c>, raw
|
||
/// 306710-306736). VERBATIM per decomp §7d: build a local params
|
||
/// (hold_key copied from <see cref="Params"/>, CancelMoveTo cleared),
|
||
/// then if a physics_obj exists: stop the current command (if any), stop
|
||
/// the aux command (if any), and — if this was an object move
|
||
/// (<see cref="TopLevelObjectId"/> != 0 AND
|
||
/// <see cref="MovementTypeState"/> != Invalid) — <see cref="_clearTarget"/>()
|
||
/// (the P4 TargetTracker unsubscribe). Finally
|
||
/// <see cref="InitializeLocalVariables"/>. Does NOT drain
|
||
/// <see cref="_pendingActions"/> — <see cref="CancelMoveTo"/>/
|
||
/// <see cref="Destroy"/> do that first.
|
||
/// </summary>
|
||
public void CleanUp()
|
||
{
|
||
var localParams = new MovementParameters
|
||
{
|
||
HoldKeyToApply = Params.HoldKeyToApply,
|
||
CancelMoveTo = false,
|
||
};
|
||
|
||
if (HasPhysicsObj)
|
||
{
|
||
if (CurrentCommand != 0) _StopMotion(CurrentCommand, localParams);
|
||
if (AuxCommand != 0) _StopMotion(AuxCommand, localParams);
|
||
if (TopLevelObjectId != 0 && MovementTypeState != MovementType.Invalid)
|
||
{
|
||
_clearTarget();
|
||
}
|
||
}
|
||
|
||
InitializeLocalVariables();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>MoveToManager::CleanUpAndCallWeenie</c> (<c>00529650</c>,
|
||
/// raw 306740-306752). Despite the name, contains NO weenie call in this
|
||
/// build (decomp §7e — the compiled-out server-side callback; body ≡
|
||
/// <see cref="CleanUp"/> + StopCompletely). <see cref="MoveToComplete"/>
|
||
/// is a documented CLIENT ADDITION (see its doc) firing here and at
|
||
/// <see cref="BeginNextNode"/>'s empty-queue completion, standing in for
|
||
/// ACE's server-side <c>OnMoveComplete</c> — do NOT present it as retail
|
||
/// behavior.
|
||
/// </summary>
|
||
public void CleanUpAndCallWeenie(WeenieError error)
|
||
{
|
||
CleanUp();
|
||
if (HasPhysicsObj) _stopCompletely();
|
||
MoveToComplete?.Invoke(error);
|
||
}
|
||
|
||
// ── internal helper: the 10-field MovementParameters copy every entry
|
||
// point performs (§3b/§3c/§3d/§3e all do this identically) ──────────
|
||
|
||
private void CopyParams(MovementParameters p)
|
||
{
|
||
Params.CanWalk = p.CanWalk;
|
||
Params.CanRun = p.CanRun;
|
||
Params.CanSidestep = p.CanSidestep;
|
||
Params.CanWalkBackwards = p.CanWalkBackwards;
|
||
Params.CanCharge = p.CanCharge;
|
||
Params.FailWalk = p.FailWalk;
|
||
Params.UseFinalHeading = p.UseFinalHeading;
|
||
Params.Sticky = p.Sticky;
|
||
Params.MoveAway = p.MoveAway;
|
||
Params.MoveTowards = p.MoveTowards;
|
||
Params.UseSpheres = p.UseSpheres;
|
||
Params.SetHoldKey = p.SetHoldKey;
|
||
Params.Autonomous = p.Autonomous;
|
||
Params.ModifyRawState = p.ModifyRawState;
|
||
Params.ModifyInterpretedState = p.ModifyInterpretedState;
|
||
Params.CancelMoveTo = p.CancelMoveTo;
|
||
Params.StopCompletelyFlag = p.StopCompletelyFlag;
|
||
Params.DisableJumpDuringLink = p.DisableJumpDuringLink;
|
||
|
||
Params.DistanceToObject = p.DistanceToObject;
|
||
Params.MinDistance = p.MinDistance;
|
||
Params.DesiredHeading = p.DesiredHeading;
|
||
Params.Speed = p.Speed;
|
||
Params.FailDistance = p.FailDistance;
|
||
Params.WalkRunThreshhold = p.WalkRunThreshhold;
|
||
Params.ContextId = p.ContextId;
|
||
Params.HoldKeyToApply = p.HoldKeyToApply;
|
||
Params.ActionStamp = p.ActionStamp;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail <c>TargetInfo</c> (acclient.h:31591, struct #3482) — the callback
|
||
/// payload <see cref="MoveToManager.HandleUpdateTarget"/> consumes AND the wire
|
||
/// record the R5 <see cref="TargetManager"/> voyeur system exchanges between
|
||
/// hosts (<c>SendVoyeurUpdate</c> → <c>receive_target_update</c> →
|
||
/// <see cref="TargetManager.ReceiveUpdate"/>).
|
||
///
|
||
/// <para>R5 EXTENDED this from the R4 4-field callback shape to the full retail
|
||
/// 10-field struct. The extra fields (<see cref="ContextId"/>,
|
||
/// <see cref="Radius"/>, <see cref="Quantum"/>,
|
||
/// <see cref="InterpolatedHeading"/>, <see cref="Velocity"/>,
|
||
/// <see cref="LastUpdateTime"/>) default to zero, so the existing 4-argument
|
||
/// <c>new TargetInfo(id, status, tp, ip)</c> call sites (MoveToManager tests,
|
||
/// the AP-79 adapter pre-V2) still compile unchanged.
|
||
/// <see cref="MoveToManager.HandleUpdateTarget"/> only reads
|
||
/// <see cref="ObjectId"/>/<see cref="Status"/>/<see cref="InterpolatedPosition"/>;
|
||
/// the extra fields are consumed by the voyeur system.</para>
|
||
/// </summary>
|
||
/// <param name="ObjectId">Retail <c>object_id</c> — matched against
|
||
/// <see cref="MoveToManager.TopLevelObjectId"/>; a mismatch is silently
|
||
/// ignored (decomp §6d).</param>
|
||
/// <param name="Status">Retail <c>status</c> — see <see cref="TargetStatus"/>.</param>
|
||
/// <param name="TargetPosition">Retail <c>target_position</c> — the raw
|
||
/// (possibly jittery) target position.</param>
|
||
/// <param name="InterpolatedPosition">Retail <c>interpolated_position</c> —
|
||
/// the smoothed tracking point <see cref="MoveToManager.MoveToObject_Internal"/>
|
||
/// steers toward.</param>
|
||
/// <param name="ContextId">Retail <c>context_id</c> — the tracking context
|
||
/// (0 = the movement context; <c>CPhysicsObj::HandleUpdateTarget</c> only fans
|
||
/// out context 0).</param>
|
||
/// <param name="Radius">Retail <c>radius</c> — the voyeur's send-on-move
|
||
/// threshold (game units).</param>
|
||
/// <param name="Quantum">Retail <c>quantum</c> — the dead-reckoning lookahead
|
||
/// horizon (seconds) for <c>GetInterpolatedPosition</c>.</param>
|
||
/// <param name="InterpolatedHeading">Retail <c>interpolated_heading</c> —
|
||
/// normalized self→target direction (falls back to +Z when degenerate).</param>
|
||
/// <param name="Velocity">Retail <c>velocity</c> — the target's velocity at
|
||
/// send time.</param>
|
||
/// <param name="LastUpdateTime">Retail <c>last_update_time</c> — receipt
|
||
/// timestamp (drives the 10 s staleness timeout).</param>
|
||
public readonly record struct TargetInfo(
|
||
uint ObjectId,
|
||
TargetStatus Status,
|
||
Position TargetPosition,
|
||
Position InterpolatedPosition,
|
||
uint ContextId = 0,
|
||
float Radius = 0f,
|
||
double Quantum = 0.0,
|
||
Vector3 InterpolatedHeading = default,
|
||
Vector3 Velocity = default,
|
||
double LastUpdateTime = 0.0);
|
||
|
||
/// <summary>
|
||
/// Retail <c>TargetStatus</c> (acclient.h:7264). Only <see cref="Ok"/> vs
|
||
/// non-<see cref="Ok"/> is behaviorally distinguished by
|
||
/// <see cref="MoveToManager.HandleUpdateTarget"/> in this build (decomp §6d)
|
||
/// — the other values are carried for parity with the P4 TargetTracker
|
||
/// contract (V0-pins.md §P4).
|
||
/// </summary>
|
||
public enum TargetStatus
|
||
{
|
||
/// <summary>0 — undefined/uninitialized.</summary>
|
||
Undefined = 0,
|
||
/// <summary>1 — target resolved and tracked normally.</summary>
|
||
Ok = 1,
|
||
/// <summary>2 — target left the world (despawned).</summary>
|
||
ExitWorld = 2,
|
||
/// <summary>3 — target teleported.</summary>
|
||
Teleported = 3,
|
||
/// <summary>4 — target became contained (picked up / stowed).</summary>
|
||
Contained = 4,
|
||
/// <summary>5 — target became parented (e.g. mounted/wielded).</summary>
|
||
Parented = 5,
|
||
/// <summary>6 — tracker timed out without an update.</summary>
|
||
TimedOut = 6,
|
||
}
|