using System.Collections.Generic; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Validates the dual command-catalog seam introduced for the L.1b /// movement/animation wire-parity slice (IMotionCommandCatalog). /// /// /// Two catalogs exist because the local DATs and ACE's wire protocol use a /// LATER-client command numbering than the Sept 2013 EoR decomp: /// (runtime default — matches ACE + /// local DAT MotionTables) and /// (conformance/reference — matches the 2013 decomp's /// command_ids[0x198] table verbatim). See /// docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md for the /// full divergence analysis (130 common names with different values; the /// contiguous low-word "+3" shift that begins at SnowAngelState /// (0x43000115 -> 0x43000118), NOT at AllegianceHometownRecall as an /// earlier code comment incorrectly claimed). /// /// public class MotionCommandCatalogTests { private static readonly AceModernCommandCatalog AceModern = new(); private static readonly Retail2013CommandCatalog Retail2013 = new(); // ── ACE mode (AceModernCommandCatalog) ────────────────────────────── // // These four wires sit inside the old override range (0x016E-0x0197) // that MotionCommandResolver used to force-map via a blind per-range // override. Built cleanly from the DatReaderWriter MotionCommand enum, // they already resolve correctly with NO override: // LifestoneRecall = 0x10000153, MarketplaceRecall = 0x10000166, // AllegianceHometownRecall = 0x10000171, OffhandSlashHigh = 0x10000173 // (DatReaderWriter.Enums.MotionCommand, Chorizite.DatReaderWriter 2.1.7). [Theory] [InlineData(0x0153, 0x10000153u)] // LifestoneRecall [InlineData(0x0166, 0x10000166u)] // MarketplaceRecall [InlineData(0x0171, 0x10000171u)] // AllegianceHometownRecall [InlineData(0x0173, 0x10000173u)] // OffhandSlashHigh public void AceModern_ResolvesShiftedRecallAndActionCommands(ushort wire, uint expected) { Assert.Equal(expected, AceModern.ReconstructFullCommand(wire)); } // Same matrix as MotionCommandResolverTests.ReconstructsKnownCommands — // proves AceModernCommandCatalog reproduces the existing resolver's // behavior for the well-established low end of the command space. [Theory] [InlineData(0x0003, 0x41000003u)] // Ready [InlineData(0x0005, 0x45000005u)] // WalkForward [InlineData(0x0007, 0x44000007u)] // RunForward [InlineData(0x0006, 0x45000006u)] // WalkBackward [InlineData(0x000D, 0x6500000Du)] // TurnRight [InlineData(0x000E, 0x6500000Eu)] // TurnLeft [InlineData(0x000F, 0x6500000Fu)] // SideStepRight [InlineData(0x0015, 0x40000015u)] // Falling [InlineData(0x0011, 0x40000011u)] // Dead [InlineData(0x0012, 0x41000012u)] // Crouch [InlineData(0x0013, 0x41000013u)] // Sitting [InlineData(0x0014, 0x41000014u)] // Sleeping [InlineData(0x0057, 0x10000057u)] // Sanctuary (death) [InlineData(0x0058, 0x10000058u)] // ThrustMed [InlineData(0x005B, 0x1000005Bu)] // SlashHigh [InlineData(0x0061, 0x10000061u)] // Shoot [InlineData(0x004B, 0x1000004Bu)] // Jumpup [InlineData(0x0050, 0x10000050u)] // FallDown [InlineData(0x0087, 0x13000087u)] // Wave [InlineData(0x0080, 0x13000080u)] // Laugh [InlineData(0x007D, 0x1300007Du)] // BowDeep public void AceModern_MatchesExistingResolverMatrix(ushort wire, uint expected) { Assert.Equal(expected, AceModern.ReconstructFullCommand(wire)); } // ── 2013 mode (Retail2013CommandCatalog) ──────────────────────────── // // Direct-index reconstruction from acclient_2013_pseudo_c.txt's // command_ids[0x198] table at 0x007c73e8 (line 1017259). The 2013 // decomp's *unshifted* values for the same four logical commands. [Theory] [InlineData(0x0150, 0x10000150u)] // LifestoneRecall (2013 value) [InlineData(0x0163, 0x10000163u)] // MarketplaceRecall (2013 value) [InlineData(0x016E, 0x1000016Eu)] // AllegianceHometownRecall (2013 value) [InlineData(0x0170, 0x10000170u)] // OffhandSlashHigh (2013 value) public void Retail2013_ResolvesUnshiftedRecallAndActionCommands(ushort wire, uint expected) { Assert.Equal(expected, Retail2013.ReconstructFullCommand(wire)); } // Anchor checks straight off the extracted table — these pin the // extraction itself, independent of the recall-command narrative. [Theory] [InlineData(0x0000, 0x80000000u)] [InlineData(0x0003, 0x41000003u)] [InlineData(0x0005, 0x45000005u)] [InlineData(0x0007, 0x44000007u)] [InlineData(0x000D, 0x6500000Du)] [InlineData(0x0150, 0x10000150u)] [InlineData(0x0153, 0x09000153u)] // NOT LifestoneRecall in 2013 numbering — anchor only. public void Retail2013_AnchorValuesMatchExtractedTable(ushort wire, uint expected) { Assert.Equal(expected, Retail2013.ReconstructFullCommand(wire)); } [Fact] public void Retail2013_OutOfRangeWireReturnsZero() { Assert.Equal(0u, Retail2013.ReconstructFullCommand(0xFFFF)); } [Fact] public void Retail2013_LastInRangeIndexResolves() { // [0x197] is the final entry of command_ids[0x198]. Assert.Equal(0x10000197u, Retail2013.ReconstructFullCommand(0x0197)); } [Fact] public void Retail2013_FirstOutOfRangeIndexReturnsZero() { // 0x198 is one past the table's last valid index. Assert.Equal(0u, Retail2013.ReconstructFullCommand(0x0198)); } // ── Class-priority collision resolution ───────────────────────────── // // Retail's class-byte priority is: lower class byte wins // (Action 0x10 < ChatEmote 0x12/0x13 < Modifier 0x20 < SubState 0x41 // < ... < Style 0x80). This is exercised directly against the build // logic via a small synthetic set of colliding low-words, because the // CURRENT DatReaderWriter.Enums.MotionCommand enum (2.1.7, 409 distinct // values) happens to contain zero same-low16 collisions today — so a // matrix test against the live enum alone wouldn't actually exercise // the tie-break rule. [Fact] public void ClassPriority_LowerClassByteWins() { var candidates = new Dictionary { // Same low word (0x0042) claimed by three different classes; // Action (0x10) must win over Modifier (0x20) and Style (0x80). [0x0042] = 0u, }; uint[] colliding = { 0x80000042u, 0x20000042u, 0x10000042u }; uint resolved = AceModernCommandCatalog.ResolveClassPriority(colliding); Assert.Equal(0x10000042u, resolved); } [Fact] public void ClassPriority_OrderOfInputDoesNotMatter() { uint[] collidingReversed = { 0x10000099u, 0x41000099u, 0x80000099u }; uint[] collidingForward = { 0x80000099u, 0x41000099u, 0x10000099u }; Assert.Equal( AceModernCommandCatalog.ResolveClassPriority(collidingForward), AceModernCommandCatalog.ResolveClassPriority(collidingReversed)); Assert.Equal(0x10000099u, AceModernCommandCatalog.ResolveClassPriority(collidingForward)); } }