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));
}
}