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>
346 lines
18 KiB
C#
346 lines
18 KiB
C#
using AcDream.Core.Physics;
|
|
using Drw = DatReaderWriter.Enums.MotionCommand;
|
|
|
|
namespace AcDream.Core.Combat;
|
|
|
|
/// <summary>
|
|
/// Retail-faithful combat animation planner for server-sent motion commands.
|
|
///
|
|
/// Retail evidence:
|
|
/// - <c>ClientCombatSystem::ExecuteAttack</c> (0x0056BB70) only sends the
|
|
/// targeted melee/missile GameAction and sets response state; it does not
|
|
/// locally choose or play a swing animation.
|
|
/// - <c>ClientCombatSystem::HandleCommenceAttackEvent</c> (0x0056AD20)
|
|
/// updates the power bar/busy state; it carries no MotionCommand.
|
|
/// - ACE <c>Player_Melee.DoSwingMotion</c> chooses a swing via
|
|
/// <c>CombatManeuverTable.GetMotion</c> and broadcasts that MotionCommand
|
|
/// in <c>UpdateMotion</c>.
|
|
///
|
|
/// So acdream treats combat GameEvents as state/UI signals and treats
|
|
/// UpdateMotion command IDs as the animation authority.
|
|
/// </summary>
|
|
public static class CombatAnimationPlanner
|
|
{
|
|
public static CombatAnimationPlan PlanForEvent(CombatAnimationEvent combatEvent)
|
|
{
|
|
_ = combatEvent;
|
|
return CombatAnimationPlan.None;
|
|
}
|
|
|
|
public static CombatAnimationPlan PlanFromWireCommand(ushort wireCommand, float speedMod = 1f)
|
|
{
|
|
uint fullCommand = MotionCommandResolver.ReconstructFullCommand(wireCommand);
|
|
return PlanFromFullCommand(fullCommand, speedMod);
|
|
}
|
|
|
|
public static CombatAnimationPlan PlanFromFullCommand(uint fullCommand, float speedMod = 1f)
|
|
{
|
|
var kind = ClassifyMotionCommand(fullCommand);
|
|
if (kind == CombatAnimationKind.None)
|
|
return CombatAnimationPlan.None;
|
|
|
|
return new CombatAnimationPlan(
|
|
kind,
|
|
AnimationCommandRouter.Classify(fullCommand),
|
|
fullCommand,
|
|
speedMod);
|
|
}
|
|
|
|
public static CombatAnimationKind ClassifyMotionCommand(uint fullCommand)
|
|
{
|
|
return fullCommand switch
|
|
{
|
|
CombatAnimationMotionCommands.HandCombat
|
|
or CombatAnimationMotionCommands.SwordCombat
|
|
or CombatAnimationMotionCommands.SwordShieldCombat
|
|
or CombatAnimationMotionCommands.TwoHandedSwordCombat
|
|
or CombatAnimationMotionCommands.TwoHandedStaffCombat
|
|
or CombatAnimationMotionCommands.BowCombat
|
|
or CombatAnimationMotionCommands.CrossbowCombat
|
|
or CombatAnimationMotionCommands.SlingCombat
|
|
or CombatAnimationMotionCommands.DualWieldCombat
|
|
or CombatAnimationMotionCommands.ThrownWeaponCombat
|
|
or CombatAnimationMotionCommands.AtlatlCombat
|
|
or CombatAnimationMotionCommands.ThrownShieldCombat
|
|
or CombatAnimationMotionCommands.Magic => CombatAnimationKind.CombatStance,
|
|
|
|
CombatAnimationMotionCommands.ThrustMed
|
|
or CombatAnimationMotionCommands.ThrustLow
|
|
or CombatAnimationMotionCommands.ThrustHigh
|
|
or CombatAnimationMotionCommands.SlashHigh
|
|
or CombatAnimationMotionCommands.SlashMed
|
|
or CombatAnimationMotionCommands.SlashLow
|
|
or CombatAnimationMotionCommands.BackhandHigh
|
|
or CombatAnimationMotionCommands.BackhandMed
|
|
or CombatAnimationMotionCommands.BackhandLow
|
|
or CombatAnimationMotionCommands.DoubleSlashLow
|
|
or CombatAnimationMotionCommands.DoubleSlashMed
|
|
or CombatAnimationMotionCommands.DoubleSlashHigh
|
|
or CombatAnimationMotionCommands.TripleSlashLow
|
|
or CombatAnimationMotionCommands.TripleSlashMed
|
|
or CombatAnimationMotionCommands.TripleSlashHigh
|
|
or CombatAnimationMotionCommands.DoubleThrustLow
|
|
or CombatAnimationMotionCommands.DoubleThrustMed
|
|
or CombatAnimationMotionCommands.DoubleThrustHigh
|
|
or CombatAnimationMotionCommands.TripleThrustLow
|
|
or CombatAnimationMotionCommands.TripleThrustMed
|
|
or CombatAnimationMotionCommands.TripleThrustHigh
|
|
or CombatAnimationMotionCommands.OffhandSlashHigh
|
|
or CombatAnimationMotionCommands.OffhandSlashMed
|
|
or CombatAnimationMotionCommands.OffhandSlashLow
|
|
or CombatAnimationMotionCommands.OffhandThrustHigh
|
|
or CombatAnimationMotionCommands.OffhandThrustMed
|
|
or CombatAnimationMotionCommands.OffhandThrustLow
|
|
or CombatAnimationMotionCommands.OffhandDoubleSlashLow
|
|
or CombatAnimationMotionCommands.OffhandDoubleSlashMed
|
|
or CombatAnimationMotionCommands.OffhandDoubleSlashHigh
|
|
or CombatAnimationMotionCommands.OffhandTripleSlashLow
|
|
or CombatAnimationMotionCommands.OffhandTripleSlashMed
|
|
or CombatAnimationMotionCommands.OffhandTripleSlashHigh
|
|
or CombatAnimationMotionCommands.OffhandDoubleThrustLow
|
|
or CombatAnimationMotionCommands.OffhandDoubleThrustMed
|
|
or CombatAnimationMotionCommands.OffhandDoubleThrustHigh
|
|
or CombatAnimationMotionCommands.OffhandTripleThrustLow
|
|
or CombatAnimationMotionCommands.OffhandTripleThrustMed
|
|
or CombatAnimationMotionCommands.OffhandTripleThrustHigh
|
|
or CombatAnimationMotionCommands.OffhandKick
|
|
or CombatAnimationMotionCommands.PunchFastHigh
|
|
or CombatAnimationMotionCommands.PunchFastMed
|
|
or CombatAnimationMotionCommands.PunchFastLow
|
|
or CombatAnimationMotionCommands.PunchSlowHigh
|
|
or CombatAnimationMotionCommands.PunchSlowMed
|
|
or CombatAnimationMotionCommands.PunchSlowLow
|
|
or CombatAnimationMotionCommands.OffhandPunchFastHigh
|
|
or CombatAnimationMotionCommands.OffhandPunchFastMed
|
|
or CombatAnimationMotionCommands.OffhandPunchFastLow
|
|
or CombatAnimationMotionCommands.OffhandPunchSlowHigh
|
|
or CombatAnimationMotionCommands.OffhandPunchSlowMed
|
|
or CombatAnimationMotionCommands.OffhandPunchSlowLow => CombatAnimationKind.MeleeSwing,
|
|
|
|
CombatAnimationMotionCommands.Shoot
|
|
or CombatAnimationMotionCommands.MissileAttack1
|
|
or CombatAnimationMotionCommands.MissileAttack2
|
|
or CombatAnimationMotionCommands.MissileAttack3
|
|
or CombatAnimationMotionCommands.Reload => CombatAnimationKind.MissileAttack,
|
|
|
|
CombatAnimationMotionCommands.AttackHigh1
|
|
or CombatAnimationMotionCommands.AttackMed1
|
|
or CombatAnimationMotionCommands.AttackLow1
|
|
or CombatAnimationMotionCommands.AttackHigh2
|
|
or CombatAnimationMotionCommands.AttackMed2
|
|
or CombatAnimationMotionCommands.AttackLow2
|
|
or CombatAnimationMotionCommands.AttackHigh3
|
|
or CombatAnimationMotionCommands.AttackMed3
|
|
or CombatAnimationMotionCommands.AttackLow3
|
|
or CombatAnimationMotionCommands.AttackHigh4
|
|
or CombatAnimationMotionCommands.AttackMed4
|
|
or CombatAnimationMotionCommands.AttackLow4
|
|
or CombatAnimationMotionCommands.AttackHigh5
|
|
or CombatAnimationMotionCommands.AttackMed5
|
|
or CombatAnimationMotionCommands.AttackLow5
|
|
or CombatAnimationMotionCommands.AttackHigh6
|
|
or CombatAnimationMotionCommands.AttackMed6
|
|
or CombatAnimationMotionCommands.AttackLow6 => CombatAnimationKind.CreatureAttack,
|
|
|
|
CombatAnimationMotionCommands.CastSpell
|
|
or CombatAnimationMotionCommands.UseMagicStaff
|
|
or CombatAnimationMotionCommands.UseMagicWand => CombatAnimationKind.SpellCast,
|
|
|
|
CombatAnimationMotionCommands.FallDown
|
|
or CombatAnimationMotionCommands.Twitch1
|
|
or CombatAnimationMotionCommands.Twitch2
|
|
or CombatAnimationMotionCommands.Twitch3
|
|
or CombatAnimationMotionCommands.Twitch4
|
|
or CombatAnimationMotionCommands.StaggerBackward
|
|
or CombatAnimationMotionCommands.StaggerForward
|
|
or CombatAnimationMotionCommands.Sanctuary => CombatAnimationKind.HitReaction,
|
|
|
|
MotionCommand.Dead => CombatAnimationKind.Death,
|
|
|
|
_ => CombatAnimationKind.None,
|
|
};
|
|
}
|
|
}
|
|
|
|
public readonly record struct CombatAnimationPlan(
|
|
CombatAnimationKind Kind,
|
|
AnimationCommandRouteKind RouteKind,
|
|
uint MotionCommand,
|
|
float SpeedMod)
|
|
{
|
|
public static CombatAnimationPlan None { get; } = new(
|
|
CombatAnimationKind.None,
|
|
AnimationCommandRouteKind.None,
|
|
0u,
|
|
0f);
|
|
|
|
public bool HasMotion => Kind != CombatAnimationKind.None && MotionCommand != 0;
|
|
}
|
|
|
|
public enum CombatAnimationEvent
|
|
{
|
|
CombatCommenceAttack,
|
|
AttackDone,
|
|
AttackerNotification,
|
|
DefenderNotification,
|
|
EvasionAttackerNotification,
|
|
EvasionDefenderNotification,
|
|
VictimNotification,
|
|
KillerNotification,
|
|
}
|
|
|
|
public enum CombatAnimationKind
|
|
{
|
|
None = 0,
|
|
CombatStance,
|
|
MeleeSwing,
|
|
MissileAttack,
|
|
CreatureAttack,
|
|
SpellCast,
|
|
HitReaction,
|
|
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
|
|
{
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
}
|