fix(combat): #159 — derive CombatAnimationMotionCommands from the DRW enum
CombatAnimationPlanner.CombatAnimationMotionCommands hand-transcribed the combat MotionCommand constants from the Sept 2013 EoR decomp. The whole late-combat block (Offhand* / Attack4-6 / Punch*) is numbered +3 lower there than in ACE/DatReaderWriter — a contiguous low-word shift that begins around SnowAngelState (0x115). So against a live ACE server every one of those 40 commands misclassified: the resolver returned the correct ACE value (e.g. wire 0x0173 -> 0x10000173 OffhandSlashHigh) but the planner's constant held the 2013 value (0x10000170), so no switch case matched. Reload was worse — hardcoded 0x100000D4, a value absent from DRW entirely; the real ACE/DRW Reload is the 0x40000016 SubState. Instead of re-transcribing 40 hex constants (exactly how the drift crept in), derive each constant directly from DatReaderWriter.Enums.MotionCommand by name: `= (uint)Drw.Name`. The value IS the oracle by construction, can never drift from the wire again, and ~80 magic numbers are gone. Ground truth was taken by reflecting over the same DatReaderWriter 2.1.7 assembly the runtime binds (409 enum values). Blocks 1-2 (stances + single melee, 0x3C-0x12A) were already correct and are unchanged in value. Tests: new PlanFromWireCommand_LateCombatBlock_UsesAceDrwNumbering pins the full wire -> resolve -> classify pipeline for 14 late-block ACE wire values (both full value and kind), plus a Reload SubState parity fact. Fixed two rows in ClassifyMotionCommand_RecognisesRetailCombatCommands whose literal values change meaning under DRW numbering (0x1000018E is AttackLow6/CreatureAttack, not PunchFastLow; Reload moves to 0x40000016). Core suite 2499 green. Note: CombatAnimationPlanner is not yet wired into the runtime dispatch (the live path is AnimationCommandRouter -> MotionInterpreter/AnimationSequencer), so this is a latent-correctness fix — it does not by itself change the #170 Mite Scamp symptom (whose attacks 0x62-0x64 live in the already-correct block). Ref: docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e4155cd962
commit
2de5a011b4
2 changed files with 191 additions and 128 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Physics;
|
||||
using Drw = DatReaderWriter.Enums.MotionCommand;
|
||||
|
||||
namespace AcDream.Core.Combat;
|
||||
|
||||
|
|
@ -200,109 +201,146 @@ public enum CombatAnimationKind
|
|||
Death,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full 32-bit retail MotionCommand values the planner classifies, each
|
||||
/// derived DIRECTLY from <see cref="Drw"/> (Chorizite.DatReaderWriter — the
|
||||
/// same enum ACE broadcasts and that the local DAT MotionTables key on) so
|
||||
/// the values can never drift from the wire again.
|
||||
///
|
||||
/// <para>
|
||||
/// History (ISSUES #159): these were previously hand-transcribed from the
|
||||
/// Sept 2013 EoR decomp's <c>command_ids</c> table, which numbers the whole
|
||||
/// late-combat block (<c>Offhand*</c> / <c>Attack4-6</c> / <c>Punch*</c>)
|
||||
/// THREE lower than ACE/DRW — a contiguous low-word "+3" shift that begins
|
||||
/// around <c>SnowAngelState</c> (0x115). Against a live ACE server those
|
||||
/// commands silently misclassified: the resolver returned the correct ACE
|
||||
/// value (e.g. wire 0x0173 -> <c>0x10000173</c> <c>OffhandSlashHigh</c>),
|
||||
/// but the planner's constant held the 2013 value (<c>0x10000170</c>), so no
|
||||
/// match. <c>Reload</c> was worse — hardcoded <c>0x100000D4</c>, a value
|
||||
/// absent from DRW entirely; the real ACE/DRW <c>Reload</c> is the
|
||||
/// <c>0x40000016</c> SubState. Deriving from the enum by name fixes the whole
|
||||
/// block at once and drops ~80 magic numbers.
|
||||
/// See <c>docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class CombatAnimationMotionCommands
|
||||
{
|
||||
public const uint HandCombat = 0x8000003Cu;
|
||||
public const uint SwordCombat = 0x8000003Eu;
|
||||
public const uint BowCombat = 0x8000003Fu;
|
||||
public const uint SwordShieldCombat = 0x80000040u;
|
||||
public const uint CrossbowCombat = 0x80000041u;
|
||||
public const uint SlingCombat = 0x80000043u;
|
||||
public const uint TwoHandedSwordCombat = 0x80000044u;
|
||||
public const uint TwoHandedStaffCombat = 0x80000045u;
|
||||
public const uint DualWieldCombat = 0x80000046u;
|
||||
public const uint ThrownWeaponCombat = 0x80000047u;
|
||||
public const uint Magic = 0x80000049u;
|
||||
public const uint AtlatlCombat = 0x8000013Bu;
|
||||
public const uint ThrownShieldCombat = 0x8000013Cu;
|
||||
// Combat stances (Style class 0x80). Already correct before #159.
|
||||
public const uint HandCombat = (uint)Drw.HandCombat;
|
||||
public const uint SwordCombat = (uint)Drw.SwordCombat;
|
||||
public const uint BowCombat = (uint)Drw.BowCombat;
|
||||
public const uint SwordShieldCombat = (uint)Drw.SwordShieldCombat;
|
||||
public const uint CrossbowCombat = (uint)Drw.CrossbowCombat;
|
||||
public const uint SlingCombat = (uint)Drw.SlingCombat;
|
||||
public const uint TwoHandedSwordCombat = (uint)Drw.TwoHandedSwordCombat;
|
||||
public const uint TwoHandedStaffCombat = (uint)Drw.TwoHandedStaffCombat;
|
||||
public const uint DualWieldCombat = (uint)Drw.DualWieldCombat;
|
||||
public const uint ThrownWeaponCombat = (uint)Drw.ThrownWeaponCombat;
|
||||
public const uint Magic = (uint)Drw.Magic;
|
||||
public const uint AtlatlCombat = (uint)Drw.AtlatlCombat;
|
||||
public const uint ThrownShieldCombat = (uint)Drw.ThrownShieldCombat;
|
||||
|
||||
public const uint FallDown = 0x10000050u;
|
||||
public const uint Twitch1 = 0x10000051u;
|
||||
public const uint Twitch2 = 0x10000052u;
|
||||
public const uint Twitch3 = 0x10000053u;
|
||||
public const uint Twitch4 = 0x10000054u;
|
||||
public const uint StaggerBackward = 0x10000055u;
|
||||
public const uint StaggerForward = 0x10000056u;
|
||||
public const uint Sanctuary = 0x10000057u;
|
||||
public const uint ThrustMed = 0x10000058u;
|
||||
public const uint ThrustLow = 0x10000059u;
|
||||
public const uint ThrustHigh = 0x1000005Au;
|
||||
public const uint SlashHigh = 0x1000005Bu;
|
||||
public const uint SlashMed = 0x1000005Cu;
|
||||
public const uint SlashLow = 0x1000005Du;
|
||||
public const uint BackhandHigh = 0x1000005Eu;
|
||||
public const uint BackhandMed = 0x1000005Fu;
|
||||
public const uint BackhandLow = 0x10000060u;
|
||||
public const uint Shoot = 0x10000061u;
|
||||
public const uint AttackHigh1 = 0x10000062u;
|
||||
public const uint AttackMed1 = 0x10000063u;
|
||||
public const uint AttackLow1 = 0x10000064u;
|
||||
public const uint AttackHigh2 = 0x10000065u;
|
||||
public const uint AttackMed2 = 0x10000066u;
|
||||
public const uint AttackLow2 = 0x10000067u;
|
||||
public const uint AttackHigh3 = 0x10000068u;
|
||||
public const uint AttackMed3 = 0x10000069u;
|
||||
public const uint AttackLow3 = 0x1000006Au;
|
||||
// Hit reactions / staggers (Action class 0x10, unshifted).
|
||||
public const uint FallDown = (uint)Drw.FallDown;
|
||||
public const uint Twitch1 = (uint)Drw.Twitch1;
|
||||
public const uint Twitch2 = (uint)Drw.Twitch2;
|
||||
public const uint Twitch3 = (uint)Drw.Twitch3;
|
||||
public const uint Twitch4 = (uint)Drw.Twitch4;
|
||||
public const uint StaggerBackward = (uint)Drw.StaggerBackward;
|
||||
public const uint StaggerForward = (uint)Drw.StaggerForward;
|
||||
public const uint Sanctuary = (uint)Drw.Sanctuary;
|
||||
|
||||
public const uint MissileAttack1 = 0x100000D0u;
|
||||
public const uint MissileAttack2 = 0x100000D1u;
|
||||
public const uint MissileAttack3 = 0x100000D2u;
|
||||
public const uint CastSpell = 0x400000D3u;
|
||||
public const uint Reload = 0x100000D4u;
|
||||
public const uint UseMagicStaff = 0x400000E0u;
|
||||
public const uint UseMagicWand = 0x400000E1u;
|
||||
// Single melee attacks (Action class 0x10, unshifted).
|
||||
public const uint ThrustMed = (uint)Drw.ThrustMed;
|
||||
public const uint ThrustLow = (uint)Drw.ThrustLow;
|
||||
public const uint ThrustHigh = (uint)Drw.ThrustHigh;
|
||||
public const uint SlashHigh = (uint)Drw.SlashHigh;
|
||||
public const uint SlashMed = (uint)Drw.SlashMed;
|
||||
public const uint SlashLow = (uint)Drw.SlashLow;
|
||||
public const uint BackhandHigh = (uint)Drw.BackhandHigh;
|
||||
public const uint BackhandMed = (uint)Drw.BackhandMed;
|
||||
public const uint BackhandLow = (uint)Drw.BackhandLow;
|
||||
|
||||
public const uint DoubleSlashLow = 0x1000011Fu;
|
||||
public const uint DoubleSlashMed = 0x10000120u;
|
||||
public const uint DoubleSlashHigh = 0x10000121u;
|
||||
public const uint TripleSlashLow = 0x10000122u;
|
||||
public const uint TripleSlashMed = 0x10000123u;
|
||||
public const uint TripleSlashHigh = 0x10000124u;
|
||||
public const uint DoubleThrustLow = 0x10000125u;
|
||||
public const uint DoubleThrustMed = 0x10000126u;
|
||||
public const uint DoubleThrustHigh = 0x10000127u;
|
||||
public const uint TripleThrustLow = 0x10000128u;
|
||||
public const uint TripleThrustMed = 0x10000129u;
|
||||
public const uint TripleThrustHigh = 0x1000012Au;
|
||||
// Missile attacks (Action 0x10) + reload (SubState 0x40, NOT the dead
|
||||
// 2013 0x100000D4 — the real DRW Reload is 0x40000016).
|
||||
public const uint Shoot = (uint)Drw.Shoot;
|
||||
public const uint MissileAttack1 = (uint)Drw.MissileAttack1;
|
||||
public const uint MissileAttack2 = (uint)Drw.MissileAttack2;
|
||||
public const uint MissileAttack3 = (uint)Drw.MissileAttack3;
|
||||
public const uint Reload = (uint)Drw.Reload;
|
||||
|
||||
public const uint OffhandSlashHigh = 0x10000170u;
|
||||
public const uint OffhandSlashMed = 0x10000171u;
|
||||
public const uint OffhandSlashLow = 0x10000172u;
|
||||
public const uint OffhandThrustHigh = 0x10000173u;
|
||||
public const uint OffhandThrustMed = 0x10000174u;
|
||||
public const uint OffhandThrustLow = 0x10000175u;
|
||||
public const uint OffhandDoubleSlashLow = 0x10000176u;
|
||||
public const uint OffhandDoubleSlashMed = 0x10000177u;
|
||||
public const uint OffhandDoubleSlashHigh = 0x10000178u;
|
||||
public const uint OffhandTripleSlashLow = 0x10000179u;
|
||||
public const uint OffhandTripleSlashMed = 0x1000017Au;
|
||||
public const uint OffhandTripleSlashHigh = 0x1000017Bu;
|
||||
public const uint OffhandDoubleThrustLow = 0x1000017Cu;
|
||||
public const uint OffhandDoubleThrustMed = 0x1000017Du;
|
||||
public const uint OffhandDoubleThrustHigh = 0x1000017Eu;
|
||||
public const uint OffhandTripleThrustLow = 0x1000017Fu;
|
||||
public const uint OffhandTripleThrustMed = 0x10000180u;
|
||||
public const uint OffhandTripleThrustHigh = 0x10000181u;
|
||||
public const uint OffhandKick = 0x10000182u;
|
||||
public const uint AttackHigh4 = 0x10000183u;
|
||||
public const uint AttackMed4 = 0x10000184u;
|
||||
public const uint AttackLow4 = 0x10000185u;
|
||||
public const uint AttackHigh5 = 0x10000186u;
|
||||
public const uint AttackMed5 = 0x10000187u;
|
||||
public const uint AttackLow5 = 0x10000188u;
|
||||
public const uint AttackHigh6 = 0x10000189u;
|
||||
public const uint AttackMed6 = 0x1000018Au;
|
||||
public const uint AttackLow6 = 0x1000018Bu;
|
||||
public const uint PunchFastHigh = 0x1000018Cu;
|
||||
public const uint PunchFastMed = 0x1000018Du;
|
||||
public const uint PunchFastLow = 0x1000018Eu;
|
||||
public const uint PunchSlowHigh = 0x1000018Fu;
|
||||
public const uint PunchSlowMed = 0x10000190u;
|
||||
public const uint PunchSlowLow = 0x10000191u;
|
||||
public const uint OffhandPunchFastHigh = 0x10000192u;
|
||||
public const uint OffhandPunchFastMed = 0x10000193u;
|
||||
public const uint OffhandPunchFastLow = 0x10000194u;
|
||||
public const uint OffhandPunchSlowHigh = 0x10000195u;
|
||||
public const uint OffhandPunchSlowMed = 0x10000196u;
|
||||
public const uint OffhandPunchSlowLow = 0x10000197u;
|
||||
// Creature attacks 1-6 (Action class 0x10). 1-3 are the unshifted low
|
||||
// block (0x62-0x6A); 4-6 live in the shifted late block (0x186+).
|
||||
public const uint AttackHigh1 = (uint)Drw.AttackHigh1;
|
||||
public const uint AttackMed1 = (uint)Drw.AttackMed1;
|
||||
public const uint AttackLow1 = (uint)Drw.AttackLow1;
|
||||
public const uint AttackHigh2 = (uint)Drw.AttackHigh2;
|
||||
public const uint AttackMed2 = (uint)Drw.AttackMed2;
|
||||
public const uint AttackLow2 = (uint)Drw.AttackLow2;
|
||||
public const uint AttackHigh3 = (uint)Drw.AttackHigh3;
|
||||
public const uint AttackMed3 = (uint)Drw.AttackMed3;
|
||||
public const uint AttackLow3 = (uint)Drw.AttackLow3;
|
||||
public const uint AttackHigh4 = (uint)Drw.AttackHigh4;
|
||||
public const uint AttackMed4 = (uint)Drw.AttackMed4;
|
||||
public const uint AttackLow4 = (uint)Drw.AttackLow4;
|
||||
public const uint AttackHigh5 = (uint)Drw.AttackHigh5;
|
||||
public const uint AttackMed5 = (uint)Drw.AttackMed5;
|
||||
public const uint AttackLow5 = (uint)Drw.AttackLow5;
|
||||
public const uint AttackHigh6 = (uint)Drw.AttackHigh6;
|
||||
public const uint AttackMed6 = (uint)Drw.AttackMed6;
|
||||
public const uint AttackLow6 = (uint)Drw.AttackLow6;
|
||||
|
||||
// Spell casts (SubState class 0x40).
|
||||
public const uint CastSpell = (uint)Drw.CastSpell;
|
||||
public const uint UseMagicStaff = (uint)Drw.UseMagicStaff;
|
||||
public const uint UseMagicWand = (uint)Drw.UseMagicWand;
|
||||
|
||||
// Multi-strike melee (Action class 0x10, unshifted 0x11F-0x12A).
|
||||
public const uint DoubleSlashLow = (uint)Drw.DoubleSlashLow;
|
||||
public const uint DoubleSlashMed = (uint)Drw.DoubleSlashMed;
|
||||
public const uint DoubleSlashHigh = (uint)Drw.DoubleSlashHigh;
|
||||
public const uint TripleSlashLow = (uint)Drw.TripleSlashLow;
|
||||
public const uint TripleSlashMed = (uint)Drw.TripleSlashMed;
|
||||
public const uint TripleSlashHigh = (uint)Drw.TripleSlashHigh;
|
||||
public const uint DoubleThrustLow = (uint)Drw.DoubleThrustLow;
|
||||
public const uint DoubleThrustMed = (uint)Drw.DoubleThrustMed;
|
||||
public const uint DoubleThrustHigh = (uint)Drw.DoubleThrustHigh;
|
||||
public const uint TripleThrustLow = (uint)Drw.TripleThrustLow;
|
||||
public const uint TripleThrustMed = (uint)Drw.TripleThrustMed;
|
||||
public const uint TripleThrustHigh = (uint)Drw.TripleThrustHigh;
|
||||
|
||||
// Offhand strikes (Action class 0x10, shifted late block 0x173+).
|
||||
public const uint OffhandSlashHigh = (uint)Drw.OffhandSlashHigh;
|
||||
public const uint OffhandSlashMed = (uint)Drw.OffhandSlashMed;
|
||||
public const uint OffhandSlashLow = (uint)Drw.OffhandSlashLow;
|
||||
public const uint OffhandThrustHigh = (uint)Drw.OffhandThrustHigh;
|
||||
public const uint OffhandThrustMed = (uint)Drw.OffhandThrustMed;
|
||||
public const uint OffhandThrustLow = (uint)Drw.OffhandThrustLow;
|
||||
public const uint OffhandDoubleSlashLow = (uint)Drw.OffhandDoubleSlashLow;
|
||||
public const uint OffhandDoubleSlashMed = (uint)Drw.OffhandDoubleSlashMed;
|
||||
public const uint OffhandDoubleSlashHigh = (uint)Drw.OffhandDoubleSlashHigh;
|
||||
public const uint OffhandTripleSlashLow = (uint)Drw.OffhandTripleSlashLow;
|
||||
public const uint OffhandTripleSlashMed = (uint)Drw.OffhandTripleSlashMed;
|
||||
public const uint OffhandTripleSlashHigh = (uint)Drw.OffhandTripleSlashHigh;
|
||||
public const uint OffhandDoubleThrustLow = (uint)Drw.OffhandDoubleThrustLow;
|
||||
public const uint OffhandDoubleThrustMed = (uint)Drw.OffhandDoubleThrustMed;
|
||||
public const uint OffhandDoubleThrustHigh = (uint)Drw.OffhandDoubleThrustHigh;
|
||||
public const uint OffhandTripleThrustLow = (uint)Drw.OffhandTripleThrustLow;
|
||||
public const uint OffhandTripleThrustMed = (uint)Drw.OffhandTripleThrustMed;
|
||||
public const uint OffhandTripleThrustHigh = (uint)Drw.OffhandTripleThrustHigh;
|
||||
public const uint OffhandKick = (uint)Drw.OffhandKick;
|
||||
|
||||
// Punches (Action class 0x10, shifted late block).
|
||||
public const uint PunchFastHigh = (uint)Drw.PunchFastHigh;
|
||||
public const uint PunchFastMed = (uint)Drw.PunchFastMed;
|
||||
public const uint PunchFastLow = (uint)Drw.PunchFastLow;
|
||||
public const uint PunchSlowHigh = (uint)Drw.PunchSlowHigh;
|
||||
public const uint PunchSlowMed = (uint)Drw.PunchSlowMed;
|
||||
public const uint PunchSlowLow = (uint)Drw.PunchSlowLow;
|
||||
public const uint OffhandPunchFastHigh = (uint)Drw.OffhandPunchFastHigh;
|
||||
public const uint OffhandPunchFastMed = (uint)Drw.OffhandPunchFastMed;
|
||||
public const uint OffhandPunchFastLow = (uint)Drw.OffhandPunchFastLow;
|
||||
public const uint OffhandPunchSlowHigh = (uint)Drw.OffhandPunchSlowHigh;
|
||||
public const uint OffhandPunchSlowMed = (uint)Drw.OffhandPunchSlowMed;
|
||||
public const uint OffhandPunchSlowLow = (uint)Drw.OffhandPunchSlowLow;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ public sealed class CombatAnimationPlannerTests
|
|||
[Theory]
|
||||
[InlineData(0x10000058u, CombatAnimationKind.MeleeSwing)] // ThrustMed
|
||||
[InlineData(0x1000005Bu, CombatAnimationKind.MeleeSwing)] // SlashHigh
|
||||
[InlineData(0x1000017Du, CombatAnimationKind.MeleeSwing)] // OffhandDoubleThrustMed
|
||||
[InlineData(0x1000018Eu, CombatAnimationKind.MeleeSwing)] // PunchFastLow
|
||||
[InlineData(0x1000017Du, CombatAnimationKind.MeleeSwing)] // OffhandTripleSlashMed (DRW)
|
||||
[InlineData(0x1000018Eu, CombatAnimationKind.CreatureAttack)] // AttackLow6 (DRW) — was mislabelled PunchFastLow under 2013 numbering
|
||||
[InlineData(0x10000061u, CombatAnimationKind.MissileAttack)] // Shoot
|
||||
[InlineData(0x100000D4u, CombatAnimationKind.MissileAttack)] // Reload
|
||||
[InlineData(0x40000016u, CombatAnimationKind.MissileAttack)] // Reload (DRW SubState) — was the dead 2013 value 0x100000D4
|
||||
[InlineData(0x10000062u, CombatAnimationKind.CreatureAttack)] // AttackHigh1
|
||||
[InlineData(0x1000018Bu, CombatAnimationKind.CreatureAttack)] // AttackLow6
|
||||
[InlineData(0x1000018Bu, CombatAnimationKind.CreatureAttack)] // AttackLow5 (DRW)
|
||||
[InlineData(0x400000D3u, CombatAnimationKind.SpellCast)] // CastSpell
|
||||
[InlineData(0x400000E0u, CombatAnimationKind.SpellCast)] // UseMagicStaff
|
||||
[InlineData(0x10000051u, CombatAnimationKind.HitReaction)] // Twitch1
|
||||
|
|
@ -30,32 +30,13 @@ public sealed class CombatAnimationPlannerTests
|
|||
Assert.Equal(expected, CombatAnimationPlanner.ClassifyMotionCommand(command));
|
||||
}
|
||||
|
||||
// L.1b correction (2026-06-30): these four expected values were pinned
|
||||
// against MotionCommandResolver's old blind per-range override
|
||||
// (0x016E-0x0197 force-mapped to class 0x10000000), not against named
|
||||
// retail truth. The override is gone — MotionCommandResolver now
|
||||
// delegates to AceModernCommandCatalog, built cleanly from
|
||||
// DatReaderWriter.Enums.MotionCommand (Chorizite.DatReaderWriter
|
||||
// 2.1.7). Per that enum, wire 0x0170 is IssueSlashCommand = 0x09000170
|
||||
// (class 0x09, UI command) — there is no Action-class (0x10) entry at
|
||||
// that wire at all, so the old "OffhandSlashHigh" expectation here was
|
||||
// simply wrong (real OffhandSlashHigh = 0x10000173, see
|
||||
// docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md). Updated
|
||||
// to the wire's real DRW-enum identity.
|
||||
//
|
||||
// NOTE: the surviving rows (0x017D/0x018B/0x018E) still pass, but only
|
||||
// because they land on SOME Action-class value at that low word — their
|
||||
// inline comments ("OffhandDoubleThrustMed"/"AttackLow6"/"PunchFastLow")
|
||||
// were also wrong names (the real DRW names are
|
||||
// OffhandTripleSlashMed/AttackLow5/AttackLow6 respectively). This
|
||||
// reveals that CombatAnimationPlanner.CombatAnimationMotionCommands
|
||||
// (src/AcDream.Core/Combat/CombatAnimationPlanner.cs:268-307) is itself
|
||||
// built from 2013-decomp numbering, not ACE/DRW numbering — a
|
||||
// pre-existing, separate bug outside this slice's scope (catalog only;
|
||||
// CombatAnimationPlanner is explicitly out of bounds for L.1b). Flagged
|
||||
// for follow-up rather than silently patched.
|
||||
// These pin the RESOLVER (wire u16 -> full 32-bit) against the DRW enum,
|
||||
// independent of the planner. wire 0x0170 is IssueSlashCommand = 0x09000170
|
||||
// (class 0x09, UI command) — there is no Action-class (0x10) entry at that
|
||||
// wire; the real OffhandSlashHigh is 0x10000173 (the 2013 decomp numbers it
|
||||
// 0x10000170, +3 low). See docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md.
|
||||
[Theory]
|
||||
[InlineData(0x0170, 0x09000170u)] // IssueSlashCommand (UI class) — see note above
|
||||
[InlineData(0x0170, 0x09000170u)] // IssueSlashCommand (UI class), NOT OffhandSlashHigh
|
||||
[InlineData(0x017D, 0x1000017Du)] // OffhandTripleSlashMed
|
||||
[InlineData(0x018B, 0x1000018Bu)] // AttackLow5
|
||||
[InlineData(0x018E, 0x1000018Eu)] // AttackLow6
|
||||
|
|
@ -66,6 +47,50 @@ public sealed class CombatAnimationPlannerTests
|
|||
Assert.Equal(expectedFullCommand, MotionCommandResolver.ReconstructFullCommand(wireCommand));
|
||||
}
|
||||
|
||||
// #159 parity: the full wire -> resolve -> classify pipeline for the
|
||||
// late-combat block, which ACE/DRW numbers +3 above the 2013 decomp.
|
||||
// CombatAnimationMotionCommands is now derived directly from
|
||||
// DatReaderWriter.Enums.MotionCommand, so these ACE wire values must both
|
||||
// reconstruct to their DRW full value AND classify into the right kind.
|
||||
// (Expected full/kind pairs cross-checked against the DRW MotionCommand
|
||||
// enum, Chorizite.DatReaderWriter 2.1.7.)
|
||||
[Theory]
|
||||
[InlineData((ushort)0x0173, 0x10000173u, CombatAnimationKind.MeleeSwing)] // OffhandSlashHigh
|
||||
[InlineData((ushort)0x0175, 0x10000175u, CombatAnimationKind.MeleeSwing)] // OffhandSlashLow
|
||||
[InlineData((ushort)0x0176, 0x10000176u, CombatAnimationKind.MeleeSwing)] // OffhandThrustHigh
|
||||
[InlineData((ushort)0x017E, 0x1000017Eu, CombatAnimationKind.MeleeSwing)] // OffhandTripleSlashHigh
|
||||
[InlineData((ushort)0x0182, 0x10000182u, CombatAnimationKind.MeleeSwing)] // OffhandTripleThrustLow
|
||||
[InlineData((ushort)0x0185, 0x10000185u, CombatAnimationKind.MeleeSwing)] // OffhandKick
|
||||
[InlineData((ushort)0x0186, 0x10000186u, CombatAnimationKind.CreatureAttack)] // AttackHigh4
|
||||
[InlineData((ushort)0x0188, 0x10000188u, CombatAnimationKind.CreatureAttack)] // AttackLow4
|
||||
[InlineData((ushort)0x018C, 0x1000018Cu, CombatAnimationKind.CreatureAttack)] // AttackHigh6
|
||||
[InlineData((ushort)0x018E, 0x1000018Eu, CombatAnimationKind.CreatureAttack)] // AttackLow6
|
||||
[InlineData((ushort)0x018F, 0x1000018Fu, CombatAnimationKind.MeleeSwing)] // PunchFastHigh
|
||||
[InlineData((ushort)0x0191, 0x10000191u, CombatAnimationKind.MeleeSwing)] // PunchFastLow
|
||||
[InlineData((ushort)0x0197, 0x10000197u, CombatAnimationKind.MeleeSwing)] // OffhandPunchFastLow
|
||||
[InlineData((ushort)0x019A, 0x1000019Au, CombatAnimationKind.MeleeSwing)] // OffhandPunchSlowLow
|
||||
public void PlanFromWireCommand_LateCombatBlock_UsesAceDrwNumbering(
|
||||
ushort wireCommand,
|
||||
uint expectedFullCommand,
|
||||
CombatAnimationKind expectedKind)
|
||||
{
|
||||
var plan = CombatAnimationPlanner.PlanFromWireCommand(wireCommand);
|
||||
Assert.Equal(expectedFullCommand, plan.MotionCommand);
|
||||
Assert.Equal(expectedKind, plan.Kind);
|
||||
}
|
||||
|
||||
// #159: Reload is the DRW SubState 0x40000016 (wire 0x0016), not the dead
|
||||
// 2013 value 0x100000D4 the planner used to hold (absent from DRW entirely).
|
||||
[Fact]
|
||||
public void PlanFromWireCommand_Reload_UsesDrwSubStateValue()
|
||||
{
|
||||
var plan = CombatAnimationPlanner.PlanFromWireCommand(0x0016);
|
||||
|
||||
Assert.Equal(0x40000016u, plan.MotionCommand);
|
||||
Assert.Equal(CombatAnimationKind.MissileAttack, plan.Kind);
|
||||
Assert.Equal(AnimationCommandRouteKind.SubState, plan.RouteKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanFromWireCommand_Swing_IsActionOverlay()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue