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:
Erik 2026-06-30 22:13:36 +02:00
parent 33ad231d18
commit 2c8620ea94
8 changed files with 612 additions and 65 deletions

View file

@ -30,11 +30,35 @@ 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.
[Theory]
[InlineData(0x0170, 0x10000170u)] // OffhandSlashHigh
[InlineData(0x017D, 0x1000017Du)] // OffhandDoubleThrustMed
[InlineData(0x018B, 0x1000018Bu)] // AttackLow6
[InlineData(0x018E, 0x1000018Eu)] // PunchFastLow
[InlineData(0x0170, 0x09000170u)] // IssueSlashCommand (UI class) — see note above
[InlineData(0x017D, 0x1000017Du)] // OffhandTripleSlashMed
[InlineData(0x018B, 0x1000018Bu)] // AttackLow5
[InlineData(0x018E, 0x1000018Eu)] // AttackLow6
public void MotionCommandResolver_UsesNamedRetailLateCombatCommands(
ushort wireCommand,
uint expectedFullCommand)