feat(R3-W1): retail state completion — action FIFOs, MovementParameters, MotionNode, WeenieError renumber (closes J2, J16-codes)

InterpretedMotionState + the unified RawMotionState (LegacyRawMotionState
folded away) gain retail's action FIFO + ApplyMotion/RemoveMotion, ported
verbatim from the raw named decomp (0x0051e790/0x0051eb60 region, pc
293252-293703) including two genuine retail quirks pinned by tests and
independently re-verified against the raw text before commit:
- RawMotionState::ApplyMotion's RunForward (0x44000007) dead branch — a
  cycle-class apply of literal RunForward writes NOTHING (retail encodes
  run as WalkForward + HoldKey_Run on the raw state; same family as the
  D6 apply_run_to_command early-return).
- InterpretedMotionState::RemoveMotion's exact-match-only TurnRight/
  SideStepRight handling (left variants fall through to the style/
  forward branches).

MovementParameters verbatim (A4 pin): 18 named flags per the absolute
mask table, ctor = 0x1EE0F expansion + distance_to_object 0.6 /
fail_distance FLT_MAX / speed 1 / walk_run_threshhold 15 / hold_key
Invalid — with the two ACE-divergence traps ported RETAIL-side
(can_charge FALSE where ACE defaults true; threshold 15.0 not 1.0).
MotionNode {ContextId, Motion, JumpErrorCode} (acclient.h:53293) — the
pending_motions node W2 consumes.

WeenieError renumbered to retail's numeric semantics (A10 table):
NotGrounded=0x24, GeneralMovementFailure 0x24→0x47, new 0x3f/0x40/0x41
combat-stance rejects + 0x42 chat-emote + 0x45 action-depth; the
airborne jump/action returns corrected 0x48→0x24 per the A10 sweep
(incl. jump_is_allowed's null-physics-obj 0x24-not-8 divergence from
ACE). Codes are local-only — wire untouched.

Outbound packer proof: RawMotionStatePacker unmodified; all 6
golden-byte RawMotionStatePackTests pass unmodified. TS-24 register row
updated (capability landed; outbound wiring still open).

Implemented by a dedicated agent against the W0-pinned spec; quirks,
scope, and suite independently verified. Full suite green: 3,531
(374+425+713+2015+4 pre-existing skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 22:12:33 +02:00
parent 220927d350
commit 8664959152
11 changed files with 1483 additions and 117 deletions

View file

@ -0,0 +1,30 @@
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R3-W1 — verbatim port of retail's <c>CMotionInterp::MotionNode</c>
/// (<c>acclient.h:53293</c>, struct #5857):
/// <code>
/// struct __cppobj CMotionInterp::MotionNode : LListData
/// {
/// unsigned int context_id; // +4 (offset from LListData's own +0 next-ptr)
/// unsigned int motion; // +8 — the "action-class" field; bit 0x10000000 = action-class flag
/// unsigned int jump_error_code; // +0xc
/// };
/// </code>
/// This is the node type queued onto <c>CMotionInterp::pending_motions</c>
/// by <c>add_to_queue</c> (0x00527b80, docs/research/2026-07-02-r3-motioninterp/
/// r3-motioninterp-decomp.md §1a) and consumed head-first, unconditionally,
/// by <c>MotionDone</c> (0x00527ec0, §1c) / <c>HandleExitWorld</c> (0x00527f30,
/// §1d). W1 ports only the shape — the queue itself (<c>PendingMotions</c>,
/// <c>add_to_queue</c>, <c>MotionDone</c>) is R3-W2 scope (r3-port-plan.md §3).
/// </summary>
/// <param name="ContextId">Retail <c>context_id</c> (+4) — the
/// <c>MovementParameters.ContextId</c> that produced this node.</param>
/// <param name="Motion">Retail <c>motion</c> (+8) — the applied motion id.
/// Bit <c>0x10000000</c> marks an "action-class" motion; <c>MotionDone</c>
/// only pops the state action-FIFO head when this bit is set on the queue
/// head it is consuming.</param>
/// <param name="JumpErrorCode">Retail <c>jump_error_code</c> (+0xc) — the
/// <c>motion_allows_jump</c> result computed at enqueue time. Peeked by
/// <c>jump_is_allowed</c>'s pending-head short-circuit (W0-pins A2).</param>
public readonly record struct MotionNode(uint ContextId, uint Motion, uint JumpErrorCode);

View file

@ -0,0 +1,187 @@
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R3-W1 — verbatim port of retail's <c>MovementParameters</c>
/// (<c>acclient.h:31453</c>, struct #3460; bitfield struct
/// <c>acclient.h:31423-31443</c>):
/// <code>
/// 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;
/// };
/// </code>
///
/// <para>
/// The bitfield's absolute mask table is pinned in
/// <c>docs/research/2026-07-02-r3-motioninterp/W0-pins.md</c> §A4 (bit-for-bit
/// identical to ACE's <c>MovementParamFlags</c>):
/// </para>
/// <list type="table">
/// <item><term>0x1</term><description>CanWalk</description></item>
/// <item><term>0x2</term><description>CanRun</description></item>
/// <item><term>0x4</term><description>CanSidestep</description></item>
/// <item><term>0x8</term><description>CanWalkBackwards</description></item>
/// <item><term>0x10</term><description>CanCharge</description></item>
/// <item><term>0x20</term><description>FailWalk</description></item>
/// <item><term>0x40</term><description>UseFinalHeading</description></item>
/// <item><term>0x80</term><description>Sticky</description></item>
/// <item><term>0x100</term><description>MoveAway</description></item>
/// <item><term>0x200</term><description>MoveTowards</description></item>
/// <item><term>0x400</term><description>UseSpheres</description></item>
/// <item><term>0x800</term><description>SetHoldKey</description></item>
/// <item><term>0x1000</term><description>Autonomous</description></item>
/// <item><term>0x2000</term><description>ModifyRawState</description></item>
/// <item><term>0x4000</term><description>ModifyInterpretedState</description></item>
/// <item><term>0x8000</term><description>CancelMoveTo</description></item>
/// <item><term>0x10000</term><description>StopCompletely</description></item>
/// <item><term>0x20000</term><description>DisableJumpDuringLink</description></item>
/// </list>
///
/// <para>
/// Ctor default (raw 300510-300534, <c>0x00524380</c>):
/// <c>(bitfield &amp; 0xfffdee0f) | 0x1ee0f</c> → <c>0x1EE0F</c> sets
/// {CanWalk, CanRun, CanSidestep, CanWalkBackwards, MoveTowards, UseSpheres,
/// SetHoldKey, ModifyRawState, ModifyInterpretedState, CancelMoveTo,
/// StopCompletely}; clears {CanCharge, FailWalk, UseFinalHeading, Sticky,
/// MoveAway, Autonomous, DisableJumpDuringLink}.
/// Scalars: <c>min_distance=0</c>, <c>distance_to_object=0.6</c>,
/// <c>fail_distance=FLT_MAX</c>, <c>desired_heading=0</c>, <c>speed=1</c>,
/// <c>walk_run_threshhold=15</c> (NOT ACE's 1.0 — W0-pins A4 divergence trap),
/// <c>context_id=0</c>, <c>hold_key_to_apply=HoldKey.Invalid</c>,
/// <c>action_stamp=0</c>.
/// </para>
///
/// <para>
/// <b>ACE-divergence traps (W0-pins A4, do not copy):</b> ACE's
/// <c>MovementParameters</c> ctor sets <c>CanCharge = true</c>
/// (MovementParameters.cs:58) — retail's default has bit <c>0x10</c> CLEAR;
/// this port defaults <c>CanCharge = false</c>. ACE also changed
/// <c>Default_WalkRunThreshold</c> to 1.0 (L50) vs retail's literal 15.0
/// (@300519) — this port defaults <c>WalkRunThreshhold = 15f</c>.
/// </para>
///
/// <para>
/// Named bool properties per plan (no <c>ToBitfield()</c>/<c>FromBitfield()</c>
/// pair — the wire never carries this struct raw; <c>RawMotionState::Pack</c>
/// 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).
/// </para>
/// </summary>
public sealed class MovementParameters
{
// ── bitfield flags (retail 0x1EE0F default; W0-pins A4) ───────────────
/// <summary>Mask 0x1 — default true.</summary>
public bool CanWalk { get; set; } = true;
/// <summary>Mask 0x2 — default true.</summary>
public bool CanRun { get; set; } = true;
/// <summary>Mask 0x4 — default true.</summary>
public bool CanSidestep { get; set; } = true;
/// <summary>Mask 0x8 — default true.</summary>
public bool CanWalkBackwards { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public bool CanCharge { get; set; }
/// <summary>Mask 0x20 — default false.</summary>
public bool FailWalk { get; set; }
/// <summary>Mask 0x40 — default false.</summary>
public bool UseFinalHeading { get; set; }
/// <summary>Mask 0x80 — default false.</summary>
public bool Sticky { get; set; }
/// <summary>Mask 0x100 — default false.</summary>
public bool MoveAway { get; set; }
/// <summary>Mask 0x200 — default true.</summary>
public bool MoveTowards { get; set; } = true;
/// <summary>Mask 0x400 — default true.</summary>
public bool UseSpheres { get; set; } = true;
/// <summary>Mask 0x800 — default true. DoMotion @306188: byte1&amp;8
/// requests a <c>SetHoldKey</c> call before <c>adjust_motion</c>.</summary>
public bool SetHoldKey { get; set; } = true;
/// <summary>
/// Mask 0x1000 — default FALSE. Not the same virtual as
/// <c>IWeenieObject.IsThePlayer</c> (W0-pins A3) — this is the
/// per-call "was this an autonomous (locally-predicted) action?" flag.
/// </summary>
public bool Autonomous { get; set; }
/// <summary>Mask 0x2000 — default true. DoMotion @306213: byte1&amp;0x20
/// mirrors the applied motion into <c>RawMotionState</c> via
/// <c>ApplyMotion</c>/<c>RemoveMotion</c>.</summary>
public bool ModifyRawState { get; set; } = true;
/// <summary>Mask 0x4000 — default true. Mirrors into
/// <c>InterpretedMotionState</c>.</summary>
public bool ModifyInterpretedState { get; set; } = true;
/// <summary>Mask 0x8000 — default true. Bitfield high-byte sign bit;
/// DoMotion/StopMotion @306183/@305684: triggers
/// <c>interrupt_current_movement</c> before the rest of the call.</summary>
public bool CancelMoveTo { get; set; } = true;
/// <summary>Mask 0x10000 — default true.</summary>
public bool StopCompletelyFlag { get; set; } = true;
/// <summary>
/// Mask 0x20000 — default FALSE. DoInterpretedMotion @305597: when set,
/// forces the computed <c>jump_error_code</c> to <c>0x48</c> (A1: jump
/// BLOCKED by motion/position) regardless of what
/// <c>motion_allows_jump</c> would have said.
/// </summary>
public bool DisableJumpDuringLink { get; set; }
// ── scalar fields (retail ctor 0x00524380 defaults) ───────────────────
/// <summary>Retail default 0.6.</summary>
public float DistanceToObject { get; set; } = 0.6f;
/// <summary>Retail default 0.</summary>
public float MinDistance { get; set; }
/// <summary>Retail default 0.</summary>
public float DesiredHeading { get; set; }
/// <summary>Retail default 1.</summary>
public float Speed { get; set; } = 1f;
/// <summary>Retail default FLT_MAX.</summary>
public float FailDistance { get; set; } = float.MaxValue;
/// <summary>
/// Retail default 15.0 (@300519). ACE-divergence trap (W0-pins A4): ACE
/// changed <c>Default_WalkRunThreshold</c> to 1.0 — do not copy.
/// </summary>
public float WalkRunThreshhold { get; set; } = 15f;
/// <summary>Retail default 0.</summary>
public uint ContextId { get; set; }
/// <summary>Retail default <see cref="Physics.HoldKey.Invalid"/>.</summary>
public HoldKey HoldKeyToApply { get; set; } = HoldKey.Invalid;
/// <summary>Retail default 0.</summary>
public uint ActionStamp { get; set; }
}