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