feat(L.1b): dual MotionCommand catalog — AceModern runtime + Retail2013 conformance
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>
This commit is contained in:
parent
33ad231d18
commit
2c8620ea94
8 changed files with 612 additions and 65 deletions
|
|
@ -1,12 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs the 32-bit retail <see cref="DRWMotionCommand"/> value from
|
||||
/// a 16-bit wire value broadcast in <c>InterpretedMotionState.Commands[]</c>.
|
||||
/// 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
|
||||
|
|
@ -19,11 +15,19 @@ namespace AcDream.Core.Physics;
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This is implemented as an eager lookup table built from all values of
|
||||
/// <see cref="DRWMotionCommand"/> via reflection. If the wire value matches
|
||||
/// more than one enum value (different class bits), we prefer the
|
||||
/// lowest-class-numbered variant that has a non-zero class byte — roughly
|
||||
/// matching retail priority (Action < Modifier < SubState < Style).
|
||||
/// 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>
|
||||
|
|
@ -42,66 +46,26 @@ namespace AcDream.Core.Physics;
|
|||
/// <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
|
||||
{
|
||||
// Lookup table built eagerly at type-init. Sparse: only values that
|
||||
// appear in the DRW enum (which came from the generated protocol XML)
|
||||
// are present. ~450 entries typical.
|
||||
private static readonly Dictionary<ushort, uint> s_lookup = BuildLookup();
|
||||
private static readonly AceModernCommandCatalog s_aceModern = new();
|
||||
|
||||
/// <summary>
|
||||
/// Given a 16-bit wire value, return the full 32-bit MotionCommand
|
||||
/// (class byte restored). Returns 0 if no matching enum value exists.
|
||||
/// (class byte restored) per the ACE/runtime catalog. Returns 0 if no
|
||||
/// matching value exists.
|
||||
/// </summary>
|
||||
public static uint ReconstructFullCommand(ushort wireCommand)
|
||||
{
|
||||
if (wireCommand == 0) return 0u;
|
||||
s_lookup.TryGetValue(wireCommand, out var full);
|
||||
return full;
|
||||
}
|
||||
|
||||
private static Dictionary<ushort, uint> BuildLookup()
|
||||
{
|
||||
var result = new Dictionary<ushort, uint>(512);
|
||||
var values = Enum.GetValues(typeof(DRWMotionCommand));
|
||||
foreach (DRWMotionCommand v in values)
|
||||
{
|
||||
uint full = (uint)v;
|
||||
ushort lo = (ushort)(full & 0xFFFFu);
|
||||
if (lo == 0) continue; // Invalid / unmappable
|
||||
|
||||
// If a value with this low-16-bit already exists, keep the one
|
||||
// with the lower class byte (Action=0x10 beats SubState=0x41
|
||||
// beats Style=0x80). This matches retail: the server tends to
|
||||
// emit Actions and ChatEmotes far more often than Styles, so
|
||||
// the Action-class reconstruction is the common case.
|
||||
if (!result.TryGetValue(lo, out var existing)
|
||||
|| (full >> 24) < (existing >> 24))
|
||||
{
|
||||
result[lo] = full;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyNamedRetailOverrides(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ApplyNamedRetailOverrides(Dictionary<ushort, uint> result)
|
||||
{
|
||||
// The generated DRW enum is shifted by three entries starting at
|
||||
// AllegianceHometownRecall. The named Sept 2013 retail command_ids
|
||||
// table is authoritative here:
|
||||
// named-retail/acclient_2013_pseudo_c.txt lines 1017626-1017658
|
||||
// and command-name table lines 1068272-1068313.
|
||||
//
|
||||
// These values cover recall, offhand, attack 4-6, and fast/slow punch
|
||||
// actions. Without the override, wire command 0x0170 reconstructs to
|
||||
// IssueSlashCommand instead of OffhandSlashHigh, so offhand swing
|
||||
// animations route as UI commands and never play.
|
||||
for (ushort lo = 0x016E; lo <= 0x0197; lo++)
|
||||
result[lo] = 0x10000000u | lo;
|
||||
return s_aceModern.ReconstructFullCommand(wireCommand);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue