using System.Collections.Generic; using System.Linq; using AcDream.Content; using AcDream.Core.Input; using AcDream.UI.Abstractions.Input; using DatReaderWriter; using Xunit; namespace AcDream.Core.Tests.Input; /// /// Campaign OP slice OP8: pins 's agreement /// with — for every /// this slice's table resolves, the UNION of DAT default bindings across every DAT /// row mapped to that action must equal KeyBindings.RetailDefaults()'s chord /// set for it. Per the slice contract: "investigate + report any disagreement rather /// than silently preferring one." Skips cleanly when the installed dats are /// unavailable (CI), matching every other live-DAT conformance test in this project. /// /// /// Two real, byte-verified disagreements survive after the mechanism fixes /// (2026-08-11 investigation, updated at the M2 rework — none are bugs in this /// slice's table; both are PRE-EXISTING /// gaps/design choices this slice does not touch, listed in /// with citations). A THIRD /// disagreement — ten CameraAlternateControls (InputMap 0x6) actions — was RETIRED /// at the M2 rework: no longer maps InputMap /// 0x6 to any at all (the aliasing that produced two /// independent rows fighting over one live target — M2, 2026-08-11 review), so this /// test never sees a ctx-0x6 row and the ctx-0x5-only union now matches /// RetailDefaults() exactly for all twelve Camera actions with no allowlist /// entry needed: /// /// /// MovementWalkMode. The DAT's raw QualifiedControl.Modifier /// for the Shift-key binding is 0 (the key itself IS Shift — there is no separate /// "modifier" to report when the primary key and the modifier are the same physical /// key). RetailDefaults() deliberately encodes Modifiers=Shift anyway — /// its own comment (K-fix1, 2026-04-26) explains the OS echoes /// CurrentModifiers=Shift alongside a Shift key-DOWN event, so the chord must /// carry the flag to match at dispatch time. Not a disagreement to fix; a raw-DAT /// artifact this slice's reader faithfully reproduces. /// Quickslot 1-9's Ctrl+N chord (and its SelectQuickSlot_1-9 /// counterpart). The DAT's own default /// master map binds Ctrl+1..9 to the SAME action id as bare 1..9 ("Quickslot N" — /// UseQuickSlot_N), NOT to the separate "Select Quickslot N" action id /// (SelectQuickSlot_N, DAT action ids 0x1000004E-56) — those carry NO /// default binding at all in the shipped DAT. RetailDefaults()'s own comment /// (citing gmToolbarUI::ListenToGlobalMessage @0x004BE4E0) asserts retail's /// CLIENT reinterprets Ctrl+N contextually as Select — a runtime behavior this raw /// keymap-default probe cannot see (it reads bound ACTIONS, not the dispatch /// function's own modifier branching). Both readings are independently retail- /// sourced; reconciling them needs the decompiled dispatch function, out of scope /// here. Reported, not silently resolved either way. /// /// public sealed class RetailActionIdentityRoundTripTests { /// Actions with a citation-backed, pre-existing reason their DAT-union /// default set legitimately differs from /// — see class doc. Every other mapped action must match exactly. private static readonly HashSet KnownRetailDefaultsDisagreements = new() { InputAction.MovementWalkMode, InputAction.UseQuickSlot_1, InputAction.UseQuickSlot_2, InputAction.UseQuickSlot_3, InputAction.UseQuickSlot_4, InputAction.UseQuickSlot_5, InputAction.UseQuickSlot_6, InputAction.UseQuickSlot_7, InputAction.UseQuickSlot_8, InputAction.UseQuickSlot_9, // Same Use-vs-Select ambiguity as the bare-numeral block above: the DAT's // own "Select Quickslot N" action ids carry NO default binding at all — // RetailDefaults()'s Ctrl+N->Select mapping rests on the decompiled // dispatch function's runtime modifier check, not the raw keymap default. InputAction.SelectQuickSlot_1, InputAction.SelectQuickSlot_2, InputAction.SelectQuickSlot_3, InputAction.SelectQuickSlot_4, InputAction.SelectQuickSlot_5, InputAction.SelectQuickSlot_6, InputAction.SelectQuickSlot_7, InputAction.SelectQuickSlot_8, InputAction.SelectQuickSlot_9, }; [Fact] public void MappedActions_DatUnionDefaultBindings_MatchRetailDefaults() { string? datDir = Conformance.ConformanceDats.ResolveDatDir(); if (datDir is null) return; using var dats = new DatCollection(datDir); var source = new DatCollectionAdapter(dats); RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(source); Assert.NotNull(snapshot); 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.True(datChordsByAction.Count > 100, $"expected >100 mapped actions, got {datChordsByAction.Count}"); Assert.Empty(unresolvedScanCodes); var mismatches = new List(); foreach ((InputAction action, HashSet datChords) in datChordsByAction) { if (KnownRetailDefaultsDisagreements.Contains(action)) continue; 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} unexpected DAT-vs-RetailDefaults() disagreements " + "(not in the documented KnownRetailDefaultsDisagreements allowlist):\n" + string.Join("\n", mismatches)); } }