using System.Collections.Generic; using System.Linq; using AcDream.Content; using AcDream.Core.Input; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions.Input; using DatReaderWriter; using Xunit; namespace AcDream.Core.Tests.Input; /// /// Campaign KB's installed-DAT contract: all 306 user-bindable ActionMap rows /// have distinct live identities and their default chord sets match the two /// retail MasterInputMaps exactly. Missing, aliased, or guessed rows fail here. /// [Trait("Lane", "InstalledDat")] public sealed class RetailActionIdentityRoundTripTests { [Fact] public void MappedActions_DatUnionDefaultBindings_MatchRetailDefaults() { string? datDir = Conformance.ConformanceDats.ResolveDatDir(); if (datDir is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); using var dats = new DatCollection(datDir); var source = new DatCollectionAdapter(dats); RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(source); Assert.NotNull(snapshot); Assert.Equal(306, snapshot!.Rows.Count); var unresolvedRows = snapshot.Rows .Where(row => !RetailActionIdentityTable.TryResolve( row.InputMapId, row.ActionId, out _)) .Select(row => $"0x{row.InputMapId:X8}/0x{row.ActionId:X8}") .ToArray(); Assert.Empty(unresolvedRows); Assert.Equal(306, RetailActionIdentityTable.Map.Count); Assert.Equal(306, RetailActionIdentityTable.Map.Values.Distinct().Count()); Assert.Equal(306, RetailActionIdentityTable.ReverseMap.Count); var optionIds = new HashSet(); foreach (RetailActionMapRow row in snapshot.Rows.Where( static row => row.InputMapId == 0x10000008u)) { Assert.True(RetailActionIdentityTable.TryResolve( row.InputMapId, row.ActionId, out InputAction action)); Assert.True( RetailActionIdentityTable.TryGetCharacterOptionId( action, out uint optionId), $"CharacterSettings row 0x{row.ActionId:X8} has no PlayerOption id"); Assert.True(CharacterOptionTable.TryGet(optionId, out _)); Assert.True(optionIds.Add(optionId), $"duplicate PlayerOption id 0x{optionId:X2}"); } Assert.Equal(48, optionIds.Count); KeyBindings retailDefaults = KeyBindings.RetailDefaults(); // Aggregate DAT default chords by resolved InputAction — a single action can // be reached by more than one DAT row (e.g. the Camera/CameraAlternate pair). var datChordsByAction = new Dictionary>(); var unresolvedScanCodes = new List(); foreach (RetailActionMapRow row in snapshot.Rows) { if (!RetailActionIdentityTable.TryResolve(row.InputMapId, row.ActionId, out InputAction action)) continue; if (!datChordsByAction.TryGetValue(action, out HashSet? set)) datChordsByAction[action] = set = new HashSet(); foreach (RetailKeyChord raw in row.DefaultBindings) { Silk.NET.Input.Key? key = RetailScanCodeMap.ToSilkKey(raw.Scan, raw.Device); if (key is null) { unresolvedScanCodes.Add( $"{action}: DAT default scan=0x{raw.Scan:X2} dev={raw.Device} has no " + "RetailScanCodeMap entry"); continue; } set.Add(new KeyChord(key.Value, RetailScanCodeMap.ToModifierMask(raw.Modifier), (byte)raw.Device)); } } Assert.Equal(306, datChordsByAction.Count); Assert.Empty(unresolvedScanCodes); var mismatches = new List(); foreach ((InputAction action, HashSet datChords) in datChordsByAction) { var acdreamChords = retailDefaults.ForAction(action).Select(b => b.Chord).ToHashSet(); if (!datChords.SetEquals(acdreamChords)) { mismatches.Add( $"{action}: DAT union=[{string.Join(",", datChords)}] vs " + $"RetailDefaults()=[{string.Join(",", acdreamChords)}]"); } } Assert.True(mismatches.Count == 0, $"{mismatches.Count} DAT-vs-RetailDefaults() disagreements:\n" + string.Join("\n", mismatches)); } }