feat(R4-V1): command-selection family + state widening (closes M2-mechanics, M11, M12, M15)
MovementParameters gains the verbatim selection family: GetCommand 0x0052aa00 (the walk-vs-run cascade INCLUDING the CanCharge 0x10 fast-path ACE dropped - retail's default can_charge=false + the fast-path present, the A13+A15 canceling-pair trap avoided; inclusive threshold edge per the raw), TowardsAndAway, GetDesiredHeading per the live Ghidra decompile (fwd-towards 0 / fwd-away 180 / back-towards 180 / back-away 0), FromWire/FromWireTurnTo (UnPackNet semantics, all 18 A4 masks round-tripped). New MoveToMath: HeadingDiff per the live Ghidra decompile of 0x00528fb0 (the 360-diff NOT-TurnRight mirror + F_EPSILON 0.000199999995f - the BN "arg unused" artifact corrected), HeadingGreater (the visible TurnRight idiom), PositionHeading/Get/SetHeading reusing the codebase's single yaw-heading convention (P5), CylinderDistance (PDB arg order; planar-minus-radii shape documented as the interpretation - the raw's x87 body is garbled; seam noted). MovementType gains Invalid + retail 6/7/8/9; MovementStruct widened (ObjectId/TopLevelId/Pos/Radius/Height/Params, additive); WeenieError += 0x0B/0x36/0x37/0x38/0x3D with retail-meaning doc comments. 148 new conformance tests. Full suite: 3,860 passed. Implemented by a dedicated agent against the V0-pinned spec; scope + suite independently verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
386b1ce550
commit
e0d2492cbb
14 changed files with 1973 additions and 4 deletions
201
src/AcDream.Core/Physics/Motion/MoveToMath.cs
Normal file
201
src/AcDream.Core/Physics/Motion/MoveToMath.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V1 — pure-math free functions consumed by the (future) MoveToManager
|
||||
/// port: <c>heading_diff</c>, <c>heading_greater</c>, <c>Position::heading</c>
|
||||
/// / <c>Frame::get_heading</c> / <c>Frame::set_heading</c>, and
|
||||
/// <c>Position::cylinder_distance</c>. No GL/App dependency — Core-only,
|
||||
/// per the Code Structure Rules. NAME WATCH: this file (not
|
||||
/// <c>MoveToManager.cs</c>, per r4-port-plan.md §3 "New code target") is the
|
||||
/// R4-V1 deliverable; the manager itself is R4-V2.
|
||||
/// </summary>
|
||||
public static class MoveToMath
|
||||
{
|
||||
/// <summary>
|
||||
/// Universal heading/distance epsilon (same literal as R3's A5/A6 —
|
||||
/// r4-moveto-decomp.md §12 constants inventory).
|
||||
/// </summary>
|
||||
public const float Epsilon = 0.000199999995f;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>heading_diff</c> (<c>0x00528fb0</c>, 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):
|
||||
/// <code>
|
||||
/// 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;
|
||||
/// </code>
|
||||
/// 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: <c>BeginTurnToHeading</c>
|
||||
/// passes the CONSTANT TurnRight (mirror explicitly disabled — the
|
||||
/// direction pick stays the ≤180 test elsewhere); <c>HandleTurnToHeading</c>
|
||||
/// passes the LIVE <c>current_command</c> (can be TurnLeft).
|
||||
/// </summary>
|
||||
/// <param name="h1">First heading, degrees.</param>
|
||||
/// <param name="h2">Second heading, degrees.</param>
|
||||
/// <param name="turnCmd">The active turn command id — gates the mirror.</param>
|
||||
/// <returns>Normalized heading difference, degrees.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>heading_greater</c> (<c>00528f60</c>, free function, raw
|
||||
/// 306281-306323), verbatim per r4-moveto-decomp.md §5f:
|
||||
/// <code>
|
||||
/// 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
|
||||
/// </code>
|
||||
/// "Has the turn passed the target heading" — direction-aware,
|
||||
/// 360°-wrap-aware. The visible TurnRight-arg idiom: the gate is
|
||||
/// <c>== TurnRight</c> (not <c>!= TurnRight</c> as in
|
||||
/// <see cref="HeadingDiff"/>'s mirror) — every OTHER command inverts,
|
||||
/// not just TurnLeft specifically.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Position::heading(from, to)</c> (<c>0x005a9520</c>, raw
|
||||
/// 438288-438290), PINNED per V0-pins.md §P5: compass degrees, 0 =
|
||||
/// North (+Y), 90 = East (+X), CLOCKWISE, range [0,360).
|
||||
/// <code>
|
||||
/// heading(from, to) = (450 - atan2Deg(dy, dx)) % 360
|
||||
/// </code>
|
||||
/// 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 <c>SceneryHelpers.cs:75</c> (render-side,
|
||||
/// independently verified — not reused directly to keep this file
|
||||
/// GL-free per the Code Structure Rules, but the formula is identical).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Frame::get_heading</c> (<c>0x00535760</c>, raw 319781) —
|
||||
/// extracts the compass heading (P5 convention) from a body orientation
|
||||
/// quaternion. <b>The packer-reuse trap (V0-pins §P5 correction):</b>
|
||||
/// acdream's outbound packer (<c>GameWindow.YawToAcQuaternion</c>) is
|
||||
/// wire-correct at the QUATERNION level but its internal scalar
|
||||
/// intermediate (<c>headingDeg = 180 - yawDeg</c>) is holtburger's
|
||||
/// SHIFTED convention, not retail's. This method uses the CORRECT
|
||||
/// scalar bridge derived from acdream's own body convention
|
||||
/// (<c>PlayerMovementController.cs:1022-1025</c>: <c>Orientation =
|
||||
/// AxisAngle(Z, Yaw - PI/2)</c>, local-forward = +Y, Yaw=0 faces +X):
|
||||
/// world-forward = <c>(cos Yaw, sin Yaw)</c>, so
|
||||
/// <c>YawDeg = atan2Deg(forward.Y, forward.X)</c> and
|
||||
/// <c>heading = (90 - YawDeg) mod 360</c> — the exact inverse of
|
||||
/// <see cref="SetHeading"/>. Identity quaternion (Yaw=PI/2, i.e. facing
|
||||
/// +Y/North) → heading 0, matching P5's "identity quaternion faces
|
||||
/// heading 0" pin.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Frame::set_heading</c> (<c>0x00535e40</c>, raw
|
||||
/// 320055-320066) — builds a body orientation quaternion facing
|
||||
/// <paramref name="headingDeg"/> (P5 compass convention), preserving
|
||||
/// acdream's body-orientation convention (rotation about world Z only;
|
||||
/// <paramref name="baseOrientation"/>'s pitch/roll, if any, is
|
||||
/// discarded — matching retail's <c>set_heading</c>, which is a pure
|
||||
/// yaw-about-Z setter). Exact inverse of <see cref="GetHeading"/>:
|
||||
/// <c>YawDeg = 90 - headingDeg</c>, then <c>Orientation =
|
||||
/// AxisAngle(Z, Yaw - PI/2)</c> per
|
||||
/// <c>PlayerMovementController.cs:1025</c>'s convention.
|
||||
/// </summary>
|
||||
/// <param name="baseOrientation">Unused beyond signature parity with
|
||||
/// the render-side <c>SceneryHelpers.SetHeading</c> twin — retail's
|
||||
/// <c>set_heading</c> 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.</param>
|
||||
/// <param name="headingDeg">Desired compass heading, degrees.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Position::cylinder_distance</c>, the pure-math shape
|
||||
/// consumed by <c>MoveToManager::GetCurrentDistance</c>
|
||||
/// (<c>005291b0</c>, r4-moveto-decomp.md §5a) when <c>use_spheres</c>
|
||||
/// (wire bit 0x400) is set — object moves use edge-to-edge cylinder
|
||||
/// distance; position moves use plain center (Euclidean) distance
|
||||
/// instead (not ported here — <c>Vector3.Distance</c> 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -184,4 +186,287 @@ public sealed class MovementParameters
|
|||
|
||||
/// <summary>Retail default 0.</summary>
|
||||
public uint ActionStamp { get; set; }
|
||||
|
||||
// ── R4-V1: command-selection family (closes M2-mechanics) ─────────────
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>MovementParameters::get_command</c> (<c>0x0052aa00</c>,
|
||||
/// 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 <see cref="HoldKey"/> cascade.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Command pick</b> (mirrors <c>towards_and_away</c>'s bands but is
|
||||
/// NOT identical — see the asymmetry note on <see cref="TowardsAndAway"/>):
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>MoveTowards && MoveAway</c> → delegate
|
||||
/// to <see cref="TowardsAndAway"/> (the three-band form).</description></item>
|
||||
/// <item><description><c>MoveTowards</c> only (or neither flag set —
|
||||
/// retail's <c>else if</c> falls through to the SAME branch): plain
|
||||
/// towards — <c>dist > DistanceToObject</c> → WalkForward,
|
||||
/// !movingAway; else idle (cmd 0).</description></item>
|
||||
/// <item><description><c>MoveAway</c> only: pure away —
|
||||
/// <c>dist < MinDistance</c> → WalkForward, movingAway=true (the
|
||||
/// heading flips +180 via <see cref="GetDesiredHeading"/> — turn-around,
|
||||
/// NOT WalkBackwards, unlike <see cref="TowardsAndAway"/>'s min-band);
|
||||
/// else idle.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>THE walk-vs-run rule</b> (confirms
|
||||
/// <c>feedback_autowalk_cancharge_bit</c> — port RETAIL's version of
|
||||
/// BOTH the fast-path ACE dropped and the threshold-close-walk pair):
|
||||
/// <c>HoldKey.Run</c> ⇐ <c>CanCharge</c> set (the fast-path — wins
|
||||
/// regardless of CanRun/CanWalk/distance), OR (<c>CanRun</c> set AND
|
||||
/// (<c>CanWalk</c> clear OR <c>dist - DistanceToObject >
|
||||
/// WalkRunThreshhold</c>)). <c>HoldKey.None</c> (walk) ⇐ no CanRun, or
|
||||
/// walk-capable within the threshold (INCLUSIVE ≤ — the raw's
|
||||
/// <c>test ah,0x41</c> after the fcom is the not-greater-than reading,
|
||||
/// §5c @308003).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="dist">Current distance-to-target (retail's
|
||||
/// <c>GetCurrentDistance</c> result — center or cylinder distance per
|
||||
/// <see cref="MoveToMath.CylinderDistance"/>).</param>
|
||||
/// <param name="headingDiff">Heading-to-target minus current heading,
|
||||
/// normalized [0,360) — UNUSED by <c>get_command</c> itself (the raw
|
||||
/// signature carries it for parity with the caller's local; retail's
|
||||
/// body never reads <c>arg3</c> in this build). Kept as a parameter for
|
||||
/// call-site symmetry with <c>BeginMoveForward</c> (§4c), which computes
|
||||
/// it immediately before calling <c>get_command</c>.</param>
|
||||
/// <param name="motion">Chosen motion command id, or 0 if no movement
|
||||
/// is needed (already in range).</param>
|
||||
/// <param name="holdKey">Chosen hold key (walk vs run).</param>
|
||||
/// <param name="movingAway">True if the chosen motion moves the mover
|
||||
/// AWAY from the target (feeds <see cref="GetDesiredHeading"/> and the
|
||||
/// arrival predicate's polarity).</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>MovementParameters::towards_and_away</c>
|
||||
/// (<c>0x0052a9a0</c>, raw 307917-307942), VERBATIM per
|
||||
/// r4-moveto-decomp.md §5d. Three bands:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>dist > DistanceToObject</c> → WalkForward,
|
||||
/// towards (not moving away).</description></item>
|
||||
/// <item><description><c>dist - MinDistance < F_EPSILON</c> (inside
|
||||
/// the min-distance band) → WalkBackward, moving away. NOTE the
|
||||
/// asymmetry vs <see cref="GetCommand"/>'s pure-away branch: this backs
|
||||
/// up with WalkBackwards (no turn-around), not WalkForward+heading-flip
|
||||
/// (r4-moveto-decomp.md :656).</description></item>
|
||||
/// <item><description>otherwise (strictly inside [MinDistance,
|
||||
/// DistanceToObject]) → idle (cmd 0).</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="dist">Current distance-to-target.</param>
|
||||
/// <param name="cmd">Chosen motion command, or 0 if already in the dead
|
||||
/// band.</param>
|
||||
/// <param name="movingAway">True only for the WalkBackward (min-band)
|
||||
/// case.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>MovementParameters::get_desired_heading</c>
|
||||
/// (<c>0x0052aad0</c>), PINNED by direct Ghidra decompile of
|
||||
/// <c>patchmem.gpr</c> (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):
|
||||
/// <code>
|
||||
/// __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;
|
||||
/// }
|
||||
/// </code>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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) ─────────
|
||||
|
||||
/// <summary>
|
||||
/// Factory for the retail <c>MovementParameters::UnPackNet</c> 7-dword
|
||||
/// MoveTo wire form (<c>0x0052ac50</c>, 0x1c bytes, raw 308118-308205 —
|
||||
/// r4-moveto-decomp.md §2g): <c>bitfield, distance_to_object,
|
||||
/// min_distance, fail_distance, speed, walk_run_threshhold,
|
||||
/// desired_heading</c>. Used by MoveToObject (type 6) and
|
||||
/// MoveToPosition (type 7) wire payloads — the SAME field order
|
||||
/// <c>UpdateMotion.TryParseMoveToPayload</c> 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 <paramref name="bitfield"/> resolves to false); the wire
|
||||
/// bitfield carries <c>can_charge</c> (0x10), the walk-vs-run answer
|
||||
/// consumed by <see cref="GetCommand"/> (cross-ref
|
||||
/// <c>feedback_autowalk_cancharge_bit</c>).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for the retail <c>MovementParameters::UnPackNet</c> 3-dword
|
||||
/// TurnTo wire form (0xc bytes, r4-moveto-decomp.md §2g): <c>bitfield,
|
||||
/// speed, desired_heading</c>. Used by TurnToObject (type 8) and
|
||||
/// TurnToHeading (type 9) wire payloads. Distance-related scalars
|
||||
/// (<c>DistanceToObject</c>/<c>MinDistance</c>/<c>FailDistance</c>/
|
||||
/// <c>WalkRunThreshhold</c>) are NOT on this wire form and keep the
|
||||
/// <see cref="MovementParameters"/> ctor defaults — retail's UnPackNet
|
||||
/// for this form only ever writes the three fields named here.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode the A4-pinned bitfield masks onto the named bool properties.
|
||||
/// Shared by <see cref="FromWire"/>/<see cref="FromWireTurnTo"/> — the
|
||||
/// wire bitfield always fully overwrites (retail's UnPackNet assigns
|
||||
/// the raw dword straight into <c>this->bitfield</c>, no merge).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue