diff --git a/src/AcDream.Core/Combat/CombatAnimationPlanner.cs b/src/AcDream.Core/Combat/CombatAnimationPlanner.cs index bf67a852..f7b7ca36 100644 --- a/src/AcDream.Core/Combat/CombatAnimationPlanner.cs +++ b/src/AcDream.Core/Combat/CombatAnimationPlanner.cs @@ -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, } +/// +/// The full 32-bit retail MotionCommand values the planner classifies, each +/// derived DIRECTLY from (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. +/// +/// +/// History (ISSUES #159): these were previously hand-transcribed from the +/// Sept 2013 EoR decomp's command_ids table, which numbers the whole +/// late-combat block (Offhand* / Attack4-6 / Punch*) +/// THREE lower than ACE/DRW — a contiguous low-word "+3" shift that begins +/// around SnowAngelState (0x115). Against a live ACE server those +/// commands silently 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 +/// match. Reload was worse — hardcoded 0x100000D4, a value +/// absent from DRW entirely; the real ACE/DRW Reload is the +/// 0x40000016 SubState. Deriving from the enum by name fixes the whole +/// block at once and drops ~80 magic numbers. +/// See docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md. +/// +/// 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; } diff --git a/tests/AcDream.Core.Tests/Combat/CombatAnimationPlannerTests.cs b/tests/AcDream.Core.Tests/Combat/CombatAnimationPlannerTests.cs index d682646c..3a178756 100644 --- a/tests/AcDream.Core.Tests/Combat/CombatAnimationPlannerTests.cs +++ b/tests/AcDream.Core.Tests/Combat/CombatAnimationPlannerTests.cs @@ -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() {