using AcDream.App.Platform; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.App.UI.Layout; /// /// Campaign OP slice OP8 re-gate (2026-08-14): retail's key-binding display /// names — the port of CInputManager_WIN32::GetNameFromKey @ 0x00687F40 /// (QualifiedControl overload) over /// GetNameFromKey_Internal @ 0x00687800. /// /// /// Retail's name for one bound control resolves in this order: /// (1) the authored DAT string table, keyed by the ELF hash of the control's /// DirectInput name (ControlSpecification::GetDIKName @ 0x0068ACB0 — /// "DIK_W", "DIK_LSHIFT", ...): plain keys through string-table enum 4 and /// meta keys through enum 5 (DBCache::GetDIDFromEnumStatic category 4 /// resolves those to DIDs 0x2300000A / 0x2300000B on the shipped /// dats — live-probed 2026-08-14; the shipped tables author overrides for /// exactly DIK_LCONTROL → "Left Ctrl" and DIK_LMENU → "Left Alt"); /// (2) the OS's own localized key name — retail asks DirectInput /// (IDirectInputDevice8::GetObjectInfo, DIPH_BYOFFSET) and shows /// tszName, which is why a Swedish layout displays "SKIFT". This port /// asks Win32 GetKeyNameTextW through /// instead — DirectInput's key names /// come from the identical keyboard-layout data, and acdream has no /// DirectInput device (register row AD-96). On non-Windows hosts the OS half /// is unavailable and the DIK-suffix spelling is the honest fallback (same /// register row). /// /// /// /// A chord's modifier prefix comes from the QualifiedControl overload: each /// set meta-mode bit, ascending (Shift=1, Ctrl=2, Alt=4 — the shipped /// keymap's own Metakeys header maps those bits to DIK_LSHIFT / DIK_LCONTROL /// / DIK_LMENU), names its METAKEY through the meta table + OS fallback and /// joins with the authored ID_KeyDescDelimiter ("+", table enum 3 → /// DID 0x23000007). A binding whose KEY IS a modifier key (retail's /// walk-mode DIK_LSHIFT row has meta-mode 0; acdream's /// carries the wire-side self-modifier bit) shows only the key name — never /// "Shift+ShiftLeft". /// /// public sealed class RetailKeyNames { /// String-table enum 4 via GetDIDFromEnumStatic category 4. public const uint KeyNameTableId = 0x2300000Au; /// String-table enum 5 via GetDIDFromEnumStatic category 4. public const uint MetaKeyNameTableId = 0x2300000Bu; /// String-table enum 3 via GetDIDFromEnumStatic category 4 — /// holds ID_KeyDescDelimiter. public const uint DelimiterTableId = 0x23000007u; private readonly Func _resolveString; private readonly Func? _osKeyName; private readonly string _delimiter; /// DAT string lookup — (tableDid, stringHash) → /// text, the same seam KeyboardConfigController already receives. /// OS-localized key-name lookup — (scanCode, /// isExtended) → name. Null selects /// (Win32 /// GetKeyNameTextW on Windows, no OS lookup elsewhere). Tests /// inject deterministic fakes here. public RetailKeyNames( Func resolveString, Func? osKeyName = null) { _resolveString = resolveString ?? throw new ArgumentNullException(nameof(resolveString)); _osKeyName = osKeyName ?? PlatformKeyNameProvider.ForCurrentProcess(); _delimiter = resolveString( DelimiterTableId, DatStringResolver.ComputeHash("ID_KeyDescDelimiter")) ?? "+"; } /// /// Display name for one bound chord — retail /// GetNameFromKey(QualifiedControl). Mouse chords keep the /// pre-existing enum spelling: retail names mouse controls through the /// DirectInput mouse device, which this port does not have (AD-95a). /// public string Describe(KeyChord chord) { if (chord == default) return string.Empty; if (!TryGetDik(chord.Key, out byte dik, out string? dikName)) return FallbackSpelling(chord); var composed = new System.Text.StringBuilder(); // Meta-mode bits ascending, skipping the key's own self-modifier bit // (retail's walk-mode LSHIFT row carries meta-mode 0 on the wire; the // chord's stored self bit is acdream's encoding, not display truth). foreach ((ModifierMask flag, Key metaKey) in MetaOrder) { if ((chord.Modifiers & flag) == 0 || IsSelfModifier(chord.Key, flag)) continue; if (!TryGetDik(metaKey, out byte metaDik, out string? metaDikName)) continue; composed.Append(LookupName(metaDikName!, metaDik, MetaKeyNameTableId)); composed.Append(_delimiter); } composed.Append(LookupName(dikName!, dik, KeyNameTableId)); return composed.ToString(); } private string LookupName(string dikName, byte dik, uint tableId) => _resolveString(tableId, DatStringResolver.ComputeHash(dikName)) ?? _osKeyName?.Invoke((byte)(dik & 0x7F), (dik & 0x80) != 0) ?? dikName["DIK_".Length..]; private static string FallbackSpelling(KeyChord chord) { string mods = chord.Modifiers == ModifierMask.None ? "" : chord.Modifiers.ToString() + "+"; return mods + chord.Key; } private static readonly (ModifierMask Flag, Key MetaKey)[] MetaOrder = { (ModifierMask.Shift, Key.ShiftLeft), (ModifierMask.Ctrl, Key.ControlLeft), (ModifierMask.Alt, Key.AltLeft), }; private static bool IsSelfModifier(Key key, ModifierMask flag) => flag switch { ModifierMask.Shift => key is Key.ShiftLeft or Key.ShiftRight, ModifierMask.Ctrl => key is Key.ControlLeft or Key.ControlRight, ModifierMask.Alt => key is Key.AltLeft or Key.AltRight, _ => false, }; /// /// Silk key → DirectInput scan code + DIK name — the reverse of /// 's keyboard table (same 84 /// DAT-observed codes) plus the modifier keys live capture can produce /// that no DAT default binds directly (DIK_LCONTROL 0x1D, DIK_LMENU 0x38, /// DIK_RMENU 0xB8). DIK codes with bit 0x80 are the extended set — the /// same split Win32's GetKeyNameText expects in bit 24. /// private static bool TryGetDik(Key key, out byte dik, out string? name) { (dik, name) = key switch { Key.Escape => ((byte)0x01, "DIK_ESCAPE"), Key.Number1 => ((byte)0x02, "DIK_1"), Key.Number2 => ((byte)0x03, "DIK_2"), Key.Number3 => ((byte)0x04, "DIK_3"), Key.Number4 => ((byte)0x05, "DIK_4"), Key.Number5 => ((byte)0x06, "DIK_5"), Key.Number6 => ((byte)0x07, "DIK_6"), Key.Number7 => ((byte)0x08, "DIK_7"), Key.Number8 => ((byte)0x09, "DIK_8"), Key.Number9 => ((byte)0x0A, "DIK_9"), Key.Number0 => ((byte)0x0B, "DIK_0"), Key.Minus => ((byte)0x0C, "DIK_MINUS"), Key.Equal => ((byte)0x0D, "DIK_EQUALS"), Key.Backspace => ((byte)0x0E, "DIK_BACK"), Key.Tab => ((byte)0x0F, "DIK_TAB"), Key.Q => ((byte)0x10, "DIK_Q"), Key.W => ((byte)0x11, "DIK_W"), Key.E => ((byte)0x12, "DIK_E"), Key.R => ((byte)0x13, "DIK_R"), Key.T => ((byte)0x14, "DIK_T"), Key.Y => ((byte)0x15, "DIK_Y"), Key.U => ((byte)0x16, "DIK_U"), Key.I => ((byte)0x17, "DIK_I"), Key.O => ((byte)0x18, "DIK_O"), Key.P => ((byte)0x19, "DIK_P"), Key.LeftBracket => ((byte)0x1A, "DIK_LBRACKET"), Key.RightBracket => ((byte)0x1B, "DIK_RBRACKET"), Key.Enter => ((byte)0x1C, "DIK_RETURN"), Key.ControlLeft => ((byte)0x1D, "DIK_LCONTROL"), Key.A => ((byte)0x1E, "DIK_A"), Key.S => ((byte)0x1F, "DIK_S"), Key.D => ((byte)0x20, "DIK_D"), Key.F => ((byte)0x21, "DIK_F"), Key.G => ((byte)0x22, "DIK_G"), Key.H => ((byte)0x23, "DIK_H"), Key.J => ((byte)0x24, "DIK_J"), Key.K => ((byte)0x25, "DIK_K"), Key.L => ((byte)0x26, "DIK_L"), Key.Semicolon => ((byte)0x27, "DIK_SEMICOLON"), Key.Apostrophe => ((byte)0x28, "DIK_APOSTROPHE"), Key.GraveAccent => ((byte)0x29, "DIK_GRAVE"), Key.ShiftLeft => ((byte)0x2A, "DIK_LSHIFT"), Key.BackSlash => ((byte)0x2B, "DIK_BACKSLASH"), Key.Z => ((byte)0x2C, "DIK_Z"), Key.X => ((byte)0x2D, "DIK_X"), Key.C => ((byte)0x2E, "DIK_C"), Key.V => ((byte)0x2F, "DIK_V"), Key.B => ((byte)0x30, "DIK_B"), Key.N => ((byte)0x31, "DIK_N"), Key.M => ((byte)0x32, "DIK_M"), Key.Comma => ((byte)0x33, "DIK_COMMA"), Key.Period => ((byte)0x34, "DIK_PERIOD"), Key.Slash => ((byte)0x35, "DIK_SLASH"), Key.ShiftRight => ((byte)0x36, "DIK_RSHIFT"), Key.KeypadMultiply => ((byte)0x37, "DIK_MULTIPLY"), Key.AltLeft => ((byte)0x38, "DIK_LMENU"), Key.Space => ((byte)0x39, "DIK_SPACE"), Key.F1 => ((byte)0x3B, "DIK_F1"), Key.F2 => ((byte)0x3C, "DIK_F2"), Key.F3 => ((byte)0x3D, "DIK_F3"), Key.F4 => ((byte)0x3E, "DIK_F4"), Key.F5 => ((byte)0x3F, "DIK_F5"), Key.F6 => ((byte)0x40, "DIK_F6"), Key.F7 => ((byte)0x41, "DIK_F7"), Key.F8 => ((byte)0x42, "DIK_F8"), Key.F9 => ((byte)0x43, "DIK_F9"), Key.F10 => ((byte)0x44, "DIK_F10"), Key.NumLock => ((byte)0x45, "DIK_NUMLOCK"), Key.ScrollLock => ((byte)0x46, "DIK_SCROLL"), Key.Keypad7 => ((byte)0x47, "DIK_NUMPAD7"), Key.Keypad8 => ((byte)0x48, "DIK_NUMPAD8"), Key.Keypad9 => ((byte)0x49, "DIK_NUMPAD9"), Key.KeypadSubtract => ((byte)0x4A, "DIK_SUBTRACT"), Key.Keypad4 => ((byte)0x4B, "DIK_NUMPAD4"), Key.Keypad5 => ((byte)0x4C, "DIK_NUMPAD5"), Key.Keypad6 => ((byte)0x4D, "DIK_NUMPAD6"), Key.KeypadAdd => ((byte)0x4E, "DIK_ADD"), Key.Keypad1 => ((byte)0x4F, "DIK_NUMPAD1"), Key.Keypad2 => ((byte)0x50, "DIK_NUMPAD2"), Key.Keypad3 => ((byte)0x51, "DIK_NUMPAD3"), Key.Keypad0 => ((byte)0x52, "DIK_NUMPAD0"), Key.KeypadDecimal => ((byte)0x53, "DIK_DECIMAL"), Key.F11 => ((byte)0x57, "DIK_F11"), Key.F12 => ((byte)0x58, "DIK_F12"), Key.KeypadEnter => ((byte)0x9C, "DIK_NUMPADENTER"), Key.ControlRight => ((byte)0x9D, "DIK_RCONTROL"), Key.KeypadDivide => ((byte)0xB5, "DIK_DIVIDE"), Key.AltRight => ((byte)0xB8, "DIK_RMENU"), Key.Home => ((byte)0xC7, "DIK_HOME"), Key.Up => ((byte)0xC8, "DIK_UP"), Key.PageUp => ((byte)0xC9, "DIK_PRIOR"), Key.Left => ((byte)0xCB, "DIK_LEFT"), Key.Right => ((byte)0xCD, "DIK_RIGHT"), Key.End => ((byte)0xCF, "DIK_END"), Key.Down => ((byte)0xD0, "DIK_DOWN"), Key.PageDown => ((byte)0xD1, "DIK_NEXT"), Key.Insert => ((byte)0xD2, "DIK_INSERT"), Key.Delete => ((byte)0xD3, "DIK_DELETE"), _ => ((byte)0, null), }; return name is not null; } }