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:
parent
2345da30e9
commit
98f58db913
2 changed files with 1737 additions and 0 deletions
692
src/AcDream.Core/Physics/Motion/CMotionTable.cs
Normal file
692
src/AcDream.Core/Physics/Motion/CMotionTable.cs
Normal 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>>= 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;
|
||||
}
|
||||
}
|
||||
1045
tests/AcDream.Core.Tests/Physics/Motion/CMotionTableTests.cs
Normal file
1045
tests/AcDream.Core.Tests/Physics/Motion/CMotionTableTests.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue