using System; using System.Collections.Generic; using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand; namespace AcDream.Core.Physics; /// /// Runtime-default . Built from the /// enum (generated from the protocol XML; /// mirrors ACE's MotionCommand enum and matches the values actually /// found in the local DAT MotionTable records). Use this catalog /// while talking to ACE — it is what /// delegates to. /// /// /// Reconstruction is a flat wireLow16 -> full32 lookup built once /// at construction. When more than one enum value shares the same low 16 /// bits (a true class collision — class byte differs, low word doesn't), /// the LOWER class byte wins: Action (0x10) < ChatEmote (0x12/0x13) /// < Modifier (0x20) < SubState (0x41/...) < ... < Style (0x80). /// See . /// /// /// /// No magic per-range override. The original /// MotionCommandResolver.ApplyNamedRetailOverrides force-mapped /// wire 0x016E-0x0197 to 0x10000000 | lo on the theory that the /// generated DRW enum was "shifted." Verified false: building the lookup /// straight from (Chorizite.DatReaderWriter /// 2.1.7, 409 distinct values, zero same-low16 collisions) already resolves /// LifestoneRecall (0x0153), MarketplaceRecall (0x0166), /// AllegianceHometownRecall (0x0171), and OffhandSlashHigh /// (0x0173) to their correct Action-class full values with no override. /// The override loop is deleted in this slice (it was masking nothing; the /// enum was already correct). See /// docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md. /// /// public sealed class AceModernCommandCatalog : IMotionCommandCatalog { private readonly Dictionary _lookup; public AceModernCommandCatalog() { _lookup = BuildLookup(); } public uint ReconstructFullCommand(ushort wireCommand) { if (wireCommand == 0) return 0u; _lookup.TryGetValue(wireCommand, out var full); return full; } private static Dictionary BuildLookup() { var byLow = new Dictionary>(512); foreach (DRWMotionCommand v in Enum.GetValues(typeof(DRWMotionCommand))) { uint full = (uint)v; ushort lo = (ushort)(full & 0xFFFFu); if (lo == 0) continue; // Invalid / unmappable if (!byLow.TryGetValue(lo, out var list)) byLow[lo] = list = new List(1); if (!list.Contains(full)) list.Add(full); } var result = new Dictionary(byLow.Count); foreach (var (lo, candidates) in byLow) { result[lo] = candidates.Count == 1 ? candidates[0] : ResolveClassPriority(candidates); } return result; } /// /// Given a set of full 32-bit MotionCommand values that all share the /// same low 16 bits, return the one whose class byte (bits 24-31) is /// numerically lowest — retail's class priority (Action 0x10 beats /// Modifier 0x20 beats SubState 0x41 beats Style 0x80, etc). Exposed /// as a static so it can be exercised directly by tests even when the /// live enum has no real collisions to exercise it through /// . /// public static uint ResolveClassPriority(IReadOnlyList candidates) { uint best = candidates[0]; for (int i = 1; i < candidates.Count; i++) { uint candidate = candidates[i]; if ((candidate >> 24) < (best >> 24)) best = candidate; } return best; } }