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:
parent
220927d350
commit
8664959152
11 changed files with 1483 additions and 117 deletions
|
|
@ -209,7 +209,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-20 | GfxObj polys drawn by dictionary iteration, not DrawingBSP traversal (**#113**): physics/no-draw polys referenced by no BSP node render as visible surfaces; the `CollectDrawingBspPolygonIds` filter exists (:1004) but is NOT applied (naive walk made doors disappear, `e46d3d9` un-applied, user-gated 2026-06-11) | `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1027` | Correct fix is full BSP-traversal-order drawing per the holistic port handoff (docs/research/2026-06-11-building-render-holistic-port-handoff.md); the id filter must first be diagnosed on a door GfxObj (Issue113PhantomStairsDumpTests) | Phantom geometry visible NOW (Holtburg meeting-hall "staircase" wall ramp 0x010014C3; 8 orphan polys on hill cottage 0x01000827); draw order also diverges from retail's BSP order | D3DPolyRender drawing-BSP traversal; ConstructMesh 0x0059dfa0 |
|
||||
| TS-21 | Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands; "we don't parse yet" comment is STALE (K-fix7 parses PD → SetCharacterSkills) | `src/AcDream.App/Input/PlayerMovementController.cs:341` | Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) | Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs | retail height = (skill/(skill+1300))×22.2 + 0.05 |
|
||||
| TS-23 | PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always `IsPlayer ∣ EdgeSlide` | `src/AcDream.App/Input/PlayerMovementController.cs:1128` | Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults too | On a PK/PKLite character, local client lets players walk through where retail collides — prediction vs server disagree the moment PvP statuses enter play | PWD._bitfield acclient.h:6431-6463; pc:406898-406918 |
|
||||
| TS-24 | RawMotionState action list always empty at runtime — the packer now emits `num_actions` (bits 11–15) + per-action u16 pairs (L.2b, `RawMotionState::Pack` 0x0051ed10), but the outbound caller builds an empty `Actions` list, so discrete motion events (emotes, one-shots) are still never broadcast | `src/AcDream.App/Rendering/GameWindow.cs:8297` (empty Actions); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91` | Discrete client-initiated motions (D2) not wired yet; packer-ready, runtime emission is Phase 2+ | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | `RawMotionState::Pack` 0x0051ed10; num_actions `PackBitfield` acclient.h:46487 |
|
||||
| TS-24 | RawMotionState action list always empty at runtime — the packer emits `num_actions` (bits 11–15) + per-action u16 pairs (L.2b, `RawMotionState::Pack` 0x0051ed10), and R3-W1 gives `RawMotionState`/`InterpretedMotionState` the retail-faithful action FIFO (`AddAction`/`RemoveAction`/`ApplyMotion`/`RemoveMotion`, `src/AcDream.Core/Physics/RawMotionState.cs` + `MotionInterpreter.cs`), but nothing calls `AddAction` yet — the outbound caller still builds an empty `Actions` list, so discrete motion events (emotes, one-shots) are still never broadcast | `src/AcDream.App/Rendering/GameWindow.cs:8297` (empty Actions); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91`; FIFO capability `src/AcDream.Core/Physics/RawMotionState.cs` | Discrete client-initiated motions (D2) not wired yet; packer-ready, state-ready (W1), runtime emission lands with R3-W2's `add_to_queue`/`DoInterpretedMotion` population | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | `RawMotionState::Pack` 0x0051ed10; num_actions `PackBitfield` acclient.h:46487 |
|
||||
| TS-25 | `current_style` (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves `CurrentStyle` at default (stance not tracked here) | `src/AcDream.App/Rendering/GameWindow.cs:8286` (CurrentStyle left default); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80` | Stance switching is M2 combat scope | Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement | `RawMotionState::Pack` current_style 0x0051ed10 |
|
||||
| TS-26 | UpdatePosition's four u16 sequence numbers parsed but never checked for freshness; retail rejects stale/out-of-order packets. (The 0xF74C UpdateMotion side of this gap CLOSED 2026-07-02 — L.2g S1 `MotionSequenceGate` ports the retail instance/movement/server-control gate; the UP side + teleport/force-position stamps remain open) | `src/AcDream.Core.Net/Messages/UpdatePosition.cs:30` | Loopback ACE rarely reorders, so the gap is invisible in the dev loop | On a real network, a reordered/post-teleport straggler applies as-is — remotes snap backward / flicker; a teleport-vs-position race renders an entity in the wrong cell | PositionPack trailer (ACE PositionPack.cs::Write); `CPhysicsObj::newer_event` 0x00451b10 |
|
||||
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
||||
|
|
|
|||
30
src/AcDream.Core/Physics/Motion/MotionNode.cs
Normal file
30
src/AcDream.Core/Physics/Motion/MotionNode.cs
Normal 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);
|
||||
187
src/AcDream.Core/Physics/Motion/MovementParameters.cs
Normal file
187
src/AcDream.Core/Physics/Motion/MovementParameters.cs
Normal 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 & 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&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&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; }
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -107,81 +109,119 @@ public enum MovementType
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// WeenieError codes returned by CMotionInterp methods.
|
||||
/// Values are the hex constants used directly in the decompiled C code.
|
||||
/// WeenieError-shaped codes returned by CMotionInterp methods. Values are
|
||||
/// the hex constants used directly in the decompiled C code.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>R3-W1 renumber (closes J16-codes/A10).</b> Prior to this slice, the
|
||||
/// names were shuffled relative to retail's actual numeric semantics
|
||||
/// (<c>GeneralMovementFailure</c> was assigned 0x24 and used at the
|
||||
/// airborne-jump gate, but retail's 0x24 means "not grounded / no contact"
|
||||
/// and retail's 0x47 is the general-movement-failure code; 0x48 meant
|
||||
/// "cannot jump while in the air" here, but retail's 0x48 means "jump
|
||||
/// blocked by the CURRENT MOTION OR POSITION" — a blocklist check
|
||||
/// (<c>motion_allows_jump</c>), not an airborne check). Renumbered per the
|
||||
/// definitive, exhaustively-swept table in
|
||||
/// <c>docs/research/2026-07-02-r3-motioninterp/W0-pins.md</c> §A10 (19
|
||||
/// return sites + 1 store site over raw 304908-306277 + 300150-300540).
|
||||
/// These codes are LOCAL-ONLY — never serialized to the wire — so the
|
||||
/// renumber is safe (verified: no consumer outside
|
||||
/// <c>MotionInterpreter.cs</c> pattern-matches on the numeric value).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public enum WeenieError : uint
|
||||
{
|
||||
/// <summary>0x00 — success.</summary>
|
||||
None = 0x00,
|
||||
/// <summary>0x08 — PhysicsObj is null.</summary>
|
||||
/// <summary>
|
||||
/// 0x08 — no <c>physics_obj</c>. Sites (A10): StopCompletely @305214;
|
||||
/// DoInterpretedMotion @305579; StopInterpretedMotion @305639;
|
||||
/// StopMotion @305680; jump @305798; DoMotion @306165.
|
||||
/// </summary>
|
||||
NoPhysicsObject = 0x08,
|
||||
/// <summary>0x24 — general movement failure.</summary>
|
||||
GeneralMovementFailure = 0x24,
|
||||
/// <summary>0x47 — cannot jump from this position (motion state blocks it).</summary>
|
||||
YouCantJumpFromThisPosition = 0x47,
|
||||
/// <summary>0x48 — cannot jump while in the air.</summary>
|
||||
YouCantJumpWhileInTheAir = 0x48,
|
||||
/// <summary>0x49 — loaded down / weenie blocked the jump.</summary>
|
||||
/// <summary>
|
||||
/// 0x24 — not grounded / no contact. Sites (A10):
|
||||
/// <c>jump_is_allowed</c> @305570 (gravity-active creature without
|
||||
/// Contact+OnWalkable; also the <c>physics_obj == null</c> case, which
|
||||
/// falls out to this same code per the A10 note — NOT 8);
|
||||
/// <c>DoInterpretedMotion</c> @305622-305623 (action-class motion
|
||||
/// blocked by <c>contact_allows_move</c>).
|
||||
/// </summary>
|
||||
NotGrounded = 0x24,
|
||||
/// <summary>
|
||||
/// 0x3f — Crouch (0x41000012) rejected while in combat stance. Site:
|
||||
/// DoMotion @306196.
|
||||
/// </summary>
|
||||
CrouchInCombatStance = 0x3f,
|
||||
/// <summary>
|
||||
/// 0x40 — Sitting (0x41000013) rejected while in combat stance. Site:
|
||||
/// DoMotion @306199.
|
||||
/// </summary>
|
||||
SitInCombatStance = 0x40,
|
||||
/// <summary>
|
||||
/// 0x41 — Sleeping (0x41000014) rejected while in combat stance. Site:
|
||||
/// DoMotion @306202.
|
||||
/// </summary>
|
||||
SleepInCombatStance = 0x41,
|
||||
/// <summary>
|
||||
/// 0x42 — <c>motion & 0x2000000</c> (the chat-emote bit) rejected
|
||||
/// outside NonCombat (0x8000003d). Site: DoMotion @306205.
|
||||
/// </summary>
|
||||
ChatEmoteOutsideNonCombat = 0x42,
|
||||
/// <summary>
|
||||
/// 0x45 — action-queue depth cap: an action-class motion (bit
|
||||
/// 0x10000000) with <c>GetNumActions() >= 6</c> pending. Site:
|
||||
/// DoMotion @306209.
|
||||
/// </summary>
|
||||
ActionDepthExceeded = 0x45,
|
||||
/// <summary>
|
||||
/// 0x47 — general movement failure. Sites (A10):
|
||||
/// <c>jump_is_allowed</c> @305525 (<c>IsFullyConstrained</c>);
|
||||
/// @305549+305556 (<c>JumpStaminaCost</c> refusal);
|
||||
/// <c>CMotionInterp::PerformMovement</c> @306227 (dispatch type-1 > 4);
|
||||
/// <c>MovementManager::PerformMovement</c> @300201 (dispatch type-1 > 8).
|
||||
/// </summary>
|
||||
GeneralMovementFailure = 0x47,
|
||||
/// <summary>
|
||||
/// 0x48 — jump BLOCKED by the current motion or position (A1: the
|
||||
/// <c>motion_allows_jump</c> literal-range blocklist — NOT an airborne
|
||||
/// check; airborne is 0x24). Sites (A10):
|
||||
/// <c>motion_allows_jump</c> @304930; <c>jump_charge_is_allowed</c>
|
||||
/// @304948 (Fallen or Crouch..Sleeping); <c>charge_jump</c> @305459
|
||||
/// (same predicate); STORED (not returned) as the queue node's
|
||||
/// <c>jump_error_code</c> in <c>DoInterpretedMotion</c> @305605 when
|
||||
/// <c>disable_jump_during_link</c> is set.
|
||||
/// </summary>
|
||||
YouCantJumpFromThisPosition = 0x48,
|
||||
/// <summary>
|
||||
/// 0x49 — the weenie's <c>CanJump(jump_extent)</c> virtual refused
|
||||
/// (e.g. stamina/burden gate). Sites: <c>jump_charge_is_allowed</c>
|
||||
/// @304941; <c>charge_jump</c> @305454.
|
||||
/// </summary>
|
||||
CantJumpLoadedDown = 0x49,
|
||||
}
|
||||
|
||||
// ── Motion state structs ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Raw (network-derived) motion state for the local player, as consumed by
|
||||
/// the (still-approximate) <see cref="MotionInterpreter"/> input-state path.
|
||||
/// Struct layout in chunk_00520000 starts at offset +0x14 (struct field +0x20 =
|
||||
/// ForwardCommand, +0x28 = ForwardSpeed, etc.).
|
||||
/// Interpreted motion state, derived from the raw state (retail
|
||||
/// <c>InterpretedMotionState</c>, ctor 0x0051e8d0, decomp 293418-293431).
|
||||
/// Struct layout: starts at offset +0x44 (ForwardCommand at +0x4C, ForwardSpeed at +0x50).
|
||||
///
|
||||
/// <para>
|
||||
/// Renamed from <c>RawMotionState</c> during the L.2b wire-parity slice
|
||||
/// (2026-06-30) to free that name for the retail-faithful, full-field
|
||||
/// <c>AcDream.Core.Physics.RawMotionState</c> (mirrors
|
||||
/// <c>acclient.h RawMotionState::PackBitfield</c>) used by the outbound
|
||||
/// wire packer. This partial type is D6/Phase-2 territory (the
|
||||
/// <c>adjust_motion</c>/<c>apply_raw_movement</c> input-state port) —
|
||||
/// out of scope for this slice. It is expected to be folded into or
|
||||
/// replaced by the retail-faithful type when D6 lands.
|
||||
/// <b>R3-W1 (closes J2):</b> gains retail's action FIFO
|
||||
/// (<see cref="AddAction"/>/<see cref="RemoveAction"/>/<see cref="GetNumActions"/>/
|
||||
/// <see cref="ApplyMotion"/>/<see cref="RemoveMotion"/>) — previously this was
|
||||
/// a flat 6-field struct with no action tracking, so
|
||||
/// <c>DoMotion</c>'s <c>GetNumActions() >= 6</c> depth cap and
|
||||
/// <c>MotionDone</c>'s action-class <c>RemoveAction</c> pop had nothing to
|
||||
/// operate on. The action list is backed by a private
|
||||
/// <see cref="List{T}"/> lazily created on first mutation (defensive
|
||||
/// against a bare <c>default(InterpretedMotionState)</c>, which C# structs
|
||||
/// permit even though every constructor in this file routes through
|
||||
/// <see cref="Default"/>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public struct LegacyRawMotionState
|
||||
{
|
||||
/// <summary>Current style / stance (retail raw_state.current_style,
|
||||
/// offset +0x18). Adopted from the inbound InterpretedMotionState by
|
||||
/// move_to_interpreted_state (0x005289c0 line 305944). Default
|
||||
/// NonCombat 0x8000003D.</summary>
|
||||
public uint CurrentStyle;
|
||||
/// <summary>Forward/backward motion command (offset +0x20).</summary>
|
||||
public uint ForwardCommand;
|
||||
/// <summary>Speed scalar for forward motion (offset +0x28).</summary>
|
||||
public float ForwardSpeed;
|
||||
/// <summary>Sidestep command (offset +0x2C).</summary>
|
||||
public uint SideStepCommand;
|
||||
/// <summary>Speed scalar for sidestep (offset +0x34, inferred from ACE).</summary>
|
||||
public float SideStepSpeed;
|
||||
/// <summary>Turn command (offset +0x38).</summary>
|
||||
public uint TurnCommand;
|
||||
/// <summary>Speed scalar for turn (offset +0x40, inferred).</summary>
|
||||
public float TurnSpeed;
|
||||
|
||||
/// <summary>Initialize to the idle/ready state (1.0 speed, Ready command).</summary>
|
||||
public static LegacyRawMotionState Default() => new()
|
||||
{
|
||||
CurrentStyle = 0x8000003Du,
|
||||
ForwardCommand = MotionCommand.Ready,
|
||||
ForwardSpeed = 1.0f,
|
||||
SideStepCommand = 0,
|
||||
SideStepSpeed = 1.0f,
|
||||
TurnCommand = 0,
|
||||
TurnSpeed = 1.0f,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interpreted motion state, derived from the raw state.
|
||||
/// Struct layout: starts at offset +0x44 (ForwardCommand at +0x4C, ForwardSpeed at +0x50).
|
||||
/// </summary>
|
||||
public struct InterpretedMotionState
|
||||
{
|
||||
/// <summary>Forward/backward interpreted command (offset +0x4C).</summary>
|
||||
|
|
@ -196,6 +236,20 @@ public struct InterpretedMotionState
|
|||
public uint TurnCommand;
|
||||
/// <summary>Speed scalar for turn (offset +0x60).</summary>
|
||||
public float TurnSpeed;
|
||||
/// <summary>Current style / stance (retail current_style). Adopted from
|
||||
/// the raw state's style channel; NOT part of retail's
|
||||
/// <c>InterpretedMotionState::ApplyMotion</c> dispatch itself (that
|
||||
/// writes <c>this->current_style</c> only via the negative-motion
|
||||
/// branch — see <see cref="ApplyMotion"/>). Default NonCombat
|
||||
/// 0x8000003D.</summary>
|
||||
public uint CurrentStyle;
|
||||
|
||||
private List<RawMotionAction>? _actions;
|
||||
|
||||
/// <summary>Action FIFO in retail order (oldest first). Empty (never
|
||||
/// null) when read — the private field is lazily created.</summary>
|
||||
public readonly IReadOnlyList<RawMotionAction> Actions
|
||||
=> (IReadOnlyList<RawMotionAction>?)_actions ?? Array.Empty<RawMotionAction>();
|
||||
|
||||
/// <summary>Initialize to the idle/ready state.</summary>
|
||||
public static InterpretedMotionState Default() => new()
|
||||
|
|
@ -206,7 +260,141 @@ public struct InterpretedMotionState
|
|||
SideStepSpeed = 1.0f,
|
||||
TurnCommand = 0,
|
||||
TurnSpeed = 1.0f,
|
||||
CurrentStyle = 0x8000003Du,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// <c>InterpretedMotionState::AddAction</c> (0x0051e9e0, decomp
|
||||
/// 293500-293527): unconditional tail-append of
|
||||
/// <c>{motion, speed, action_stamp, autonomous}</c>. Identical shape to
|
||||
/// <see cref="RawMotionState.AddAction"/> — retail duplicates the
|
||||
/// LListData append logic per state type rather than sharing it.
|
||||
/// </summary>
|
||||
public void AddAction(uint motion, float speed, uint actionStamp, bool autonomous)
|
||||
{
|
||||
_actions ??= new List<RawMotionAction>();
|
||||
_actions.Add(new RawMotionAction(
|
||||
Command: (ushort)motion,
|
||||
Stamp: (ushort)actionStamp,
|
||||
Autonomous: autonomous,
|
||||
Speed: speed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>InterpretedMotionState::RemoveAction</c> (0x0051ead0, decomp
|
||||
/// 293568-293586): pop the FIFO head unconditionally, returning its
|
||||
/// <c>motion</c> field (0 when empty).
|
||||
/// </summary>
|
||||
public uint RemoveAction()
|
||||
{
|
||||
if (_actions is null || _actions.Count == 0)
|
||||
return 0;
|
||||
var head = _actions[0];
|
||||
_actions.RemoveAt(0);
|
||||
return head.Command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>InterpretedMotionState::GetNumActions</c> (0x0051eb00, decomp
|
||||
/// 293590-293603): count the FIFO by walking it (retail has no O(1)
|
||||
/// count field on the LList).
|
||||
/// </summary>
|
||||
public readonly uint GetNumActions() => (uint)(_actions?.Count ?? 0);
|
||||
|
||||
/// <summary>
|
||||
/// <c>InterpretedMotionState::ApplyMotion</c> (0x0051ea40, decomp
|
||||
/// 293531-293564). Verbatim dispatch, quoted from the raw named decomp:
|
||||
/// <code>
|
||||
/// if (arg2 == 0x6500000d) { turn_command = arg2; turn_speed = params.speed; return; }
|
||||
/// if (arg2 == 0x6500000f) { sidestep_command = arg2; sidestep_speed = params.speed; return; }
|
||||
/// if ((arg2 & 0x40000000) != 0) { forward_command = arg2; forward_speed = params.speed; return; }
|
||||
/// if (arg2 < 0) { forward_command = 0x41000003; current_style = arg2; return; }
|
||||
/// if ((arg2 & 0x10000000) != 0)
|
||||
/// AddAction(arg2, params.speed, params.action_stamp, (params.bitfield>>0xc)&1);
|
||||
/// </code>
|
||||
/// Note only TurnRight (0x6500000d) and SideStepRight (0x6500000f) are
|
||||
/// tested here — unlike <see cref="RawMotionState.ApplyMotion"/>, the
|
||||
/// LEFT variants (TurnLeft/SideStepLeft) are NOT separately cased;
|
||||
/// retail's <c>adjust_motion</c> normalizes Left→Right upstream before
|
||||
/// this ever runs, so this asymmetry is verbatim, not a gap.
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c> — the motion id AFTER
|
||||
/// <c>adjust_motion</c> normalization (the ADJUSTED id, unlike
|
||||
/// <see cref="RawMotionState.ApplyMotion"/> which takes the ORIGINAL).</param>
|
||||
/// <param name="p">Retail <c>arg3</c> (<c>MovementParameters const*</c>).</param>
|
||||
public void ApplyMotion(uint motion, MovementParameters p)
|
||||
{
|
||||
if (motion == 0x6500000du) // TurnRight
|
||||
{
|
||||
TurnCommand = motion;
|
||||
TurnSpeed = p.Speed;
|
||||
return;
|
||||
}
|
||||
if (motion == 0x6500000fu) // SideStepRight
|
||||
{
|
||||
SideStepCommand = motion;
|
||||
SideStepSpeed = p.Speed;
|
||||
return;
|
||||
}
|
||||
if ((motion & 0x40000000u) != 0)
|
||||
{
|
||||
ForwardCommand = motion;
|
||||
ForwardSpeed = p.Speed;
|
||||
return;
|
||||
}
|
||||
if (motion >= 0x80000000u) // arg2 < 0 as signed int32
|
||||
{
|
||||
ForwardCommand = 0x41000003u;
|
||||
CurrentStyle = motion;
|
||||
return;
|
||||
}
|
||||
if ((motion & 0x10000000u) != 0)
|
||||
AddAction(motion, p.Speed, p.ActionStamp, p.Autonomous);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>InterpretedMotionState::RemoveMotion</c> (0x0051e790, decomp
|
||||
/// 293315-293340):
|
||||
/// <code>
|
||||
/// if (arg2 == 0x6500000d) { turn_command = 0; return; }
|
||||
/// if (arg2 == 0x6500000f) { sidestep_command = 0; return; }
|
||||
/// if ((arg2 & 0x40000000) == 0) {
|
||||
/// if (arg2 < 0 && arg2 == current_style) current_style = 0x8000003d;
|
||||
/// } else if (arg2 == forward_command) {
|
||||
/// forward_command = 0x41000003; forward_speed = 1f;
|
||||
/// }
|
||||
/// </code>
|
||||
/// Note the asymmetric range test vs <see cref="RawMotionState.RemoveMotion"/>
|
||||
/// (which uses <c>(arg2 - 0x6500000d) > 3</c> covering all four
|
||||
/// turn/sidestep ids) — this one only special-cases the RIGHT variants
|
||||
/// by exact equality; TurnLeft/SideStepLeft fall through to the
|
||||
/// style/forward-command branch below. Verbatim, not a gap (mirrors
|
||||
/// retail's own asymmetry between the two RemoveMotion bodies).
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c>.</param>
|
||||
public void RemoveMotion(uint motion)
|
||||
{
|
||||
if (motion == 0x6500000du)
|
||||
{
|
||||
TurnCommand = 0;
|
||||
return;
|
||||
}
|
||||
if (motion == 0x6500000fu)
|
||||
{
|
||||
SideStepCommand = 0;
|
||||
return;
|
||||
}
|
||||
if ((motion & 0x40000000u) == 0)
|
||||
{
|
||||
if (motion >= 0x80000000u && motion == CurrentStyle)
|
||||
CurrentStyle = 0x8000003du;
|
||||
}
|
||||
else if (motion == ForwardCommand)
|
||||
{
|
||||
ForwardCommand = 0x41000003u;
|
||||
ForwardSpeed = 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -317,8 +505,18 @@ public sealed class MotionInterpreter
|
|||
/// <summary>Optional WeenieObject for stamina / run-rate queries (struct offset +0x04).</summary>
|
||||
public IWeenieObject? WeenieObj { get; set; }
|
||||
|
||||
/// <summary>Raw (network-derived) motion state (struct offsets +0x14..+0x44).</summary>
|
||||
public LegacyRawMotionState RawState;
|
||||
/// <summary>
|
||||
/// Raw (network-derived) motion state (struct offsets +0x14..+0x44).
|
||||
///
|
||||
/// <para>
|
||||
/// R3-W1 fold (closes J2): this now holds the SAME retail-faithful,
|
||||
/// full-field <see cref="Physics.RawMotionState"/> type the outbound
|
||||
/// wire packer reads from — the former flat <c>LegacyRawMotionState</c>
|
||||
/// struct (no action FIFO, no <c>ApplyMotion</c>/<c>RemoveMotion</c>)
|
||||
/// is deleted. One raw-state type end to end.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public RawMotionState RawState;
|
||||
|
||||
/// <summary>Interpreted motion state derived from raw (struct offsets +0x44..+0x7C).</summary>
|
||||
public InterpretedMotionState InterpretedState;
|
||||
|
|
@ -385,7 +583,7 @@ public sealed class MotionInterpreter
|
|||
|
||||
public MotionInterpreter()
|
||||
{
|
||||
RawState = LegacyRawMotionState.Default();
|
||||
RawState = new RawMotionState();
|
||||
InterpretedState = InterpretedMotionState.Default();
|
||||
}
|
||||
|
||||
|
|
@ -393,7 +591,7 @@ public sealed class MotionInterpreter
|
|||
{
|
||||
PhysicsObj = physicsObj;
|
||||
WeenieObj = weenieObj;
|
||||
RawState = LegacyRawMotionState.Default();
|
||||
RawState = new RawMotionState();
|
||||
InterpretedState = InterpretedMotionState.Default();
|
||||
}
|
||||
|
||||
|
|
@ -477,9 +675,12 @@ public sealed class MotionInterpreter
|
|||
|
||||
if (!contact_allows_move(motion))
|
||||
{
|
||||
// Action commands (bit 0x10000000) are blocked mid-air.
|
||||
// Action commands (bit 0x10000000) are blocked mid-air. A10:
|
||||
// DoInterpretedMotion @305622-305623 returns 0x24 (NotGrounded)
|
||||
// here, not 0x48 (that code is reserved for the
|
||||
// motion_allows_jump literal-range blocklist).
|
||||
if ((motion & 0x10000000u) != 0)
|
||||
return WeenieError.YouCantJumpWhileInTheAir;
|
||||
return WeenieError.NotGrounded;
|
||||
// Non-action motions are queued silently; state still updates.
|
||||
}
|
||||
|
||||
|
|
@ -505,10 +706,10 @@ public sealed class MotionInterpreter
|
|||
RawState.ForwardCommand = MotionCommand.Ready;
|
||||
RawState.ForwardSpeed = 1.0f;
|
||||
}
|
||||
if (RawState.SideStepCommand == motion)
|
||||
if (RawState.SidestepCommand == motion)
|
||||
{
|
||||
RawState.SideStepCommand = 0;
|
||||
RawState.SideStepSpeed = 1.0f;
|
||||
RawState.SidestepCommand = 0;
|
||||
RawState.SidestepSpeed = 1.0f;
|
||||
}
|
||||
if (RawState.TurnCommand == motion)
|
||||
{
|
||||
|
|
@ -563,7 +764,7 @@ public sealed class MotionInterpreter
|
|||
/// uVar1 = FUN_005285e0(InterpretedState.ForwardCommand) — motion_allows_jump
|
||||
/// *(+0x20) = 0x41000003 (RawState.ForwardCommand = Ready)
|
||||
/// *(+0x28) = 0x3f800000 (RawState.ForwardSpeed = 1.0f)
|
||||
/// *(+0x2c) = 0 (RawState.SideStepCommand = 0)
|
||||
/// *(+0x2c) = 0 (RawState.SidestepCommand = 0)
|
||||
/// *(+0x38) = 0 (RawState.TurnCommand = 0)
|
||||
/// *(+0x4c) = 0x41000003 (InterpretedState.ForwardCommand = Ready)
|
||||
/// *(+0x50) = 0x3f800000 (InterpretedState.ForwardSpeed = 1.0f)
|
||||
|
|
@ -582,8 +783,8 @@ public sealed class MotionInterpreter
|
|||
// Reset raw state
|
||||
RawState.ForwardCommand = MotionCommand.Ready;
|
||||
RawState.ForwardSpeed = 1.0f;
|
||||
RawState.SideStepCommand = 0;
|
||||
RawState.SideStepSpeed = 1.0f;
|
||||
RawState.SidestepCommand = 0;
|
||||
RawState.SidestepSpeed = 1.0f;
|
||||
RawState.TurnCommand = 0;
|
||||
RawState.TurnSpeed = 1.0f;
|
||||
|
||||
|
|
@ -1039,7 +1240,7 @@ public sealed class MotionInterpreter
|
|||
/// elif WeenieObj.IsCreature() returns false: proceed
|
||||
/// iVar2 = PhysicsObj
|
||||
/// if Gravity flag NOT set OR (Contact AND OnWalkable): ← grounded or no gravity
|
||||
/// return 0x24 (GeneralMovementFailure)
|
||||
/// return 0x24 (NotGrounded)
|
||||
/// if FUN_0050f730() (IsFullyConstrained) != 0: return 0x47
|
||||
/// if pending queue action has non-zero jump error: return that error
|
||||
/// iVar2 = FUN_00528660() (jump_charge_is_allowed)
|
||||
|
|
@ -1052,8 +1253,13 @@ public sealed class MotionInterpreter
|
|||
/// </summary>
|
||||
public WeenieError jump_is_allowed(float extent, int staminaCost)
|
||||
{
|
||||
// A10: jump_is_allowed returns 0x24 (NotGrounded), NOT 8, when
|
||||
// physics_obj == null — the "8 = no physics obj" convention holds
|
||||
// everywhere else in CMotionInterp but not here (raw structure
|
||||
// 305512 + 305570: the `if (physics_obj != 0) {...}` wrapper falls
|
||||
// out to `return 0x24;`).
|
||||
if (PhysicsObj is null)
|
||||
return WeenieError.GeneralMovementFailure;
|
||||
return WeenieError.NotGrounded;
|
||||
|
||||
// Must have gravity and be grounded (Contact + OnWalkable) to start a jump.
|
||||
bool hasGravity = PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity);
|
||||
|
|
@ -1061,7 +1267,7 @@ public sealed class MotionInterpreter
|
|||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||||
|
||||
if (!hasGravity || !isGrounded)
|
||||
return WeenieError.YouCantJumpWhileInTheAir;
|
||||
return WeenieError.NotGrounded;
|
||||
|
||||
// Delegate jump eligibility to WeenieObj if present.
|
||||
if (WeenieObj is not null && !WeenieObj.CanJump(extent))
|
||||
|
|
@ -1421,6 +1627,6 @@ public sealed class MotionInterpreter
|
|||
if ((motion & 0x10000000u) == 0)
|
||||
return WeenieError.None; // apply-only: state kept, no cycle change
|
||||
|
||||
return WeenieError.GeneralMovementFailure; // retail 0x24
|
||||
return WeenieError.NotGrounded; // retail 0x24 (A10)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -15,57 +17,306 @@ public enum HoldKey : uint
|
|||
/// event (jump, attack, etc.) layered on top of the continuous
|
||||
/// forward/sidestep/turn axes.
|
||||
///
|
||||
/// Retail packs each action as <c>u16 command</c> then
|
||||
/// <c>u16 ((stamp & 0x7FFF) | (autonomous ? 0x8000 : 0))</c>
|
||||
/// (<c>RawMotionState::Pack</c>, 0x0051ed10, decomp lines ~293998-294010).
|
||||
/// </summary>
|
||||
public readonly record struct RawMotionAction(ushort Command, ushort Stamp, bool Autonomous);
|
||||
|
||||
/// <summary>
|
||||
/// Pure-data mirror of retail's <c>RawMotionState</c> struct
|
||||
/// (<c>acclient.h</c> <c>RawMotionState::PackBitfield</c>, line 46474;
|
||||
/// packer at <c>RawMotionState::Pack</c>, 0x0051ed10).
|
||||
/// <para>
|
||||
/// Retail's in-memory action node (<c>RawMotionState::AddAction</c>,
|
||||
/// 0x0051e840, and <c>InterpretedMotionState::AddAction</c>, 0x0051e9e0 —
|
||||
/// both a 0x14-byte <c>LListData</c>-derived node) carries FOUR fields:
|
||||
/// <c>motion</c> (+4), <c>speed</c> (+8), <c>action_stamp</c> (+0xc),
|
||||
/// <c>autonomous</c> (+0x10). <see cref="Speed"/> is the in-memory-only
|
||||
/// field the R3-W1 action FIFO needs for <c>ApplyMotion</c>'s velocity
|
||||
/// bookkeeping; it is NOT part of the wire encoding.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This type carries the COMPLETE motion-state snapshot the way retail's
|
||||
/// <c>CPhysicsObj::InqRawMotionState()</c> would return it. The packer
|
||||
/// (<see cref="AcDream.Core.Net"/>) compares every field against
|
||||
/// <see cref="Default"/> and only emits the fields that differ — see
|
||||
/// <c>RawMotionState::Pack</c> for the exact default-difference logic.
|
||||
/// Retail packs each action ON THE WIRE as <c>u16 command</c> then
|
||||
/// <c>u16 ((stamp & 0x7FFF) | (autonomous ? 0x8000 : 0))</c>
|
||||
/// (<c>RawMotionState::Pack</c>, 0x0051ed10, decomp lines ~293998-294010)
|
||||
/// — <see cref="Command"/>/<see cref="Stamp"/>/<see cref="Autonomous"/> are
|
||||
/// exactly the packed triple; <see cref="RawMotionStatePacker"/> reads only
|
||||
/// those three.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public readonly record struct RawMotionAction(
|
||||
ushort Command,
|
||||
ushort Stamp,
|
||||
bool Autonomous,
|
||||
float Speed = 1f);
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — verbatim, full-field port of retail's <c>RawMotionState</c>
|
||||
/// (<c>acclient.h</c> <c>RawMotionState::PackBitfield</c>, line 46474; ctor
|
||||
/// <c>RawMotionState::RawMotionState</c>, 0x0051e7f0, decomp lines
|
||||
/// 293344-293361; packer at <c>RawMotionState::Pack</c>, 0x0051ed10).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>R3-W1 fold (closes J2):</b> this type now ALSO serves as
|
||||
/// <c>MotionInterpreter.RawState</c> — the former <c>LegacyRawMotionState</c>
|
||||
/// (a flat 7-field struct with no action FIFO) is deleted. One raw-state
|
||||
/// type end to end: the physics-side motion interpreter mutates it via
|
||||
/// <see cref="ApplyMotion"/>/<see cref="RemoveMotion"/>/
|
||||
/// <see cref="AddAction"/>/<see cref="RemoveAction"/>, and the SAME
|
||||
/// instance's <see cref="Actions"/> is read by <see cref="RawMotionStatePacker"/>
|
||||
/// when building the outbound wire packet. Previously the packer's action
|
||||
/// list was always empty (register TS-24) because nothing populated it —
|
||||
/// starting R3-W2 (the <c>add_to_queue</c>/<c>MotionDone</c> port),
|
||||
/// <c>CMotionInterp</c> populates this list locally exactly like retail
|
||||
/// does, so the packed bytes will start reflecting real queued actions.
|
||||
/// W1 itself does not change what gets packed (no caller invokes
|
||||
/// <see cref="AddAction"/> yet) — only adds the capability + tests.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// PURE DATA: no PacketWriter / GL / Net dependency. Lives in
|
||||
/// <c>AcDream.Core.Physics</c> so the (future) motion interpreter, which is
|
||||
/// <c>AcDream.Core.Physics</c> so the motion interpreter, which is
|
||||
/// also Core.Physics, can populate it without taking a Core.Net dependency
|
||||
/// (Code Structure Rule #2).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class RawMotionState
|
||||
{
|
||||
public HoldKey CurrentHoldKey { get; init; } = HoldKey.None;
|
||||
public uint CurrentStyle { get; init; } = 0x8000003Du;
|
||||
public uint ForwardCommand { get; init; } = 0x41000003u;
|
||||
public HoldKey ForwardHoldKey { get; init; } = HoldKey.Invalid;
|
||||
public float ForwardSpeed { get; init; } = 1.0f;
|
||||
public uint SidestepCommand { get; init; } = 0u;
|
||||
public HoldKey SidestepHoldKey { get; init; } = HoldKey.Invalid;
|
||||
public float SidestepSpeed { get; init; } = 1.0f;
|
||||
public uint TurnCommand { get; init; } = 0u;
|
||||
public HoldKey TurnHoldKey { get; init; } = HoldKey.Invalid;
|
||||
public float TurnSpeed { get; init; } = 1.0f;
|
||||
/// <summary>Retail <c>current_holdkey</c> (ctor default HoldKey_None).</summary>
|
||||
public HoldKey CurrentHoldKey { get; set; } = HoldKey.None;
|
||||
/// <summary>Retail <c>current_style</c> (ctor default 0x8000003D, NonCombat).</summary>
|
||||
public uint CurrentStyle { get; set; } = 0x8000003Du;
|
||||
/// <summary>Retail <c>forward_command</c> (ctor default 0x41000003, Ready).</summary>
|
||||
public uint ForwardCommand { get; set; } = 0x41000003u;
|
||||
/// <summary>Retail <c>forward_holdkey</c> (ctor default HoldKey_Invalid).</summary>
|
||||
public HoldKey ForwardHoldKey { get; set; } = HoldKey.Invalid;
|
||||
/// <summary>Retail <c>forward_speed</c> (ctor default 1.0).</summary>
|
||||
public float ForwardSpeed { get; set; } = 1.0f;
|
||||
/// <summary>Retail <c>sidestep_command</c> (ctor default 0).</summary>
|
||||
public uint SidestepCommand { get; set; }
|
||||
/// <summary>Retail <c>sidestep_holdkey</c> (ctor default HoldKey_Invalid).</summary>
|
||||
public HoldKey SidestepHoldKey { get; set; } = HoldKey.Invalid;
|
||||
/// <summary>Retail <c>sidestep_speed</c> (ctor default 1.0).</summary>
|
||||
public float SidestepSpeed { get; set; } = 1.0f;
|
||||
/// <summary>Retail <c>turn_command</c> (ctor default 0).</summary>
|
||||
public uint TurnCommand { get; set; }
|
||||
/// <summary>Retail <c>turn_holdkey</c> (ctor default HoldKey_Invalid).</summary>
|
||||
public HoldKey TurnHoldKey { get; set; } = HoldKey.Invalid;
|
||||
/// <summary>Retail <c>turn_speed</c> (ctor default 1.0).</summary>
|
||||
public float TurnSpeed { get; set; } = 1.0f;
|
||||
|
||||
private readonly List<RawMotionAction> _actions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Discrete action list. Retail packs <c>num_actions</c> (count, not the
|
||||
/// Discrete action FIFO (retail <c>actions</c>, an <c>LListData</c>
|
||||
/// tail-append queue). Retail packs <c>num_actions</c> (count, not the
|
||||
/// values) into bits 11-15 of the flags dword, then emits each action's
|
||||
/// command+stamp pair unconditionally after the continuous-axis fields.
|
||||
/// command+stamp pair unconditionally after the continuous-axis fields
|
||||
/// (<see cref="RawMotionStatePacker"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Settable via object-initializer for test fixtures / the wire-packer
|
||||
/// call sites that build a one-shot snapshot; assigning replaces the
|
||||
/// whole FIFO. <see cref="AddAction"/>/<see cref="RemoveAction"/> are
|
||||
/// the retail-faithful mutators for live FIFO discipline.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public IReadOnlyList<RawMotionAction> Actions { get; init; } = Array.Empty<RawMotionAction>();
|
||||
public IReadOnlyList<RawMotionAction> Actions
|
||||
{
|
||||
get => _actions;
|
||||
set
|
||||
{
|
||||
_actions.Clear();
|
||||
_actions.AddRange(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail defaults — a field bit is set in the packed flags dword ONLY
|
||||
/// when the live value differs from these (<c>RawMotionState::Pack</c>,
|
||||
/// 0x0051ed10).
|
||||
/// 0x0051ed10). NEVER mutate this shared instance.
|
||||
/// </summary>
|
||||
public static readonly RawMotionState Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// <c>RawMotionState::AddAction</c> (0x0051e840, decomp 293365-293392):
|
||||
/// unconditional tail-append of
|
||||
/// <c>{motion, speed, action_stamp, autonomous}</c>.
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c> — the action motion id.</param>
|
||||
/// <param name="speed">Retail <c>arg3</c>.</param>
|
||||
/// <param name="actionStamp">Retail <c>arg4</c>.</param>
|
||||
/// <param name="autonomous">Retail <c>arg5</c> (nonzero = autonomous).</param>
|
||||
public void AddAction(uint motion, float speed, uint actionStamp, bool autonomous)
|
||||
{
|
||||
_actions.Add(new RawMotionAction(
|
||||
Command: (ushort)motion,
|
||||
Stamp: (ushort)actionStamp,
|
||||
Autonomous: autonomous,
|
||||
Speed: speed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>RawMotionState::RemoveAction</c> (0x0051e8a0, decomp 293396-293414):
|
||||
/// pop the FIFO head unconditionally, returning its <c>motion</c> field
|
||||
/// (0 when empty — retail returns <c>*(head+4)</c>, i.e. the stored
|
||||
/// <c>arg2</c>/<c>Command</c>, widened; the C# port returns the widened
|
||||
/// <see cref="RawMotionAction.Command"/> as a <c>uint</c>).
|
||||
/// </summary>
|
||||
public uint RemoveAction()
|
||||
{
|
||||
if (_actions.Count == 0)
|
||||
return 0;
|
||||
var head = _actions[0];
|
||||
_actions.RemoveAt(0);
|
||||
return head.Command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>RawMotionState::ApplyMotion</c> (0x0051eb60, decomp 293630-293703).
|
||||
/// Verbatim dispatch, quoted from the raw named decomp:
|
||||
/// <code>
|
||||
/// if ((arg2 - 0x6500000d) > 3) { // outside [0x6500000d, 0x65000010]
|
||||
/// if ((arg2 & 0x40000000) == 0) { // NOT forward-class
|
||||
/// if (arg2 >= 0) { // NOT style-class (high bit clear)
|
||||
/// if ((arg2 & 0x10000000) != 0)
|
||||
/// AddAction(arg2, params.speed, params.action_stamp, (params.bitfield>>0xc)&1);
|
||||
/// } else if (current_style != arg2) {
|
||||
/// forward_command = 0x41000003; current_style = arg2;
|
||||
/// }
|
||||
/// } else if (arg2 != 0x44000007) { // forward-class, NOT RunForward
|
||||
/// forward_command = arg2;
|
||||
/// if (params.bitfield byte1 & 8 != 0) { forward_holdkey = Invalid; forward_speed = params.speed; }
|
||||
/// else { forward_holdkey = params.hold_key_to_apply; forward_speed = params.speed; }
|
||||
/// }
|
||||
/// // arg2 == 0x44000007 (RunForward) with the forward-class bit set: falls through, no write.
|
||||
/// return;
|
||||
/// }
|
||||
/// switch (arg2) {
|
||||
/// case 0x6500000d: case 0x6500000e: // TurnRight/TurnLeft
|
||||
/// turn_command = arg2;
|
||||
/// if (SetHoldKey bit) { turn_holdkey = Invalid; turn_speed = params.speed; }
|
||||
/// else { turn_holdkey = params.hold_key_to_apply; turn_speed = params.speed; }
|
||||
/// return;
|
||||
/// case 0x6500000f: case 0x65000010: // SideStepRight/SideStepLeft
|
||||
/// sidestep_command = arg2;
|
||||
/// if (SetHoldKey bit) { sidestep_holdkey = Invalid; sidestep_speed = params.speed; }
|
||||
/// else { sidestep_holdkey = params.hold_key_to_apply; sidestep_speed = params.speed; }
|
||||
/// return;
|
||||
/// }
|
||||
/// </code>
|
||||
/// The <c>arg2 == 0x44000007</c> (RunForward) no-write fallthrough on the
|
||||
/// forward-class branch is a genuine retail quirk (RunForward bit
|
||||
/// 0x40000000 IS set, but the `else if (arg2 != 0x44000007)` guard
|
||||
/// excludes it) — ported verbatim, not "fixed".
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c> — the ORIGINAL (pre-adjustment)
|
||||
/// motion id per the DoMotion/StopMotion mirror-discipline callers.</param>
|
||||
/// <param name="p">Retail <c>arg3</c> (<c>MovementParameters const*</c>).</param>
|
||||
public void ApplyMotion(uint motion, MovementParameters p)
|
||||
{
|
||||
if (motion - 0x6500000du > 3u)
|
||||
{
|
||||
if ((motion & 0x40000000u) == 0)
|
||||
{
|
||||
if (motion < 0x80000000u) // arg2 >= 0 as signed int32
|
||||
{
|
||||
if ((motion & 0x10000000u) != 0)
|
||||
AddAction(motion, p.Speed, p.ActionStamp, p.Autonomous);
|
||||
}
|
||||
else if (CurrentStyle != motion)
|
||||
{
|
||||
ForwardCommand = 0x41000003u;
|
||||
CurrentStyle = motion;
|
||||
}
|
||||
}
|
||||
else if (motion != 0x44000007u)
|
||||
{
|
||||
ForwardCommand = motion;
|
||||
if (p.SetHoldKey)
|
||||
{
|
||||
ForwardHoldKey = HoldKey.Invalid;
|
||||
ForwardSpeed = p.Speed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ForwardHoldKey = p.HoldKeyToApply;
|
||||
ForwardSpeed = p.Speed;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (motion)
|
||||
{
|
||||
case 0x6500000du: // TurnRight
|
||||
case 0x6500000eu: // TurnLeft
|
||||
TurnCommand = motion;
|
||||
if (p.SetHoldKey)
|
||||
{
|
||||
TurnHoldKey = HoldKey.Invalid;
|
||||
TurnSpeed = p.Speed;
|
||||
}
|
||||
else
|
||||
{
|
||||
TurnHoldKey = p.HoldKeyToApply;
|
||||
TurnSpeed = p.Speed;
|
||||
}
|
||||
return;
|
||||
case 0x6500000fu: // SideStepRight
|
||||
case 0x65000010u: // SideStepLeft
|
||||
SidestepCommand = motion;
|
||||
if (p.SetHoldKey)
|
||||
{
|
||||
SidestepHoldKey = HoldKey.Invalid;
|
||||
SidestepSpeed = p.Speed;
|
||||
}
|
||||
else
|
||||
{
|
||||
SidestepHoldKey = p.HoldKeyToApply;
|
||||
SidestepSpeed = p.Speed;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>RawMotionState::RemoveMotion</c> (0x0051e6e0, decomp 293252-293288):
|
||||
/// <code>
|
||||
/// if ((arg2 - 0x6500000d) > 3) {
|
||||
/// if ((arg2 & 0x40000000) == 0) {
|
||||
/// if (arg2 < 0 && arg2 == current_style) current_style = 0x8000003d;
|
||||
/// } else if (arg2 == forward_command) {
|
||||
/// forward_command = 0x41000003; forward_speed = 1f;
|
||||
/// }
|
||||
/// return;
|
||||
/// }
|
||||
/// switch (arg2) {
|
||||
/// case 0x6500000d: case 0x6500000e: turn_command = 0; return;
|
||||
/// case 0x6500000f: case 0x65000010: sidestep_command = 0; return;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c> — the ORIGINAL motion id.</param>
|
||||
public void RemoveMotion(uint motion)
|
||||
{
|
||||
if (motion - 0x6500000du > 3u)
|
||||
{
|
||||
if ((motion & 0x40000000u) == 0)
|
||||
{
|
||||
if (motion >= 0x80000000u && motion == CurrentStyle)
|
||||
CurrentStyle = 0x8000003du;
|
||||
}
|
||||
else if (motion == ForwardCommand)
|
||||
{
|
||||
ForwardCommand = 0x41000003u;
|
||||
ForwardSpeed = 1f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (motion)
|
||||
{
|
||||
case 0x6500000du:
|
||||
case 0x6500000eu:
|
||||
TurnCommand = 0;
|
||||
return;
|
||||
case 0x6500000fu:
|
||||
case 0x65000010u:
|
||||
SidestepCommand = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — action FIFO discipline + <c>ApplyMotion</c>/<c>RemoveMotion</c>
|
||||
/// field effects on <see cref="InterpretedMotionState"/> (closes J2). Oracle:
|
||||
/// docs/research/named-retail/acclient_2013_pseudo_c.txt — verbatim bodies
|
||||
/// quoted in MotionInterpreter.cs doc comments:
|
||||
/// <c>InterpretedMotionState::AddAction</c> (0x0051e9e0), <c>RemoveAction</c>
|
||||
/// (0x0051ead0), <c>GetNumActions</c> (0x0051eb00), <c>ApplyMotion</c>
|
||||
/// (0x0051ea40), <c>RemoveMotion</c> (0x0051e790).
|
||||
/// </summary>
|
||||
public sealed class InterpretedMotionStateActionFifoTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_HasEmptyActionsAndZeroCount()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
Assert.Empty(ims.Actions);
|
||||
Assert.Equal(0u, ims.GetNumActions());
|
||||
}
|
||||
|
||||
// ── AddAction / RemoveAction / GetNumActions FIFO discipline ──────────
|
||||
|
||||
[Fact]
|
||||
public void AddAction_AppendsInOrder_GetNumActionsCounts()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
|
||||
ims.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
|
||||
|
||||
Assert.Equal(2u, ims.GetNumActions());
|
||||
Assert.Equal((ushort)0x004Bu, ims.Actions[0].Command);
|
||||
Assert.Equal((ushort)0x0050u, ims.Actions[1].Command);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAction_PopsHeadFirst_FifoOrder()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
|
||||
ims.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
|
||||
|
||||
uint first = ims.RemoveAction();
|
||||
uint second = ims.RemoveAction();
|
||||
|
||||
Assert.Equal(0x004Bu, first);
|
||||
Assert.Equal(0x0050u, second);
|
||||
Assert.Equal(0u, ims.GetNumActions());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAction_Empty_ReturnsZero()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
Assert.Equal(0u, ims.RemoveAction());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNumActions_DefaultStruct_NoNullRef()
|
||||
{
|
||||
// Defensive: a bare default(InterpretedMotionState) (bypassing
|
||||
// .Default()) must not NRE on GetNumActions/Actions/RemoveAction —
|
||||
// the lazy-list field starts null.
|
||||
InterpretedMotionState bare = default;
|
||||
Assert.Equal(0u, bare.GetNumActions());
|
||||
Assert.Empty(bare.Actions);
|
||||
Assert.Equal(0u, bare.RemoveAction());
|
||||
}
|
||||
|
||||
// ── DoMotion's depth-cap gate consumes this directly (documentation) ──
|
||||
|
||||
[Fact]
|
||||
public void GetNumActions_SixQueued_MeetsDoMotionDepthCapThreshold()
|
||||
{
|
||||
// DoMotion (0x00528d20 @306159) rejects with 0x45 when an
|
||||
// action-class motion arrives AND GetNumActions() >= 6. W1 only
|
||||
// proves the counter is correct; the gate itself lands in W5.
|
||||
var ims = InterpretedMotionState.Default();
|
||||
for (uint i = 0; i < 6; i++)
|
||||
ims.AddAction(0x10000000u + i, 1.0f, i, false);
|
||||
|
||||
Assert.Equal(6u, ims.GetNumActions());
|
||||
Assert.True(ims.GetNumActions() >= 6);
|
||||
}
|
||||
|
||||
// ── ApplyMotion field effects (0x0051ea40) ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_TurnRight_SetsTurnCommandAndSpeed()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
var p = new MovementParameters { Speed = 1.5f };
|
||||
|
||||
ims.ApplyMotion(0x6500000du, p);
|
||||
|
||||
Assert.Equal(0x6500000du, ims.TurnCommand);
|
||||
Assert.Equal(1.5f, ims.TurnSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_SideStepRight_SetsSideStepCommandAndSpeed()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
var p = new MovementParameters { Speed = 1.248f };
|
||||
|
||||
ims.ApplyMotion(0x6500000fu, p);
|
||||
|
||||
Assert.Equal(0x6500000fu, ims.SideStepCommand);
|
||||
Assert.Equal(1.248f, ims.SideStepSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_ForwardClassMotion_SetsForwardCommandAndSpeed()
|
||||
{
|
||||
// Unlike RawMotionState::ApplyMotion, InterpretedMotionState's
|
||||
// forward-class branch has NO RunForward exclusion — every
|
||||
// forward-class id (bit 0x40000000) writes forward_command.
|
||||
var ims = InterpretedMotionState.Default();
|
||||
var p = new MovementParameters { Speed = 2.94f };
|
||||
|
||||
ims.ApplyMotion(0x44000007u, p); // RunForward
|
||||
|
||||
Assert.Equal(0x44000007u, ims.ForwardCommand);
|
||||
Assert.Equal(2.94f, ims.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_StyleClassMotion_ResetsForwardToReady_SetsCurrentStyle()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.ForwardCommand = 0x45000005u;
|
||||
var p = new MovementParameters();
|
||||
|
||||
ims.ApplyMotion(0x80000042u, p);
|
||||
|
||||
Assert.Equal(0x41000003u, ims.ForwardCommand);
|
||||
Assert.Equal(0x80000042u, ims.CurrentStyle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_ActionClassMotion_AddsAction()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
var p = new MovementParameters { Speed = 1.0f, ActionStamp = 7u, Autonomous = true };
|
||||
|
||||
ims.ApplyMotion(0x1000004Bu, p);
|
||||
|
||||
Assert.Equal(1u, ims.GetNumActions());
|
||||
var a = ims.Actions[0];
|
||||
Assert.Equal((ushort)0x004Bu, a.Command);
|
||||
Assert.Equal(1.0f, a.Speed);
|
||||
Assert.Equal((ushort)7u, a.Stamp);
|
||||
Assert.True(a.Autonomous);
|
||||
}
|
||||
|
||||
// ── RemoveMotion field effects (0x0051e790) ────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_TurnRightExact_ClearsTurnCommand()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.TurnCommand = 0x6500000du;
|
||||
ims.RemoveMotion(0x6500000du);
|
||||
Assert.Equal(0u, ims.TurnCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_TurnLeftExact_DoesNotMatchTurnBranch_FallsThroughToStyleCheck()
|
||||
{
|
||||
// Asymmetric vs RawMotionState::RemoveMotion: InterpretedMotionState
|
||||
// only exact-matches 0x6500000d (TurnRight) for the turn branch, NOT
|
||||
// 0x6500000e (TurnLeft) — verbatim per the raw decomp.
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.TurnCommand = 0x6500000eu;
|
||||
ims.RemoveMotion(0x6500000eu);
|
||||
// Falls through: bit 0x40000000 clear, arg2 (0x6500000e) is not
|
||||
// negative and != current_style (0x8000003D default) -> no-op.
|
||||
Assert.Equal(0x6500000eu, ims.TurnCommand); // untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_SideStepRightExact_ClearsSideStepCommand()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.SideStepCommand = 0x6500000fu;
|
||||
ims.RemoveMotion(0x6500000fu);
|
||||
Assert.Equal(0u, ims.SideStepCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_ForwardClassMotion_MatchingCommand_ResetsToReady()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.ForwardCommand = 0x45000005u;
|
||||
ims.ForwardSpeed = 3.0f;
|
||||
|
||||
ims.RemoveMotion(0x45000005u);
|
||||
|
||||
Assert.Equal(0x41000003u, ims.ForwardCommand);
|
||||
Assert.Equal(1f, ims.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_StyleClassMotion_MatchingCurrentStyle_ResetsToNonCombat()
|
||||
{
|
||||
var ims = InterpretedMotionState.Default();
|
||||
ims.CurrentStyle = 0x80000042u;
|
||||
|
||||
ims.RemoveMotion(0x80000042u);
|
||||
|
||||
Assert.Equal(0x8000003du, ims.CurrentStyle);
|
||||
}
|
||||
}
|
||||
33
tests/AcDream.Core.Tests/Physics/Motion/MotionNodeTests.cs
Normal file
33
tests/AcDream.Core.Tests/Physics/Motion/MotionNodeTests.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — <c>CMotionInterp::MotionNode</c> shape pin. Oracle:
|
||||
/// acclient.h:53293 (struct #5857). W2 consumes this as the
|
||||
/// <c>pending_motions</c> element type; W1 only ports the shape.
|
||||
/// </summary>
|
||||
public sealed class MotionNodeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_StoresAllThreeFields()
|
||||
{
|
||||
var node = new MotionNode(ContextId: 7u, Motion: 0x41000003u, JumpErrorCode: 0x48u);
|
||||
|
||||
Assert.Equal(7u, node.ContextId);
|
||||
Assert.Equal(0x41000003u, node.Motion);
|
||||
Assert.Equal(0x48u, node.JumpErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equality_IsValueBased()
|
||||
{
|
||||
var a = new MotionNode(1u, 2u, 3u);
|
||||
var b = new MotionNode(1u, 2u, 3u);
|
||||
var c = new MotionNode(1u, 2u, 4u);
|
||||
|
||||
Assert.Equal(a, b);
|
||||
Assert.NotEqual(a, c);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — <c>MovementParameters</c> ctor-default pins. Oracle:
|
||||
/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A4 (bitfield 0x1EE0F
|
||||
/// expansion) + r3-motioninterp-decomp.md §0 (scalar ctor, raw 300510-300534,
|
||||
/// 0x00524380). Every flag asserted individually against the retail
|
||||
/// 0x1EE0F default so a future bit-numbering slip fails loudly, plus the two
|
||||
/// ACE-divergence traps (CanCharge, WalkRunThreshhold) get dedicated tests.
|
||||
/// </summary>
|
||||
public sealed class MovementParametersTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_SetFlags_MatchRetail0x1EE0FExpansion()
|
||||
{
|
||||
var p = new MovementParameters();
|
||||
|
||||
// Bits SET in 0x1EE0F: 0x1,0x2,0x4,0x8,0x200,0x400,0x800,0x2000,0x4000,0x8000,0x10000
|
||||
Assert.True(p.CanWalk); // 0x1
|
||||
Assert.True(p.CanRun); // 0x2
|
||||
Assert.True(p.CanSidestep); // 0x4
|
||||
Assert.True(p.CanWalkBackwards); // 0x8
|
||||
Assert.True(p.MoveTowards); // 0x200
|
||||
Assert.True(p.UseSpheres); // 0x400
|
||||
Assert.True(p.SetHoldKey); // 0x800
|
||||
Assert.True(p.ModifyRawState); // 0x2000
|
||||
Assert.True(p.ModifyInterpretedState); // 0x4000
|
||||
Assert.True(p.CancelMoveTo); // 0x8000
|
||||
Assert.True(p.StopCompletelyFlag); // 0x10000
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ClearFlags_MatchRetail0x1EE0FExpansion()
|
||||
{
|
||||
var p = new MovementParameters();
|
||||
|
||||
// Bits CLEAR in 0x1EE0F: 0x10,0x20,0x40,0x80,0x100,0x1000,0x20000
|
||||
Assert.False(p.CanCharge); // 0x10 — ACE-divergence trap
|
||||
Assert.False(p.FailWalk); // 0x20
|
||||
Assert.False(p.UseFinalHeading); // 0x40
|
||||
Assert.False(p.Sticky); // 0x80
|
||||
Assert.False(p.MoveAway); // 0x100
|
||||
Assert.False(p.Autonomous); // 0x1000
|
||||
Assert.False(p.DisableJumpDuringLink); // 0x20000
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Bitfield_ReconstitutesExactly0x1EE0F()
|
||||
{
|
||||
var p = new MovementParameters();
|
||||
|
||||
uint bitfield = 0;
|
||||
if (p.CanWalk) bitfield |= 0x1;
|
||||
if (p.CanRun) bitfield |= 0x2;
|
||||
if (p.CanSidestep) bitfield |= 0x4;
|
||||
if (p.CanWalkBackwards) bitfield |= 0x8;
|
||||
if (p.CanCharge) bitfield |= 0x10;
|
||||
if (p.FailWalk) bitfield |= 0x20;
|
||||
if (p.UseFinalHeading) bitfield |= 0x40;
|
||||
if (p.Sticky) bitfield |= 0x80;
|
||||
if (p.MoveAway) bitfield |= 0x100;
|
||||
if (p.MoveTowards) bitfield |= 0x200;
|
||||
if (p.UseSpheres) bitfield |= 0x400;
|
||||
if (p.SetHoldKey) bitfield |= 0x800;
|
||||
if (p.Autonomous) bitfield |= 0x1000;
|
||||
if (p.ModifyRawState) bitfield |= 0x2000;
|
||||
if (p.ModifyInterpretedState) bitfield |= 0x4000;
|
||||
if (p.CancelMoveTo) bitfield |= 0x8000;
|
||||
if (p.StopCompletelyFlag) bitfield |= 0x10000;
|
||||
if (p.DisableJumpDuringLink) bitfield |= 0x20000;
|
||||
|
||||
Assert.Equal(0x1EE0Fu, bitfield);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ScalarDefaults_MatchRetail()
|
||||
{
|
||||
var p = new MovementParameters();
|
||||
|
||||
Assert.Equal(0f, p.MinDistance);
|
||||
Assert.Equal(0.6f, p.DistanceToObject);
|
||||
Assert.Equal(float.MaxValue, p.FailDistance);
|
||||
Assert.Equal(0f, p.DesiredHeading);
|
||||
Assert.Equal(1f, p.Speed);
|
||||
// ACE-divergence trap: retail is 15.0, NOT ACE's 1.0.
|
||||
Assert.Equal(15f, p.WalkRunThreshhold);
|
||||
Assert.Equal(0u, p.ContextId);
|
||||
Assert.Equal(HoldKey.Invalid, p.HoldKeyToApply);
|
||||
Assert.Equal(0u, p.ActionStamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_CanCharge_DefaultsFalse_NotAceTrue()
|
||||
{
|
||||
// Dedicated regression test for the single highest-risk ACE trap:
|
||||
// ACE's MovementParameters.cs:58 sets CanCharge = true at construction.
|
||||
// Retail's 0x1EE0F has bit 0x10 CLEAR.
|
||||
var p = new MovementParameters();
|
||||
Assert.False(p.CanCharge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_WalkRunThreshhold_Is15_NotAce1()
|
||||
{
|
||||
var p = new MovementParameters();
|
||||
Assert.Equal(15f, p.WalkRunThreshhold);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — action FIFO discipline + <c>ApplyMotion</c>/<c>RemoveMotion</c>
|
||||
/// field effects on <see cref="RawMotionState"/> (closes J2). Oracle:
|
||||
/// docs/research/named-retail/acclient_2013_pseudo_c.txt — verbatim bodies
|
||||
/// quoted in RawMotionState.cs doc comments:
|
||||
/// <c>RawMotionState::AddAction</c> (0x0051e840), <c>RemoveAction</c>
|
||||
/// (0x0051e8a0), <c>ApplyMotion</c> (0x0051eb60), <c>RemoveMotion</c>
|
||||
/// (0x0051e6e0).
|
||||
/// </summary>
|
||||
public sealed class RawMotionStateActionFifoTests
|
||||
{
|
||||
// ── AddAction / RemoveAction / GetNumActions FIFO discipline ──────────
|
||||
|
||||
[Fact]
|
||||
public void AddAction_AppendsInOrder()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
raw.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
|
||||
raw.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
|
||||
|
||||
Assert.Equal(2, raw.Actions.Count);
|
||||
Assert.Equal((ushort)0x004Bu, raw.Actions[0].Command); // widened to ushort on wire, verified below
|
||||
Assert.Equal((ushort)0x0050u, raw.Actions[1].Command);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAction_PopsHeadFirst_FifoOrder()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
raw.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
|
||||
raw.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
|
||||
|
||||
uint first = raw.RemoveAction();
|
||||
uint second = raw.RemoveAction();
|
||||
|
||||
Assert.Equal(0x004Bu, first); // head popped first (FIFO)
|
||||
Assert.Equal(0x0050u, second);
|
||||
Assert.Empty(raw.Actions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAction_Empty_ReturnsZero()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
Assert.Equal(0u, raw.RemoveAction());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAction_StoresSpeedStampAutonomous()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
raw.AddAction(0x1000004Bu, speed: 2.5f, actionStamp: 0x7FFFu, autonomous: true);
|
||||
|
||||
var a = raw.Actions[0];
|
||||
Assert.Equal(2.5f, a.Speed);
|
||||
Assert.Equal((ushort)0x7FFFu, a.Stamp);
|
||||
Assert.True(a.Autonomous);
|
||||
}
|
||||
|
||||
// ── ApplyMotion field effects (0x0051eb60) ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_TurnRight_SetsTurnCommandAndSpeed_HonorsSetHoldKeyBit()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
var p = new MovementParameters { Speed = 1.5f, SetHoldKey = true };
|
||||
|
||||
raw.ApplyMotion(0x6500000du, p); // TurnRight
|
||||
|
||||
Assert.Equal(0x6500000du, raw.TurnCommand);
|
||||
Assert.Equal(1.5f, raw.TurnSpeed);
|
||||
Assert.Equal(HoldKey.Invalid, raw.TurnHoldKey); // SetHoldKey bit set -> Invalid
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_TurnRight_SetHoldKeyClear_UsesHoldKeyToApply()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
var p = new MovementParameters { Speed = 1.5f, SetHoldKey = false, HoldKeyToApply = HoldKey.Run };
|
||||
|
||||
raw.ApplyMotion(0x6500000du, p);
|
||||
|
||||
Assert.Equal(HoldKey.Run, raw.TurnHoldKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_SideStepRight_SetsSidestepCommandAndSpeed()
|
||||
{
|
||||
var raw = new RawMotionState();
|
||||
var p = new MovementParameters { Speed = 1.248f, SetHoldKey = true };
|
||||
|
||||
raw.ApplyMotion(0x6500000fu, p); // SideStepRight
|
||||
|
||||
Assert.Equal(0x6500000fu, raw.SidestepCommand);
|
||||
Assert.Equal(1.248f, raw.SidestepSpeed);
|
||||
Assert.Equal(HoldKey.Invalid, raw.SidestepHoldKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_ForwardClassMotion_SetsForwardCommandAndSpeed()
|
||||
{
|
||||
// WalkForward = 0x45000005 has bit 0x40000000 set (forward-class)
|
||||
// and is NOT 0x44000007 (RunForward) -> the write branch fires.
|
||||
var raw = new RawMotionState();
|
||||
var p = new MovementParameters { Speed = 1.0f, SetHoldKey = true };
|
||||
|
||||
raw.ApplyMotion(0x45000005u, p);
|
||||
|
||||
Assert.Equal(0x45000005u, raw.ForwardCommand);
|
||||
Assert.Equal(1.0f, raw.ForwardSpeed);
|
||||
Assert.Equal(HoldKey.Invalid, raw.ForwardHoldKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_RunForwardExactId_ForwardClassButExcluded_NoWrite()
|
||||
{
|
||||
// Verbatim retail quirk (0x0051eb60): arg2 == 0x44000007 (RunForward)
|
||||
// with the forward-class bit (0x40000000) set falls through BOTH
|
||||
// inner branches — no field write occurs. Port verbatim, not fixed.
|
||||
var raw = new RawMotionState();
|
||||
var before = raw.ForwardCommand;
|
||||
var p = new MovementParameters { Speed = 3.0f };
|
||||
|
||||
raw.ApplyMotion(0x44000007u, p);
|
||||
|
||||
Assert.Equal(before, raw.ForwardCommand); // untouched
|
||||
Assert.Equal(1.0f, raw.ForwardSpeed); // untouched (still ctor default)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_StyleClassMotion_SetsCurrentStyleAndResetsForwardToReady()
|
||||
{
|
||||
// High bit set (>= 0x80000000) and current_style differs -> style branch.
|
||||
var raw = new RawMotionState { ForwardCommand = 0x45000005u };
|
||||
var p = new MovementParameters();
|
||||
|
||||
raw.ApplyMotion(0x80000042u, p);
|
||||
|
||||
Assert.Equal(0x41000003u, raw.ForwardCommand); // reset to Ready
|
||||
Assert.Equal(0x80000042u, raw.CurrentStyle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_StyleClassMotion_SameAsCurrentStyle_NoOp()
|
||||
{
|
||||
var raw = new RawMotionState { CurrentStyle = 0x80000042u, ForwardCommand = 0x45000005u };
|
||||
var p = new MovementParameters();
|
||||
|
||||
raw.ApplyMotion(0x80000042u, p);
|
||||
|
||||
// current_style already equals arg2 -> the style branch's condition
|
||||
// (current_style != arg2) is false, so forward_command is untouched.
|
||||
Assert.Equal(0x45000005u, raw.ForwardCommand);
|
||||
Assert.Equal(0x80000042u, raw.CurrentStyle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMotion_ActionClassMotion_AddsAction()
|
||||
{
|
||||
// Outside turn/sidestep range, bit 0x40000000 clear, arg2 >= 0
|
||||
// (not style-class), bit 0x10000000 set -> AddAction.
|
||||
var raw = new RawMotionState();
|
||||
var p = new MovementParameters { Speed = 1.0f, ActionStamp = 42u, Autonomous = true };
|
||||
|
||||
raw.ApplyMotion(0x1000004Bu, p); // Jumpup action id
|
||||
|
||||
Assert.Single(raw.Actions);
|
||||
var a = raw.Actions[0];
|
||||
Assert.Equal((ushort)0x004Bu, a.Command);
|
||||
Assert.Equal(1.0f, a.Speed);
|
||||
Assert.Equal((ushort)42u, a.Stamp);
|
||||
Assert.True(a.Autonomous);
|
||||
}
|
||||
|
||||
// ── RemoveMotion field effects (0x0051e6e0) ────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_TurnRange_ClearsTurnCommand()
|
||||
{
|
||||
var raw = new RawMotionState { TurnCommand = 0x6500000du };
|
||||
raw.RemoveMotion(0x6500000du);
|
||||
Assert.Equal(0u, raw.TurnCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_TurnLeftRange_ClearsTurnCommand()
|
||||
{
|
||||
var raw = new RawMotionState { TurnCommand = 0x6500000eu };
|
||||
raw.RemoveMotion(0x6500000eu);
|
||||
Assert.Equal(0u, raw.TurnCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_SidestepRange_ClearsSidestepCommand()
|
||||
{
|
||||
var raw = new RawMotionState { SidestepCommand = 0x6500000fu };
|
||||
raw.RemoveMotion(0x6500000fu);
|
||||
Assert.Equal(0u, raw.SidestepCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_ForwardClassMotion_MatchingCommand_ResetsToReady()
|
||||
{
|
||||
var raw = new RawMotionState { ForwardCommand = 0x45000005u, ForwardSpeed = 3.0f };
|
||||
raw.RemoveMotion(0x45000005u);
|
||||
|
||||
Assert.Equal(0x41000003u, raw.ForwardCommand);
|
||||
Assert.Equal(1f, raw.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_ForwardClassMotion_NonMatchingCommand_NoOp()
|
||||
{
|
||||
var raw = new RawMotionState { ForwardCommand = 0x45000005u, ForwardSpeed = 3.0f };
|
||||
raw.RemoveMotion(0x44000007u); // different forward-class id
|
||||
|
||||
Assert.Equal(0x45000005u, raw.ForwardCommand); // untouched
|
||||
Assert.Equal(3.0f, raw.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_StyleClassMotion_MatchingCurrentStyle_ResetsToNonCombat()
|
||||
{
|
||||
var raw = new RawMotionState { CurrentStyle = 0x80000042u };
|
||||
raw.RemoveMotion(0x80000042u);
|
||||
|
||||
Assert.Equal(0x8000003du, raw.CurrentStyle); // reset to NonCombat
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveMotion_StyleClassMotion_NonMatchingCurrentStyle_NoOp()
|
||||
{
|
||||
var raw = new RawMotionState { CurrentStyle = 0x80000042u };
|
||||
raw.RemoveMotion(0x80000099u); // different style id
|
||||
|
||||
Assert.Equal(0x80000042u, raw.CurrentStyle); // untouched
|
||||
}
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ public sealed class MotionInterpreterTests
|
|||
{
|
||||
var interp = MakeInterp();
|
||||
interp.RawState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.RawState.SideStepCommand = MotionCommand.SideStepRight;
|
||||
interp.RawState.SidestepCommand = MotionCommand.SideStepRight;
|
||||
interp.RawState.TurnCommand = MotionCommand.TurnRight;
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
interp.InterpretedState.TurnCommand = MotionCommand.TurnLeft;
|
||||
|
|
@ -186,7 +186,7 @@ public sealed class MotionInterpreterTests
|
|||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand);
|
||||
Assert.Equal(1.0f, interp.RawState.ForwardSpeed, precision: 5);
|
||||
Assert.Equal(0u, interp.RawState.SideStepCommand);
|
||||
Assert.Equal(0u, interp.RawState.SidestepCommand);
|
||||
Assert.Equal(0u, interp.RawState.TurnCommand);
|
||||
Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand);
|
||||
Assert.Equal(1.0f, interp.InterpretedState.ForwardSpeed, precision: 5);
|
||||
|
|
@ -440,14 +440,16 @@ public sealed class MotionInterpreterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_Airborne_ReturnsYouCantJumpWhileInTheAir()
|
||||
public void Jump_Airborne_ReturnsNotGrounded()
|
||||
{
|
||||
// R3-W1 renumber (A10): airborne is 0x24 (NotGrounded), not 0x48
|
||||
// (0x48 is now reserved for the motion_allows_jump blocklist).
|
||||
var body = MakeAirborne();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -586,13 +588,13 @@ public sealed class MotionInterpreterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_Airborne_ReturnsYouCantJumpWhileInTheAir()
|
||||
public void JumpIsAllowed_Airborne_ReturnsNotGrounded()
|
||||
{
|
||||
var interp = MakeInterp(MakeAirborne());
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -607,7 +609,7 @@ public sealed class MotionInterpreterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NoGravityFlag_ReturnsYouCantJumpWhileInTheAir()
|
||||
public void JumpIsAllowed_NoGravityFlag_ReturnsNotGrounded()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
|
|
@ -619,17 +621,20 @@ public sealed class MotionInterpreterTests
|
|||
// No gravity → must be airborne-style
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NullPhysicsObj_ReturnsGeneralFailure()
|
||||
public void JumpIsAllowed_NullPhysicsObj_ReturnsNotGrounded()
|
||||
{
|
||||
// A10 note: jump_is_allowed returns 0x24 (NotGrounded), NOT 8, when
|
||||
// physics_obj == null — the "8 = no physics obj" convention that
|
||||
// holds everywhere else in CMotionInterp does not hold here.
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure, result);
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W1 — pins <see cref="WeenieError"/>'s numeric values against the
|
||||
/// definitive retail table in
|
||||
/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A10 (an exhaustive
|
||||
/// sweep of every <c>return <code></c> site across raw 304908-306277 +
|
||||
/// 300150-300540: 19 return sites + 1 store site, all attributed). These
|
||||
/// codes are local-only (never on the wire), so the renumber from the
|
||||
/// pre-R3 shuffled names is a safe rename — this test is the single
|
||||
/// source of truth that a future edit can't silently un-pin.
|
||||
/// </summary>
|
||||
public sealed class WeenieErrorCodeTableTests
|
||||
{
|
||||
[Fact]
|
||||
public void None_Is0x00()
|
||||
=> Assert.Equal(0x00u, (uint)WeenieError.None);
|
||||
|
||||
[Fact]
|
||||
public void NoPhysicsObject_Is0x08()
|
||||
=> Assert.Equal(0x08u, (uint)WeenieError.NoPhysicsObject);
|
||||
|
||||
[Fact]
|
||||
public void NotGrounded_Is0x24()
|
||||
=> Assert.Equal(0x24u, (uint)WeenieError.NotGrounded);
|
||||
|
||||
[Fact]
|
||||
public void CrouchInCombatStance_Is0x3f()
|
||||
=> Assert.Equal(0x3fu, (uint)WeenieError.CrouchInCombatStance);
|
||||
|
||||
[Fact]
|
||||
public void SitInCombatStance_Is0x40()
|
||||
=> Assert.Equal(0x40u, (uint)WeenieError.SitInCombatStance);
|
||||
|
||||
[Fact]
|
||||
public void SleepInCombatStance_Is0x41()
|
||||
=> Assert.Equal(0x41u, (uint)WeenieError.SleepInCombatStance);
|
||||
|
||||
[Fact]
|
||||
public void ChatEmoteOutsideNonCombat_Is0x42()
|
||||
=> Assert.Equal(0x42u, (uint)WeenieError.ChatEmoteOutsideNonCombat);
|
||||
|
||||
[Fact]
|
||||
public void ActionDepthExceeded_Is0x45()
|
||||
=> Assert.Equal(0x45u, (uint)WeenieError.ActionDepthExceeded);
|
||||
|
||||
[Fact]
|
||||
public void GeneralMovementFailure_Is0x47()
|
||||
=> Assert.Equal(0x47u, (uint)WeenieError.GeneralMovementFailure);
|
||||
|
||||
[Fact]
|
||||
public void YouCantJumpFromThisPosition_Is0x48()
|
||||
=> Assert.Equal(0x48u, (uint)WeenieError.YouCantJumpFromThisPosition);
|
||||
|
||||
[Fact]
|
||||
public void CantJumpLoadedDown_Is0x49()
|
||||
=> Assert.Equal(0x49u, (uint)WeenieError.CantJumpLoadedDown);
|
||||
|
||||
/// <summary>
|
||||
/// Every code in the A10 table in one pass — guards against a
|
||||
/// future partial edit desyncing an individual test above from the
|
||||
/// enum declaration.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(WeenieError.None, 0x00u)]
|
||||
[InlineData(WeenieError.NoPhysicsObject, 0x08u)]
|
||||
[InlineData(WeenieError.NotGrounded, 0x24u)]
|
||||
[InlineData(WeenieError.CrouchInCombatStance, 0x3fu)]
|
||||
[InlineData(WeenieError.SitInCombatStance, 0x40u)]
|
||||
[InlineData(WeenieError.SleepInCombatStance, 0x41u)]
|
||||
[InlineData(WeenieError.ChatEmoteOutsideNonCombat, 0x42u)]
|
||||
[InlineData(WeenieError.ActionDepthExceeded, 0x45u)]
|
||||
[InlineData(WeenieError.GeneralMovementFailure, 0x47u)]
|
||||
[InlineData(WeenieError.YouCantJumpFromThisPosition, 0x48u)]
|
||||
[InlineData(WeenieError.CantJumpLoadedDown, 0x49u)]
|
||||
public void A10Table_EveryCode_MatchesRetailNumericValue(WeenieError code, uint expected)
|
||||
=> Assert.Equal(expected, (uint)code);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue