Splits MotionCommandResolver's single ACE-modern lookup into an IMotionCommandCatalog seam with two implementations: - AceModernCommandCatalog (runtime default): built cleanly from the DatReaderWriter MotionCommand enum (mirrors ACE + local DAT MotionTables) with a documented class-priority tiebreak. The old blind 0x016E-0x0197 per-range override is DELETED — verified the ACE matrix (LifestoneRecall 0x0153 to 0x10000153, MarketplaceRecall 0x0166, AllegianceHometownRecall 0x0171, OffhandSlashHigh 0x0173) resolves correctly straight from the enum with no override. Stale shift-start comment corrected to SnowAngelState (0x43000115 to 0x43000118), not AllegianceHometownRecall. - Retail2013CommandCatalog (conformance/reference): full verbatim extraction of command_ids[0x198] at 0x007c73e8 (acclient_2013_pseudo_c.txt:1017259-1017667), direct wire-low to full index lookup. 408 entries, anchors verified against source ([0x150]=0x10000150, [0x153]=0x09000153, [0x197]=0x10000197). MotionCommandResolver.ReconstructFullCommand stays a static facade delegating to an AceModern singleton — all ~10 runtime callers unchanged. Removing the override exposed a pre-existing bug: CombatAnimationPlanner's late-combat command block uses 2013 numbering, not ACE/DRW. Corrected the one catalog test assertion pinned to the override's output (wire 0x0170 to 0x09000170 IssueSlashCommand, its true DRW identity) and filed the planner bug as #159 rather than silently patching out-of-scope code. Tests: +catalog matrices (ACE/2013), class-priority collisions, boundary cases, real-DAT availability (gap-doc hit counts reproduced). Build + full suite green (Core.Tests 1718, no regressions). Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md (1) Research: docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.1 KiB
C#
71 lines
3.1 KiB
C#
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// Reconstructs the 32-bit retail MotionCommand value from a 16-bit wire
|
|
/// value broadcast in <c>InterpretedMotionState.Commands[]</c>.
|
|
///
|
|
/// <para>
|
|
/// The server serializes MotionCommands as <c>u16</c> (ACE
|
|
/// <c>InterpretedMotionState.cs:139</c>), truncating the class byte (Style /
|
|
/// SubState / Modifier / Action / ChatEmote / UI / Toggle / Mappable /
|
|
/// Command — see r03 §3.1). The client must re-attach the class byte before
|
|
/// routing the command into the motion table, because the same low 16 bits
|
|
/// can map to different classes (e.g. 0x0003 is <c>Ready</c> as a SubState,
|
|
/// but there's no other 0x0003).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// As of the L.1b command-catalog slice, this static facade delegates to a
|
|
/// single shared <see cref="AceModernCommandCatalog"/> instance — the
|
|
/// runtime-default catalog built from the DatReaderWriter
|
|
/// <c>MotionCommand</c> enum (matches ACE + the local DATs). All ~10
|
|
/// existing runtime callers (<c>AnimationCommandRouter</c>,
|
|
/// <c>CombatAnimationPlanner</c>, 8x <c>GameWindow</c>) are unaffected by
|
|
/// this refactor — the public signature and behavior for the ACE/runtime
|
|
/// path is unchanged. A second catalog,
|
|
/// <see cref="Retail2013CommandCatalog"/>, is available for
|
|
/// conformance/reference work against the Sept 2013 EoR decomp's own
|
|
/// (differently-numbered) command table; callers that need that catalog
|
|
/// must construct it directly via the <see cref="IMotionCommandCatalog"/>
|
|
/// seam — this facade only ever returns ACE/runtime values.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Cited references:
|
|
/// <list type="bullet">
|
|
/// <item><description>
|
|
/// <c>references/ACE/Source/ACE.Server/Network/Motion/InterpretedMotionState.cs::Write</c>
|
|
/// L138-L144 — writer emits u16 for every command field.
|
|
/// </description></item>
|
|
/// <item><description>
|
|
/// <c>references/ACE/Source/ACE.Entity/Enum/CommandMasks.cs</c> — the
|
|
/// class bit assignments: 0x80=Style, 0x40=SubState, 0x20=Modifier,
|
|
/// 0x10=Action, 0x13 and 0x12=ChatEmote (with Mappable set), etc.
|
|
/// </description></item>
|
|
/// <item><description>
|
|
/// <c>docs/research/deepdives/r03-motion-animation.md</c> §3 — complete
|
|
/// command catalogue.
|
|
/// </description></item>
|
|
/// <item><description>
|
|
/// <c>docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md</c> —
|
|
/// the ACE-vs-2013 catalog divergence that motivated the dual-catalog
|
|
/// seam (the shift begins at <c>SnowAngelState</c>, 0x43000115 ->
|
|
/// 0x43000118, not at <c>AllegianceHometownRecall</c>).
|
|
/// </description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </summary>
|
|
public static class MotionCommandResolver
|
|
{
|
|
private static readonly AceModernCommandCatalog s_aceModern = new();
|
|
|
|
/// <summary>
|
|
/// Given a 16-bit wire value, return the full 32-bit MotionCommand
|
|
/// (class byte restored) per the ACE/runtime catalog. Returns 0 if no
|
|
/// matching value exists.
|
|
/// </summary>
|
|
public static uint ReconstructFullCommand(ushort wireCommand)
|
|
{
|
|
return s_aceModern.ReconstructFullCommand(wireCommand);
|
|
}
|
|
}
|