feat(R2-Q2): verbatim CMotionTable — the GetObjectSequence dispatcher

CMotionTable (Core/Physics/Motion, 692 lines) wrapping the dat
MotionTable, per r2-motiontable-decomp.md + Q0 pins:

- GetObjectSequence (0x00522860): entry guards; modifier-class no-op
  fast path; Branch 1 style-change (exit link + style link +
  default_style double-hop + re_modify); Branch 2 cycle (UNCONDITIONAL
  default-style retry per label_522ae6, is_allowed gate, same-substate
  re-speed fast path = ChangeCycleSpeed + SubtractMotion(old) +
  CombineMotion(new), clear-modifiers bit0, direct-link vs !SameSign
  double-hop, A2 signedSpeed, outgoing-modifier re-registration,
  re_modify, outTicks per A3); Branch 3 action (direct link OR the
  4-layer out-and-back with the base cycle re-added at the OLD
  substate_mod; outTicks WITHOUT ACE's double-count, A4-#1); Branch 4
  modifier (PHYSICS-ONLY CombineMotion + AddModifier stop-then-re-add —
  the AP-73 retirement mechanism).
- get_link (A1 pin: either-negative -> swapped keys — the adapter's
  field-validated port re-homed), is_allowed (Bitfield & 2; A5
  CONFIRMED on DatReaderWriter 2.1.7), re_modify (deep-copy snapshot
  termination bound), StopSequenceMotion, SetDefaultState,
  DoObjectMotion/StopObjectMotion/StopObjectCompletely (A4-#4 return).
- Free functions: AddMotion (UNCONDITIONAL velocity/omega set — the
  G17 core), CombineMotion/SubtractMotion (physics-only),
  ChangeCycleSpeed (verbatim incl. the A4-#2 retail gap), SameSign.

44 conformance tests pinning H1/H4/H5/H7/H8/H10-H14 + all Q0 pins,
incl. the run-while-turning gated-cycle -> Branch-4 physics-only test
(retail's actual turn-blend mechanism) and missing-cycle ->
sequence-untouched (retires the HasCycle fallback rationale).

Implemented by a dedicated agent against the Q2 spec; diff + key
branches reviewed, suite re-verified (3934 green) before commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 20:48:26 +02:00
parent 2345da30e9
commit 98f58db913
2 changed files with 1737 additions and 0 deletions

View file

@ -0,0 +1,692 @@
using System;
using System.Linq;
using System.Numerics;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
// Alias the DatReaderWriter enum so it doesn't clash with
// AcDream.Core.Physics.MotionCommand (a static class of uint constants) —
// same convention as AnimationSequencerTests.cs.
using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand;
namespace AcDream.Core.Physics.Motion;
// ─────────────────────────────────────────────────────────────────────────────
// R2-Q2 — verbatim port of retail's CMotionTable (the motion-selection
// dispatcher). Oracle: docs/research/2026-07-02-r2-motiontable/r2-motiontable-decomp.md
// (all §-references below point there); ambiguity pins:
// docs/research/2026-07-02-r2-motiontable/Q0-pins.md (A1-A5).
//
// Scope (r2-port-plan.md §3 Q2): pure motion-SELECTION logic — resolves which
// MotionData(s) to append to a CSequence and updates a MotionState in place.
// Does NOT own a pending-animation queue (that is MotionTableManager, Q3) and
// does NOT drive per-tick advance (CSequence.Update, R1). This file re-homes
// the adapter's already-field-validated GetLink (AnimationSequencer.cs
// :953-997) verbatim per the keep-list (r2-port-plan.md §2) and adds every
// other CMotionTable/free-function member the decomp documents.
//
// Command-word class bits (decomp §1 "Motion-id class bits", §15):
// 0x80000000 style-class (top bit set; the low 31 bits + top bit read as
// a NEGATIVE int32 — retail's `(int32_t)ebx_1 < 0` test)
// 0x40000000 cycle-class (cyclic/looping base state, `this->cycles`)
// 0x20000000 modifier-class (physics-only overlay, `this->modifiers`)
// 0x10000000 action-class (one-shot, `add_action` FIFO)
// 0x41000003 Ready — the "stop / default state" sentinel (decomp §15)
//
// DatReaderWriter.DBObjs.MotionTable field map (verified via reflection,
// package Chorizite.DatReaderWriter 2.1.7, 2026-07-02):
// DefaultStyle : MotionCommand → this->default_style
// StyleDefaults : Dictionary<MotionCommand,MotionCommand> → this->style_defaults
// Cycles : Dictionary<int,MotionData> → this->cycles
// Modifiers : Dictionary<int,MotionData> → this->modifiers
// Links : Dictionary<int,MotionCommandData> → this->links
// (MotionCommandData.MotionData : Dictionary<int,MotionData> = the inner
// per-target-substate hash)
// MotionData.Bitfield : byte (A5 CONFIRMED present — bit0=clear-mods-on-
// entry, bit1=substate-gated for is_allowed)
// MotionData.Anims : List<AnimData> — retail's `num_anims`/`anims[]` pair;
// Anims.Count IS num_anims (A3 — no separate packed-byte field exists on
// the managed type; the decomp's "packed byte at MotionData+0x10" is
// this same count, just read through the raw struct layout).
// ─────────────────────────────────────────────────────────────────────────────
/// <summary>
/// Retail's <c>CMotionTable</c> (0x00522xxx-0x00523xxx region) — the
/// motion-selection dispatcher. Wraps a loaded <see cref="MotionTable"/> DBObj
/// and resolves <c>(style, substate, speed)</c> requests into MotionData
/// chains appended to a <see cref="CSequence"/>, mutating a
/// <see cref="MotionState"/> in place.
/// </summary>
public sealed class CMotionTable
{
private readonly MotionTable _table;
public CMotionTable(MotionTable table)
{
ArgumentNullException.ThrowIfNull(table);
_table = table;
}
// ── free functions (decomp §2) ──────────────────────────────────────
/// <summary>
/// <c>same_sign</c> 0x00522260 (@298253). True when <paramref name="a"/>
/// and <paramref name="b"/> are on the same side of zero (0 counts as
/// non-negative, matching the decomp's <c>&gt;= 0f</c> reading).
/// </summary>
internal static bool SameSign(float a, float b) => (a >= 0f) == (b >= 0f);
/// <summary>
/// <c>change_cycle_speed</c> 0x00522290 (@298276): rescale
/// <paramref name="sequence"/>'s cyclic-tail framerate by
/// <c>newSpeed/oldSpeed</c>. Ported VERBATIM including the A4-#2 gap: when
/// <paramref name="oldSpeed"/> is ~0 and <paramref name="newSpeed"/> is
/// NOT ~0, retail's fabsl branch structure suppresses the rescale
/// entirely (no zeroing, no scaling — a silent no-op). Only when BOTH are
/// ~0 does retail explicitly zero the framerate.
/// </summary>
internal static void ChangeCycleSpeed(CSequence sequence, MotionData? cyclic, float oldSpeed, float newSpeed)
{
const float Epsilon = 0.000199999995f; // ~0.0002f, verbatim retail constant
if (MathF.Abs(oldSpeed) > Epsilon)
{
sequence.MultiplyCyclicAnimationFramerate(newSpeed / oldSpeed);
return;
}
// oldSpeed ~ 0: only the "newSpeed also ~0" leg does anything (zero
// the framerate). The "newSpeed NOT ~0" leg is a silent no-op —
// retail's own gap, kept verbatim (Q0-pins A4-#2).
if (MathF.Abs(newSpeed) <= Epsilon)
{
sequence.MultiplyCyclicAnimationFramerate(0f);
}
}
/// <summary>
/// <c>add_motion</c> 0x005224b0 (@298437): UNCONDITIONALLY sets
/// <paramref name="sequence"/>'s velocity/omega to
/// <c>motion.Velocity/Omega * speedMod</c> (replace, not accumulate — a
/// dat-silent MotionData carries a zero Vector3, so "unconditional
/// replace" and "replace with zero" are the same call; G17 core), then
/// appends every AnimData in <paramref name="motion"/> speed-scaled
/// (framerate only, retail <c>AnimData::operator*</c>).
/// </summary>
internal static void AddMotion(CSequence sequence, MotionData? motion, float speedMod)
{
if (motion is null)
return;
sequence.SetVelocity(motion.Velocity * speedMod);
sequence.SetOmega(motion.Omega * speedMod);
foreach (var ad in motion.Anims)
{
sequence.AppendAnimation(new AnimData
{
AnimId = ad.AnimId,
LowFrame = ad.LowFrame,
HighFrame = ad.HighFrame,
Framerate = ad.Framerate * speedMod,
});
}
}
/// <summary>
/// <c>combine_motion</c> 0x00522580 (@298472): ADDS
/// <paramref name="motion"/>'s velocity/omega (scaled by
/// <paramref name="speedMod"/>) onto the sequence's existing physics.
/// Never touches <c>anims</c> — used for modifier overlays where the base
/// cycle's animation frames stay untouched.
/// </summary>
internal static void CombineMotion(CSequence sequence, MotionData? motion, float speedMod)
{
if (motion is null)
return;
sequence.CombinePhysics(motion.Velocity * speedMod, motion.Omega * speedMod);
}
/// <summary>
/// <c>subtract_motion</c> 0x00522600 (@298492): inverse of
/// <see cref="CombineMotion"/> — used when REMOVING a modifier's
/// physics contribution.
/// </summary>
internal static void SubtractMotion(CSequence sequence, MotionData? motion, float speedMod)
{
if (motion is null)
return;
sequence.SubtractPhysics(motion.Velocity * speedMod, motion.Omega * speedMod);
}
// ── members ──────────────────────────────────────────────────────────
/// <summary>
/// <c>is_allowed</c> 0x005226c0 (@298526): a bitfield-bit1 ("gated")
/// cycle is only reusable when the current substate matches the
/// candidate, OR the current substate already equals the owning style's
/// default substate. Null <paramref name="candidate"/> is never allowed.
/// </summary>
public bool IsAllowed(uint candidateSubstate, MotionData? candidate, MotionState state)
{
if (candidate is null)
return false;
if ((candidate.Bitfield & 2) != 0)
{
uint substate = state.Substate;
if (candidateSubstate != substate)
{
uint defaultSubstate = LookupStyleDefault(state.Style);
return defaultSubstate == substate;
}
}
return true;
}
/// <summary>
/// <c>get_link</c> 0x00522710 (@298552). Re-homed verbatim from the
/// working adapter (<c>AnimationSequencer.GetLink</c>, field-validated —
/// the reversed-key branch fixed the Ready→WalkBackward "left leg
/// twitches" glitch) per Q0-pins A1: EITHER speed negative routes through
/// the swapped-key branch (link stored FROM <paramref name="toSubstate"/>
/// TO <paramref name="fromSubstate"/>), falling back to the style's
/// default-substate hop; otherwise the forward branch (link FROM
/// <paramref name="fromSubstate"/> TO <paramref name="toSubstate"/>),
/// falling back to the style-level catch-all (unstyled outer key).
/// </summary>
public MotionData? GetLink(uint fromStyle, uint fromSubstate, float fromSubstateMod, uint toSubstate, float toSubstateMod)
{
if (toSubstateMod < 0f || fromSubstateMod < 0f)
{
// Reversed-direction path: link FROM toSubstate TO fromSubstate.
int reversedKey = (int)((fromStyle << 16) | (toSubstate & 0xFFFFFFu));
if (_table.Links.TryGetValue(reversedKey, out var revLink)
&& revLink.MotionData.TryGetValue((int)fromSubstate, out var revResult))
{
return revResult;
}
// Style-defaults fallback (decomp §4 second predicate block, "else" branch).
uint defaultSubstate = LookupStyleDefault(fromStyle);
int subKey = (int)((fromStyle << 16) | (fromSubstate & 0xFFFFFFu));
if (_table.Links.TryGetValue(subKey, out var subLink)
&& subLink.MotionData.TryGetValue((int)defaultSubstate, out var subResult))
{
return subResult;
}
return null;
}
// Forward-direction path: link FROM fromSubstate TO toSubstate.
int outerKey1 = (int)((fromStyle << 16) | (fromSubstate & 0xFFFFFFu));
if (_table.Links.TryGetValue(outerKey1, out var cmd1)
&& cmd1.MotionData.TryGetValue((int)toSubstate, out var result1))
{
return result1;
}
// Fallback: style-level catch-all (unstyled outer key, decomp §4 "else if" branch).
int outerKey2 = (int)(fromStyle << 16);
if (_table.Links.TryGetValue(outerKey2, out var cmd2)
&& cmd2.MotionData.TryGetValue((int)toSubstate, out var result2))
{
return result2;
}
return null;
}
/// <summary>
/// <c>GetObjectSequence</c> 0x00522860 (@298636) — the FULL dispatcher.
/// See decomp §5 for the annotated retail source; the branch structure
/// below mirrors it 1:1 (comments cite the corresponding decomp block).
/// </summary>
/// <param name="motion">Requested target substate/motion id.</param>
/// <param name="state">In/out current motion state.</param>
/// <param name="sequence">Sequence to build/mutate.</param>
/// <param name="speed">Requested speed_mod for the new substate.</param>
/// <param name="outTicks">OUT: tick count of appended animation, minus 1.</param>
/// <param name="stopCall">false = normal "do motion"; true = re-invoked
/// from <see cref="StopSequenceMotion"/>.</param>
public bool GetObjectSequence(uint motion, MotionState state, CSequence sequence, float speed, out uint outTicks, bool stopCall)
{
outTicks = 0;
uint style = state.Style;
if (style == 0)
return false;
uint substate = state.Substate;
if (substate == 0)
return false;
uint styleDefault = LookupStyleDefault(style); // var_c
uint target = motion; // ebx_1 working copy
// ---- FAST PATH: requesting the style default while already a
// modifier-class substate, not a stop-call. ----
if (target == styleDefault && !stopCall && (substate & 0x20000000) != 0)
return true;
float requestedSpeed = speed; // ebp_1
// ================================================================
// BRANCH 1: target interpreted as NEGATIVE int32 — style-change request.
// ================================================================
if ((int)target < 0)
{
if (style == target)
return true; // already in that style
uint currentStyleDefault = LookupStyleDefault(style); // eax_1
MotionData? exitLink = null; // var_4_1
if (substate != currentStyleDefault)
{
exitLink = GetLink(style, substate, state.SubstateMod, currentStyleDefault, requestedSpeed);
}
uint targetStyleDefault = LookupStyleDefault(target); // arg7_style_default
if (_table.StyleDefaults.ContainsKey((DRWMotionCommand)target))
{
MotionData? newCycle = LookupCycle(target, targetStyleDefault); // eax_5
if (newCycle is not null)
{
if ((newCycle.Bitfield & 1) != 0)
state.ClearModifiers();
// link FROM current style's default substate TO target style's default substate
MotionData? directOrHop1 = GetLink(style, styleDefault, state.SubstateMod, target, requestedSpeed); // arg2
MotionData? hop2 = null; // var_10_1
if (directOrHop1 is null && target != style)
{
// DOUBLE-HOP VIA default_style.
directOrHop1 = GetLink(style, styleDefault, 1f, (uint)_table.DefaultStyle, 1f);
uint defaultStyleDefaultSubstate = LookupStyleDefault((uint)_table.DefaultStyle);
hop2 = GetLink((uint)_table.DefaultStyle, defaultStyleDefaultSubstate, 1f, target, 1f);
}
sequence.ClearPhysics();
sequence.RemoveCyclicAnims();
AddMotion(sequence, exitLink, requestedSpeed);
AddMotion(sequence, directOrHop1, requestedSpeed);
AddMotion(sequence, hop2, requestedSpeed);
AddMotion(sequence, newCycle, requestedSpeed);
state.Substate = targetStyleDefault;
state.Style = target;
state.SubstateMod = speed;
ReModify(sequence, state);
uint numAnims2 = (uint)(exitLink?.Anims.Count ?? 0);
uint ecx20 = (uint)(directOrHop1?.Anims.Count ?? 0);
uint numAnims1 = (uint)(hop2?.Anims.Count ?? 0);
outTicks = (uint)newCycle.Anims.Count + numAnims1 + ecx20 + numAnims2 - 1;
return true;
}
}
// else: fall through to the class-bit blocks below with target still negative.
}
// ================================================================
// BRANCH 2: target has the CYCLE-CLASS bit (0x40000000) set.
// ================================================================
if ((target & 0x40000000) != 0)
{
uint substateId = target & 0xFFFFFFu;
MotionData? cyclic = LookupCycle(style, substateId); // eax_24
if (cyclic is null)
{
// No cycle for THIS style at that id -> retry under the
// table-wide default_style (decomp §5 "label_522ae6" retry —
// unconditional, no style==0 guard in retail).
cyclic = LookupCycle((uint)_table.DefaultStyle, substateId);
}
if (cyclic is not null && IsAllowed(target, cyclic, state))
{
// ---- FAST RE-SPEED PATH ----
if (target == substate
&& SameSign(requestedSpeed, state.SubstateMod)
&& sequence.HasAnims())
{
ChangeCycleSpeed(sequence, cyclic, state.SubstateMod, requestedSpeed);
SubtractMotion(sequence, cyclic, state.SubstateMod);
CombineMotion(sequence, cyclic, requestedSpeed);
state.SubstateMod = speed;
return true;
}
if ((cyclic.Bitfield & 1) != 0)
state.ClearModifiers();
MotionData? directLink = GetLink(style, substate, state.SubstateMod, target, requestedSpeed); // eax_34
bool sameSignDirect = directLink is not null && SameSign(requestedSpeed, state.SubstateMod);
MotionData? hop2 = null; // var_10_1
MotionData? linkOrHop1 = directLink; // arg2
if (directLink is null || !sameSignDirect)
{
uint styleDefaultSubstate = LookupStyleDefault(style);
linkOrHop1 = GetLink(style, substate, state.SubstateMod, styleDefaultSubstate, 1f);
hop2 = GetLink(style, styleDefaultSubstate, 1f, target, requestedSpeed);
}
sequence.ClearPhysics();
sequence.RemoveCyclicAnims();
if (hop2 is null)
{
float signedSpeed = (state.SubstateMod == 0f || SameSign(state.SubstateMod, speed))
? speed : -speed;
AddMotion(sequence, linkOrHop1, signedSpeed);
}
else
{
AddMotion(sequence, linkOrHop1, state.SubstateMod);
AddMotion(sequence, hop2, requestedSpeed);
}
AddMotion(sequence, cyclic, requestedSpeed);
// Leaving a modifier-class substate for something else: re-register
// the OLD substate as a still-active modifier unless the style's
// default substate IS the new target.
uint oldSubstate = state.Substate;
if (oldSubstate != target && (oldSubstate & 0x20000000) != 0)
{
uint styleDefaultSubstate2 = LookupStyleDefault(style);
if (styleDefaultSubstate2 != target)
state.AddModifierNoCheck(oldSubstate, state.SubstateMod);
}
state.SubstateMod = speed;
state.Substate = target;
ReModify(sequence, state);
uint ecx45 = (uint)(linkOrHop1?.Anims.Count ?? 0);
uint numAnims1b = (uint)(hop2?.Anims.Count ?? 0);
outTicks = (uint)cyclic.Anims.Count + numAnims1b + ecx45 - 1;
return true;
}
// is_allowed rejected (or no cyclic resolved) -> fall through.
}
// ================================================================
// BRANCH 3: target has the ACTION-CLASS bit (0x10000000) set.
// ================================================================
if ((target & 0x10000000) != 0)
{
MotionData? baseCycle = LookupCycle(style, substate & 0xFFFFFFu); // eax_57
if (baseCycle is not null)
{
MotionData? directLink = GetLink(style, substate, state.SubstateMod, target, requestedSpeed); // eax_60
if (directLink is not null)
{
state.AddAction(target, requestedSpeed);
sequence.ClearPhysics();
sequence.RemoveCyclicAnims();
AddMotion(sequence, directLink, requestedSpeed);
AddMotion(sequence, baseCycle, state.SubstateMod);
ReModify(sequence, state);
outTicks = (uint)directLink.Anims.Count;
return true;
}
// No direct link -> route through the style default (double-hop out-and-back).
uint styleDefaultSubstate = LookupStyleDefault(style);
MotionData? outHop = GetLink(style, substate, state.SubstateMod, styleDefaultSubstate, 1f); // eax_66
if (outHop is not null)
{
MotionData? actionLink = GetLink(style, styleDefaultSubstate, 1f, target, requestedSpeed); // eax_68
if (actionLink is not null)
{
MotionData? baseCycleRefetch = LookupCycle(style, substate & 0xFFFFFFu); // eax_69 (same key, re-fetched)
if (baseCycleRefetch is not null)
{
MotionData? returnHop = GetLink(style, styleDefaultSubstate, 1f, substate, state.SubstateMod);
state.AddAction(target, requestedSpeed);
sequence.ClearPhysics();
sequence.RemoveCyclicAnims();
AddMotion(sequence, outHop, 1f);
AddMotion(sequence, actionLink, requestedSpeed);
AddMotion(sequence, returnHop, 1f);
AddMotion(sequence, baseCycleRefetch, state.SubstateMod);
ReModify(sequence, state);
// A4-#1: outTicks = outHop + actionLink [+ returnHop] ONLY —
// never the base cycle, never double-counted (ACE's bug, not retail's).
uint ticks = (uint)outHop.Anims.Count + (uint)actionLink.Anims.Count;
if (returnHop is not null)
ticks += (uint)returnHop.Anims.Count;
outTicks = ticks;
return true;
}
}
}
}
}
// ================================================================
// BRANCH 4: target has the MODIFIER-CLASS bit (0x20000000) set.
// ================================================================
if ((target & 0x20000000) != 0)
{
MotionData? baseCycle = LookupCycle(style, substate & 0xFFFFFFu); // eax_81
if (baseCycle is not null && (baseCycle.Bitfield & 1) == 0)
{
uint modKey = target & 0xFFFFFFu;
MotionData? modifierStyled = LookupModifier(style, modKey, styleSpecific: true); // eax_85
MotionData? modifierGlobal = null; // eax_87
if (modifierStyled is null)
modifierGlobal = LookupModifier(style, modKey, styleSpecific: false);
MotionData? modifierData = modifierStyled ?? modifierGlobal;
if (modifierStyled is not null || modifierGlobal is not null)
{
bool added = state.AddModifier(target, requestedSpeed);
if (!added)
{
// Toggle: already active (or == current substate) -> stop then re-add.
StopSequenceMotion(target, 1f, state, sequence, out _);
added = state.AddModifier(target, requestedSpeed);
}
if (added)
{
CombineMotion(sequence, modifierData, requestedSpeed);
return true;
}
}
}
}
return false;
}
/// <summary>
/// <c>re_modify</c> 0x005222e0 (@298300): after installing a new base
/// cycle/substate, replay every previously-active modifier back onto the
/// sequence by re-invoking <see cref="GetObjectSequence"/> for each one.
/// Q0-pins A4-#5: the retail copy-ctor snapshot exists ONLY as a
/// loop-termination bound (both the live list and the snapshot pop one
/// entry per iteration; the loop ends when the snapshot empties) — the
/// C# port deep-copies via <c>MotionState(MotionState other)</c>, pops
/// both in lockstep, and terminates on the snapshot's emptiness.
/// </summary>
public void ReModify(CSequence sequence, MotionState state)
{
if (!state.Modifiers.Any())
return;
var snapshot = new MotionState(state);
do
{
var head = state.Modifiers.First();
uint motion = head.Motion;
float speedMod = head.SpeedMod;
state.RemoveModifier(head);
var snapshotHead = snapshot.Modifiers.First();
snapshot.RemoveModifier(snapshotHead);
GetObjectSequence(motion, state, sequence, speedMod, out _, stopCall: false);
} while (snapshot.Modifiers.Any());
}
/// <summary>
/// <c>StopSequenceMotion</c> 0x00522fc0 (@298954): two independent stop
/// mechanisms. Stopping the active cycle re-drives
/// <see cref="GetObjectSequence"/> toward the style's default substate
/// (i.e. "return to idle"). Stopping a modifier just unwinds its physics
/// contribution and removes it from the chain — no animation
/// re-sequencing needed since modifiers don't touch <c>anims[]</c>.
/// Action-class ids are NOT handled here (decomp §7 note — actions
/// complete via the MotionTableManager tick-countdown, Q3 scope).
/// </summary>
public bool StopSequenceMotion(uint motion, float speed, MotionState state, CSequence sequence, out uint outTicks)
{
outTicks = 0;
// Case A: stopping the CYCLE-class substate we are currently in.
if ((motion & 0x40000000) != 0 && motion == state.Substate)
{
uint styleDefaultSubstate = LookupStyleDefault(state.Style);
GetObjectSequence(styleDefaultSubstate, state, sequence, 1f, out outTicks, stopCall: true);
return true;
}
// Case B: stopping a MODIFIER-class id.
if ((motion & 0x20000000) != 0)
{
foreach (var node in state.Modifiers)
{
if (node.Motion != motion)
continue;
uint modKey = motion & 0xFFFFFFu;
MotionData? modData = LookupModifier(state.Style, modKey, styleSpecific: true);
modData ??= LookupModifier(state.Style, modKey, styleSpecific: false);
if (modData is not null)
{
SubtractMotion(sequence, modData, node.SpeedMod);
state.RemoveModifier(node);
return true;
}
break; // matching motion id found but no MotionData anywhere -> give up
}
}
return false;
}
/// <summary>
/// <c>SetDefaultState</c> 0x005230a0 (@299004): reset a MotionState to
/// the motion table's baseline (<c>default_style</c> + that style's
/// default substate), clearing all modifiers/actions and installing the
/// base cyclic animation for that (style,substate) fresh via
/// <see cref="CSequence.ClearAnimations"/> (hard reset, not just
/// <c>RemoveCyclicAnims</c>).
/// </summary>
public bool SetDefaultState(MotionState state, CSequence sequence, out uint outTicks)
{
outTicks = 0;
if (!_table.StyleDefaults.TryGetValue(_table.DefaultStyle, out var defaultSubstateCmd))
return false;
uint defaultSubstate = (uint)defaultSubstateCmd;
state.ClearModifiers();
state.ClearActions();
MotionData? cyclic = LookupCycle((uint)_table.DefaultStyle, defaultSubstate);
if (cyclic is null)
return false;
state.Style = (uint)_table.DefaultStyle;
state.Substate = defaultSubstate;
state.SubstateMod = 1f;
outTicks = (uint)cyclic.Anims.Count - 1;
sequence.ClearPhysics();
sequence.ClearAnimations();
AddMotion(sequence, cyclic, state.SubstateMod);
return true;
}
/// <summary><c>DoObjectMotion</c> 0x00523e90 (@300045): thin wrapper — "do" ==
/// <see cref="GetObjectSequence"/> with the stop-flag forced to false.</summary>
public bool DoObjectMotion(uint motion, MotionState state, CSequence sequence, float speed, out uint outTicks)
=> GetObjectSequence(motion, state, sequence, speed, out outTicks, stopCall: false);
/// <summary><c>StopObjectMotion</c> 0x00523ec0 (@300053): thin wrapper —
/// tailcall straight into <see cref="StopSequenceMotion"/>.</summary>
public bool StopObjectMotion(uint motion, float speed, MotionState state, CSequence sequence, out uint outTicks)
=> StopSequenceMotion(motion, speed, state, sequence, out outTicks);
/// <summary>
/// <c>StopObjectCompletely</c> 0x00523ed0 (@300062): the "return to idle
/// from everything" call — strips every active modifier first (each one
/// individually going through the same subtract-and-unlink path as a
/// single <see cref="StopObjectMotion"/> call), then stops the base
/// cycle/substate itself (which re-drives <see cref="GetObjectSequence"/>
/// toward the style's default substate). Q0-pins A4-#4: return =
/// <c>finalStopOk ? true : anyModifierStopOk</c>, ported verbatim.
/// </summary>
public bool StopObjectCompletely(MotionState state, CSequence sequence, out uint outTicks)
{
outTicks = 0;
bool anyModifierStopOk = false;
// Stop EVERY currently-active modifier first (list shrinks each
// iteration because StopSequenceMotion unlinks the node it stops).
while (state.Modifiers.Any())
{
var node = state.Modifiers.First();
float speedMod = node.SpeedMod;
if (StopSequenceMotion(node.Motion, speedMod, state, sequence, out outTicks))
anyModifierStopOk = true;
else
break; // defensive: avoid infinite loop if a stop can't unlink (shouldn't happen)
}
// Finally stop the base cycle/substate itself.
if (StopSequenceMotion(state.Substate, state.SubstateMod, state, sequence, out outTicks))
return true;
return anyModifierStopOk;
}
// ── private lookup helpers ──────────────────────────────────────────
private uint LookupStyleDefault(uint style)
=> _table.StyleDefaults.TryGetValue((DRWMotionCommand)style, out var def) ? (uint)def : 0u;
private MotionData? LookupCycle(uint style, uint substate)
{
int key = (int)((style << 16) | (substate & 0xFFFFFFu));
return _table.Cycles.TryGetValue(key, out var data) ? data : null;
}
private MotionData? LookupModifier(uint style, uint modKey, bool styleSpecific)
{
int key = styleSpecific ? (int)((style << 16) | modKey) : (int)modKey;
return _table.Modifiers.TryGetValue(key, out var data) ? data : null;
}
}

File diff suppressed because it is too large Load diff