using System; using System.Numerics; namespace AcDream.Core.Physics.Motion; /// /// R4-V1 — pure-math free functions consumed by the (future) MoveToManager /// port: heading_diff, heading_greater, Position::heading /// / Frame::get_heading / Frame::set_heading, and /// Position::cylinder_distance. No GL/App dependency — Core-only, /// per the Code Structure Rules. NAME WATCH: this file (not /// MoveToManager.cs, per r4-port-plan.md §3 "New code target") is the /// R4-V1 deliverable; the manager itself is R4-V2. /// public static class MoveToMath { /// /// Universal heading/distance epsilon (same literal as R3's A5/A6 — /// r4-moveto-decomp.md §12 constants inventory). /// public const float Epsilon = 0.000199999995f; /// /// Retail heading_diff (0x00528fb0, free function, raw /// 306327-306347), PINNED by direct disassembly of the PDB-matched /// retail binary (ghidra-confirmations.md §P3 — the strongest evidence /// tier in the R4 pin set): /// /// d = h1 - h2; /// if (fabs(h1 - h2) < F_EPSILON) d = 0; /// if (d < -F_EPSILON) d += 360; /// if (F_EPSILON < d && turnCmd != TurnRight) d = 360 - d; // the mirror /// return d; /// /// The mirror gates on the turn command NOT being TurnRight /// (0x6500000d) — TurnLeft (and any other command) measures the /// COMPLEMENTARY angle. This CONTRADICTS r4-moveto-decomp.md §5g's /// "arg3 UNUSED" claim, which the Ghidra disassembly pin overrides /// (V0-pins.md §P3 adjudication). Call sites: BeginTurnToHeading /// passes the CONSTANT TurnRight (mirror explicitly disabled — the /// direction pick stays the ≤180 test elsewhere); HandleTurnToHeading /// passes the LIVE current_command (can be TurnLeft). /// /// First heading, degrees. /// Second heading, degrees. /// The active turn command id — gates the mirror. /// Normalized heading difference, degrees. public static float HeadingDiff(float h1, float h2, uint turnCmd) { float d = h1 - h2; if (MathF.Abs(h1 - h2) < Epsilon) { d = 0f; } if (d < -Epsilon) { d += 360f; } if (Epsilon < d && turnCmd != MotionCommand.TurnRight) { d = 360f - d; } return d; } /// /// Retail heading_greater (00528f60, free function, raw /// 306281-306323), verbatim per r4-moveto-decomp.md §5f: /// /// if (fabs(a - b) > 180) greater = (b > a); // wrapped case: compare flipped /// else greater = (a > b); /// if (turnCmd == TurnRight) return greater; /// return !greater; // TurnLeft (and any other cmd): inverted /// /// "Has the turn passed the target heading" — direction-aware, /// 360°-wrap-aware. The visible TurnRight-arg idiom: the gate is /// == TurnRight (not != TurnRight as in /// 's mirror) — every OTHER command inverts, /// not just TurnLeft specifically. /// public static bool HeadingGreater(float a, float b, uint turnCmd) { bool greater = MathF.Abs(a - b) > 180f ? b > a : a > b; return turnCmd == MotionCommand.TurnRight ? greater : !greater; } /// /// Retail Position::heading(from, to) (0x005a9520, raw /// 438288-438290), PINNED per V0-pins.md §P5: compass degrees, 0 = /// North (+Y), 90 = East (+X), CLOCKWISE, range [0,360). /// /// heading(from, to) = (450 - atan2Deg(dy, dx)) % 360 /// /// Golden cardinals: N(0,+1)→0, E(+1,0)→90, S(0,-1)→180, W(-1,0)→270. /// Horizontal (X/Y) only — Z (height) does not participate, matching /// retail's compass-heading semantics. An in-tree twin of this formula /// already exists at SceneryHelpers.cs:75 (render-side, /// independently verified — not reused directly to keep this file /// GL-free per the Code Structure Rules, but the formula is identical). /// public static float PositionHeading(Vector3 from, Vector3 to) { float dx = to.X - from.X; float dy = to.Y - from.Y; float headingDeg = 450f - MathF.Atan2(dy, dx) * (180f / MathF.PI); headingDeg %= 360f; if (headingDeg < 0f) headingDeg += 360f; return headingDeg; } /// /// Retail Frame::get_heading (0x00535760, raw 319781) — /// extracts the compass heading (P5 convention) from a body orientation /// quaternion. The packer-reuse trap (V0-pins §P5 correction): /// acdream's outbound packer (GameWindow.YawToAcQuaternion) is /// wire-correct at the QUATERNION level but its internal scalar /// intermediate (headingDeg = 180 - yawDeg) is holtburger's /// SHIFTED convention, not retail's. This method uses the CORRECT /// scalar bridge derived from acdream's own body convention /// (PlayerMovementController.cs:1022-1025: Orientation = /// AxisAngle(Z, Yaw - PI/2), local-forward = +Y, Yaw=0 faces +X): /// world-forward = (cos Yaw, sin Yaw), so /// YawDeg = atan2Deg(forward.Y, forward.X) and /// heading = (90 - YawDeg) mod 360 — the exact inverse of /// . Identity quaternion (Yaw=PI/2, i.e. facing /// +Y/North) → heading 0, matching P5's "identity quaternion faces /// heading 0" pin. /// public static float GetHeading(Quaternion orientation) { var forward = Vector3.Transform(new Vector3(0f, 1f, 0f), orientation); float yawDeg = MathF.Atan2(forward.Y, forward.X) * (180f / MathF.PI); float headingDeg = 90f - yawDeg; headingDeg %= 360f; if (headingDeg < 0f) headingDeg += 360f; return headingDeg; } /// /// Retail Frame::set_heading (0x00535e40, raw /// 320055-320066) — builds a body orientation quaternion facing /// (P5 compass convention), preserving /// acdream's body-orientation convention (rotation about world Z only; /// 's pitch/roll, if any, is /// discarded — matching retail's set_heading, which is a pure /// yaw-about-Z setter). Exact inverse of : /// YawDeg = 90 - headingDeg, then Orientation = /// AxisAngle(Z, Yaw - PI/2) per /// PlayerMovementController.cs:1025's convention. /// /// Unused beyond signature parity with /// the render-side SceneryHelpers.SetHeading twin — retail's /// set_heading is a pure yaw-about-Z setter with no dependency /// on the prior orientation's roll/pitch component in the body-frame /// convention this port uses. /// Desired compass heading, degrees. public static Quaternion SetHeading(Quaternion baseOrientation, float headingDeg) { _ = baseOrientation; float yawDeg = 90f - headingDeg; float yaw = yawDeg * (MathF.PI / 180f); return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, yaw - MathF.PI / 2f); } /// /// Retail Position::cylinder_distance, the pure-math shape /// consumed by MoveToManager::GetCurrentDistance /// (005291b0, r4-moveto-decomp.md §5a) when use_spheres /// (wire bit 0x400) is set — object moves use edge-to-edge cylinder /// distance; position moves use plain center (Euclidean) distance /// instead (not ported here — Vector3.Distance already covers /// it). BN garbles the x87 plumbing in the raw, so the exact /// radius-combination arithmetic is not directly visible; ported per /// the PDB argument ORDER (own radius/height/position, target /// radius/height/position) with the standard cylinder-distance shape: /// planar (X/Y) center distance minus the sum of both radii, clamped /// at zero (overlapping cylinders report 0, never negative). Height is /// accepted for signature parity with the retail call (own/target /// height feed the caller's contact-plane logic elsewhere) but does /// NOT participate in this edge-to-edge planar computation — matching /// retail's use of cylinder_distance purely for the horizontal arrival /// gate. /// public static float CylinderDistance( float ownRadius, float ownHeight, Vector3 ownPos, float targetRadius, float targetHeight, Vector3 targetPos) { _ = ownHeight; _ = targetHeight; float dx = targetPos.X - ownPos.X; float dy = targetPos.Y - ownPos.Y; float centerDist = MathF.Sqrt(dx * dx + dy * dy); float edgeDist = centerDist - ownRadius - targetRadius; return edgeDist > 0f ? edgeDist : 0f; } }