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>
322 lines
13 KiB
C#
322 lines
13 KiB
C#
using AcDream.Core.Physics.Motion;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// Retail hold-key enum (<c>acclient.h</c> <c>enum HoldKey</c>, line 3394).
|
|
/// </summary>
|
|
public enum HoldKey : uint
|
|
{
|
|
Invalid = 0x0,
|
|
None = 0x1,
|
|
Run = 0x2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// One entry in <see cref="RawMotionState.Actions"/> — a discrete motion
|
|
/// event (jump, attack, etc.) layered on top of the continuous
|
|
/// forward/sidestep/turn axes.
|
|
///
|
|
/// <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>
|
|
/// 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 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
|
|
{
|
|
/// <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 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
|
|
/// (<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 => _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). 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;
|
|
}
|
|
}
|
|
}
|