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) | 0x1ee0f0x1EE0F 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.RunCanCharge 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; } }