using AcDream.Core.Physics;
namespace AcDream.Core.Physics.Motion;
///
/// R3-W1 — verbatim port of retail's MovementParameters
/// (acclient.h:31453, struct #3460; bitfield struct
/// acclient.h:31423-31443):
///
/// struct __cppobj MovementParameters : PackObj
/// {
/// union { unsigned int bitfield; ... } ___u1;
/// float distance_to_object;
/// float min_distance;
/// float desired_heading;
/// float speed;
/// float fail_distance;
/// float walk_run_threshhold;
/// unsigned int context_id;
/// HoldKey hold_key_to_apply;
/// unsigned int action_stamp;
/// };
///
///
///
/// The bitfield's absolute mask table is pinned in
/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A4 (bit-for-bit
/// identical to ACE's MovementParamFlags):
///
///
/// - 0x1CanWalk
/// - 0x2CanRun
/// - 0x4CanSidestep
/// - 0x8CanWalkBackwards
/// - 0x10CanCharge
/// - 0x20FailWalk
/// - 0x40UseFinalHeading
/// - 0x80Sticky
/// - 0x100MoveAway
/// - 0x200MoveTowards
/// - 0x400UseSpheres
/// - 0x800SetHoldKey
/// - 0x1000Autonomous
/// - 0x2000ModifyRawState
/// - 0x4000ModifyInterpretedState
/// - 0x8000CancelMoveTo
/// - 0x10000StopCompletely
/// - 0x20000DisableJumpDuringLink
///
///
///
/// Ctor default (raw 300510-300534, 0x00524380):
/// (bitfield & 0xfffdee0f) | 0x1ee0f → 0x1EE0F sets
/// {CanWalk, CanRun, CanSidestep, CanWalkBackwards, MoveTowards, UseSpheres,
/// SetHoldKey, ModifyRawState, ModifyInterpretedState, CancelMoveTo,
/// StopCompletely}; clears {CanCharge, FailWalk, UseFinalHeading, Sticky,
/// MoveAway, Autonomous, DisableJumpDuringLink}.
/// Scalars: min_distance=0, distance_to_object=0.6,
/// fail_distance=FLT_MAX, desired_heading=0, speed=1,
/// walk_run_threshhold=15 (NOT ACE's 1.0 — W0-pins A4 divergence trap),
/// context_id=0, hold_key_to_apply=HoldKey.Invalid,
/// action_stamp=0.
///
///
///
/// ACE-divergence traps (W0-pins A4, do not copy): ACE's
/// MovementParameters ctor sets CanCharge = true
/// (MovementParameters.cs:58) — retail's default has bit 0x10 CLEAR;
/// this port defaults CanCharge = false. ACE also changed
/// Default_WalkRunThreshold to 1.0 (L50) vs retail's literal 15.0
/// (@300519) — this port defaults WalkRunThreshhold = 15f.
///
///
///
/// Named bool properties per plan (no ToBitfield()/FromBitfield()
/// pair — the wire never carries this struct raw; RawMotionState::Pack
/// serializes the STATE, not this transient parameter block. If a future
/// slice needs the packed form, add the pair then with a cited call site).
///
///
public sealed class MovementParameters
{
// ── bitfield flags (retail 0x1EE0F default; W0-pins A4) ───────────────
/// Mask 0x1 — default true.
public bool CanWalk { get; set; } = true;
/// Mask 0x2 — default true.
public bool CanRun { get; set; } = true;
/// Mask 0x4 — default true.
public bool CanSidestep { get; set; } = true;
/// Mask 0x8 — default true.
public bool CanWalkBackwards { get; set; } = true;
///
/// Mask 0x10 — default FALSE. ACE-divergence trap (W0-pins A4): ACE's
/// ctor sets this true (MovementParameters.cs:58); retail's 0x1EE0F
/// default has bit 0x10 CLEAR. Do not "fix" this to true.
///
public bool CanCharge { get; set; }
/// Mask 0x20 — default false.
public bool FailWalk { get; set; }
/// Mask 0x40 — default false.
public bool UseFinalHeading { get; set; }
/// Mask 0x80 — default false.
public bool Sticky { get; set; }
/// Mask 0x100 — default false.
public bool MoveAway { get; set; }
/// Mask 0x200 — default true.
public bool MoveTowards { get; set; } = true;
/// Mask 0x400 — default true.
public bool UseSpheres { get; set; } = true;
/// Mask 0x800 — default true. DoMotion @306188: byte1&8
/// requests a SetHoldKey call before adjust_motion.
public bool SetHoldKey { get; set; } = true;
///
/// Mask 0x1000 — default FALSE. Not the same virtual as
/// IWeenieObject.IsThePlayer (W0-pins A3) — this is the
/// per-call "was this an autonomous (locally-predicted) action?" flag.
///
public bool Autonomous { get; set; }
/// Mask 0x2000 — default true. DoMotion @306213: byte1&0x20
/// mirrors the applied motion into RawMotionState via
/// ApplyMotion/RemoveMotion.
public bool ModifyRawState { get; set; } = true;
/// Mask 0x4000 — default true. Mirrors into
/// InterpretedMotionState.
public bool ModifyInterpretedState { get; set; } = true;
/// Mask 0x8000 — default true. Bitfield high-byte sign bit;
/// DoMotion/StopMotion @306183/@305684: triggers
/// interrupt_current_movement before the rest of the call.
public bool CancelMoveTo { get; set; } = true;
/// Mask 0x10000 — default true.
public bool StopCompletelyFlag { get; set; } = true;
///
/// Mask 0x20000 — default FALSE. DoInterpretedMotion @305597: when set,
/// forces the computed jump_error_code to 0x48 (A1: jump
/// BLOCKED by motion/position) regardless of what
/// motion_allows_jump would have said.
///
public bool DisableJumpDuringLink { get; set; }
// ── scalar fields (retail ctor 0x00524380 defaults) ───────────────────
/// Retail default 0.6.
public float DistanceToObject { get; set; } = 0.6f;
/// Retail default 0.
public float MinDistance { get; set; }
/// Retail default 0.
public float DesiredHeading { get; set; }
/// Retail default 1.
public float Speed { get; set; } = 1f;
/// Retail default FLT_MAX.
public float FailDistance { get; set; } = float.MaxValue;
///
/// Retail default 15.0 (@300519). ACE-divergence trap (W0-pins A4): ACE
/// changed Default_WalkRunThreshold to 1.0 — do not copy.
///
public float WalkRunThreshhold { get; set; } = 15f;
/// Retail default 0.
public uint ContextId { get; set; }
/// Retail default .
public HoldKey HoldKeyToApply { get; set; } = HoldKey.Invalid;
/// Retail default 0.
public uint ActionStamp { get; set; }
// ── R4-V1: command-selection family (closes M2-mechanics) ─────────────
///
/// Retail MovementParameters::get_command (0x0052aa00,
/// raw 307946-308012), VERBATIM per
/// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §5c. Picks the
/// motion command + moving-away flag from the towards/away bitfield
/// combination, THEN the walk-vs-run cascade.
///
///
/// Command pick (mirrors towards_and_away's bands but is
/// NOT identical — see the asymmetry note on ):
///
/// - MoveTowards && MoveAway → delegate
/// to (the three-band form).
/// - MoveTowards only (or neither flag set —
/// retail's else if falls through to the SAME branch): plain
/// towards — dist > DistanceToObject → WalkForward,
/// !movingAway; else idle (cmd 0).
/// - MoveAway only: pure away —
/// dist < MinDistance → WalkForward, movingAway=true (the
/// heading flips +180 via — turn-around,
/// NOT WalkBackwards, unlike 's min-band);
/// else idle.
///
///
///
///
/// THE walk-vs-run rule (confirms
/// feedback_autowalk_cancharge_bit — port RETAIL's version of
/// BOTH the fast-path ACE dropped and the threshold-close-walk pair):
/// HoldKey.Run ⇐ CanCharge set (the fast-path — wins
/// regardless of CanRun/CanWalk/distance), OR (CanRun set AND
/// (CanWalk clear OR dist - DistanceToObject >
/// WalkRunThreshhold)). HoldKey.None (walk) ⇐ no CanRun, or
/// walk-capable within the threshold (INCLUSIVE ≤ — the raw's
/// test ah,0x41 after the fcom is the not-greater-than reading,
/// §5c @308003).
///
///
/// Current distance-to-target (retail's
/// GetCurrentDistance result — center or cylinder distance per
/// ).
/// Heading-to-target minus current heading,
/// normalized [0,360) — UNUSED by get_command itself (the raw
/// signature carries it for parity with the caller's local; retail's
/// body never reads arg3 in this build). Kept as a parameter for
/// call-site symmetry with BeginMoveForward (§4c), which computes
/// it immediately before calling get_command.
/// Chosen motion command id, or 0 if no movement
/// is needed (already in range).
/// Chosen hold key (walk vs run).
/// True if the chosen motion moves the mover
/// AWAY from the target (feeds and the
/// arrival predicate's polarity).
public void GetCommand(float dist, float headingDiff, out uint motion, out HoldKey holdKey, out bool movingAway)
{
_ = headingDiff; // retail's arg3 — unread in this build's body (§5c)
// ── command + moving_away pick ──────────────────────────────────
if (MoveTowards && MoveAway)
{
TowardsAndAway(dist, out motion, out movingAway);
}
else if (MoveAway && !MoveTowards)
{
// pure AWAY: dist < min_distance → WalkForward, moving away
// (turn-around; heading flips +180 via GetDesiredHeading).
if (dist < MinDistance)
{
motion = MotionCommand.WalkForward;
movingAway = true;
}
else
{
motion = 0u;
movingAway = false;
}
}
else
{
// plain TOWARDS (MoveTowards set, or neither flag set — retail's
// `else if ((flags & 0x100) == 0)` falls to the same label).
if (dist > DistanceToObject)
{
motion = MotionCommand.WalkForward;
movingAway = false;
}
else
{
motion = 0u;
movingAway = false;
}
}
// ── walk-vs-run HoldKey cascade ─────────────────────────────────
if (CanCharge)
{
// THE fast-path ACE dropped: can_charge short-circuits straight
// to Run regardless of CanRun/CanWalk/distance.
holdKey = HoldKey.Run;
return;
}
if (!CanRun)
{
holdKey = HoldKey.None;
return;
}
if (CanWalk && (dist - DistanceToObject) <= WalkRunThreshhold)
{
holdKey = HoldKey.None;
return;
}
holdKey = HoldKey.Run;
}
///
/// Retail MovementParameters::towards_and_away
/// (0x0052a9a0, raw 307917-307942), VERBATIM per
/// r4-moveto-decomp.md §5d. Three bands:
///
/// - dist > DistanceToObject → WalkForward,
/// towards (not moving away).
/// - dist - MinDistance < F_EPSILON (inside
/// the min-distance band) → WalkBackward, moving away. NOTE the
/// asymmetry vs 's pure-away branch: this backs
/// up with WalkBackwards (no turn-around), not WalkForward+heading-flip
/// (r4-moveto-decomp.md :656).
/// - otherwise (strictly inside [MinDistance,
/// DistanceToObject]) → idle (cmd 0).
///
///
/// Current distance-to-target.
/// Chosen motion command, or 0 if already in the dead
/// band.
/// True only for the WalkBackward (min-band)
/// case.
public void TowardsAndAway(float dist, out uint cmd, out bool movingAway)
{
const float epsilon = 0.000199999995f;
if (dist > DistanceToObject)
{
cmd = MotionCommand.WalkForward;
movingAway = false;
return;
}
if (dist - MinDistance < epsilon)
{
cmd = MotionCommand.WalkBackward;
movingAway = true;
return;
}
cmd = 0u;
movingAway = false;
}
///
/// Retail MovementParameters::get_desired_heading
/// (0x0052aad0), PINNED by direct Ghidra decompile of
/// patchmem.gpr (fetched live during V0 — see
/// docs/research/2026-07-03-r4-moveto/ghidra-confirmations.md §P2,
/// ACE-shaped constants CONFIRMED exact, superseding the earlier
/// BN-garble-based "high confidence" pin):
///
/// __thiscall get_desired_heading(command, movingAway)
/// {
/// if (command == RunForward || command == WalkForward) {
/// if (!movingAway) return 0.0f;
/// } else {
/// if (command != WalkBackward) return 0.0f;
/// if (movingAway) return 0.0f;
/// }
/// return 180.0f;
/// }
///
/// Truth table: forward/run + towards → 0°; forward/run + away → 180°;
/// backward + towards → 180°; backward + away → 0°; any other command
/// → 0°. This is the heading OFFSET added to the raw heading-to-target
/// so an "away" walk faces away and an "away" backstep faces the
/// target.
///
public float GetDesiredHeading(uint command, bool movingAway)
{
if (command == MotionCommand.RunForward || command == MotionCommand.WalkForward)
{
if (!movingAway) return 0f;
}
else
{
if (command != MotionCommand.WalkBackward) return 0f;
if (movingAway) return 0f;
}
return 180f;
}
// ── R4-V1: wire factory (closes M15/wire-exposure groundwork) ─────────
///
/// Factory for the retail MovementParameters::UnPackNet 7-dword
/// MoveTo wire form (0x0052ac50, 0x1c bytes, raw 308118-308205 —
/// r4-moveto-decomp.md §2g): bitfield, distance_to_object,
/// min_distance, fail_distance, speed, walk_run_threshhold,
/// desired_heading. Used by MoveToObject (type 6) and
/// MoveToPosition (type 7) wire payloads — the SAME field order
/// UpdateMotion.TryParseMoveToPayload already reads
/// (UpdateMotion.cs:328-341). The bitfield fully OVERWRITES every named
/// flag (UnPackNet does not merge with ctor defaults — every bit not
/// present in resolves to false); the wire
/// bitfield carries can_charge (0x10), the walk-vs-run answer
/// consumed by (cross-ref
/// feedback_autowalk_cancharge_bit).
///
public static MovementParameters FromWire(
uint bitfield,
float distanceToObject,
float minDistance,
float failDistance,
float speed,
float walkRunThreshhold,
float desiredHeading)
{
var p = new MovementParameters();
ApplyBitfield(p, bitfield);
p.DistanceToObject = distanceToObject;
p.MinDistance = minDistance;
p.FailDistance = failDistance;
p.Speed = speed;
p.WalkRunThreshhold = walkRunThreshhold;
p.DesiredHeading = desiredHeading;
return p;
}
///
/// Factory for the retail MovementParameters::UnPackNet 3-dword
/// TurnTo wire form (0xc bytes, r4-moveto-decomp.md §2g): bitfield,
/// speed, desired_heading. Used by TurnToObject (type 8) and
/// TurnToHeading (type 9) wire payloads. Distance-related scalars
/// (DistanceToObject/MinDistance/FailDistance/
/// WalkRunThreshhold) are NOT on this wire form and keep the
/// ctor defaults — retail's UnPackNet
/// for this form only ever writes the three fields named here.
///
public static MovementParameters FromWireTurnTo(
uint bitfield,
float speed,
float desiredHeading)
{
var p = new MovementParameters();
ApplyBitfield(p, bitfield);
p.Speed = speed;
p.DesiredHeading = desiredHeading;
return p;
}
///
/// Decode the A4-pinned bitfield masks onto the named bool properties.
/// Shared by / — the
/// wire bitfield always fully overwrites (retail's UnPackNet assigns
/// the raw dword straight into this->bitfield, no merge).
///
private static void ApplyBitfield(MovementParameters p, uint bitfield)
{
p.CanWalk = (bitfield & 0x1u) != 0;
p.CanRun = (bitfield & 0x2u) != 0;
p.CanSidestep = (bitfield & 0x4u) != 0;
p.CanWalkBackwards = (bitfield & 0x8u) != 0;
p.CanCharge = (bitfield & 0x10u) != 0;
p.FailWalk = (bitfield & 0x20u) != 0;
p.UseFinalHeading = (bitfield & 0x40u) != 0;
p.Sticky = (bitfield & 0x80u) != 0;
p.MoveAway = (bitfield & 0x100u) != 0;
p.MoveTowards = (bitfield & 0x200u) != 0;
p.UseSpheres = (bitfield & 0x400u) != 0;
p.SetHoldKey = (bitfield & 0x800u) != 0;
p.Autonomous = (bitfield & 0x1000u) != 0;
p.ModifyRawState = (bitfield & 0x2000u) != 0;
p.ModifyInterpretedState = (bitfield & 0x4000u) != 0;
p.CancelMoveTo = (bitfield & 0x8000u) != 0;
p.StopCompletelyFlag = (bitfield & 0x10000u) != 0;
p.DisableJumpDuringLink = (bitfield & 0x20000u) != 0;
}
}