Three findings from the user's first Configure Keyboard look (OP8 gate, 2026-08-14), each root-caused against the named retail decomp: - #394 row-caption font: the synthesized action-label UiText never set DatFont and fell to the debug bitmap font. The authored row template (0x21000009/0x1000002F, retail UIOption_ActionKeyMap) carries FontDid 0x4000000A (18px serif) — Bind now takes resolveTemplateFont and applies the template's own authored font, resolved once per template pair. - #395 key captions: raw enum spellings ("Shift+ShiftLeft") replaced by the port of CInputManager_WIN32::GetNameFromKey @0x00687F40 / GetNameFromKey_Internal @0x00687800 (RetailKeyNames): DAT string-table override by DIK-name hash (key enum 4 -> 0x2300000A, meta enum 5 -> 0x2300000B, delimiter enum 3 -> 0x23000007 — GetDIDByEnum category 4, live-probed), else the OS keyboard layout's own key name ("SKIFT") via PlatformKeyNameProvider (Win32 GetKeyNameTextW — register row AD-96 for the DirectInput-vs-GetKeyNameText adaptation), else the DIK-suffix spelling. Bare modifier-key bindings show only the key name. - #396 capture feedback: clicking a mapping button now opens retail's instruction dialog (InitiateBinding @0x004899D0 -> OpenMapWarnDialog @0x00488A00): a type-2 WAIT dialog on retail's MapWarn queue key 0x10000001 with ID_ActionKeyMap_MapInstructions (0x23000004, ACTION variable interpolated), closed on key hit or ESC through the capture callback; capture is not armed if the dialog cannot open, matching retail. New RetailWaitDialogView (wait root 0x31 — same authored popup/message pair 0x3D/0x3E as the confirmation root, live-DAT probed) behind a shared IRetailDialogView presenter seam. Probe evidence (env-gated, kept): KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings. Register: AD-96 filed. Gate script OP8 section updated (step 4 rewritten; the "pressed/active state is enough" contract is retired). Full Release solution suite green (13,424 passed / 4 skips). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
254 lines
12 KiB
C#
254 lines
12 KiB
C#
using AcDream.App.Platform;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Campaign OP slice OP8 re-gate (2026-08-14): retail's key-binding display
|
|
/// names — the port of <c>CInputManager_WIN32::GetNameFromKey @ 0x00687F40</c>
|
|
/// (QualifiedControl overload) over
|
|
/// <c>GetNameFromKey_Internal @ 0x00687800</c>.
|
|
///
|
|
/// <para>
|
|
/// 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 (<c>ControlSpecification::GetDIKName @ 0x0068ACB0</c> —
|
|
/// "DIK_W", "DIK_LSHIFT", ...): plain keys through string-table enum 4 and
|
|
/// meta keys through enum 5 (<c>DBCache::GetDIDFromEnumStatic</c> category 4
|
|
/// resolves those to DIDs <c>0x2300000A</c> / <c>0x2300000B</c> 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
|
|
/// (<c>IDirectInputDevice8::GetObjectInfo</c>, DIPH_BYOFFSET) and shows
|
|
/// <c>tszName</c>, which is why a Swedish layout displays "SKIFT". This port
|
|
/// asks Win32 <c>GetKeyNameTextW</c> through
|
|
/// <see cref="PlatformKeyNameProvider"/> 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).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// 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 <c>ID_KeyDescDelimiter</c> ("+", table enum 3 →
|
|
/// DID <c>0x23000007</c>). A binding whose KEY IS a modifier key (retail's
|
|
/// walk-mode DIK_LSHIFT row has meta-mode 0; acdream's <see cref="KeyChord"/>
|
|
/// carries the wire-side self-modifier bit) shows only the key name — never
|
|
/// "Shift+ShiftLeft".
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class RetailKeyNames
|
|
{
|
|
/// <summary>String-table enum 4 via GetDIDFromEnumStatic category 4.</summary>
|
|
public const uint KeyNameTableId = 0x2300000Au;
|
|
|
|
/// <summary>String-table enum 5 via GetDIDFromEnumStatic category 4.</summary>
|
|
public const uint MetaKeyNameTableId = 0x2300000Bu;
|
|
|
|
/// <summary>String-table enum 3 via GetDIDFromEnumStatic category 4 —
|
|
/// holds <c>ID_KeyDescDelimiter</c>.</summary>
|
|
public const uint DelimiterTableId = 0x23000007u;
|
|
|
|
private readonly Func<uint, uint, string?> _resolveString;
|
|
private readonly Func<byte, bool, string?>? _osKeyName;
|
|
private readonly string _delimiter;
|
|
|
|
/// <param name="resolveString">DAT string lookup — (tableDid, stringHash) →
|
|
/// text, the same seam <c>KeyboardConfigController</c> already receives.</param>
|
|
/// <param name="osKeyName">OS-localized key-name lookup — (scanCode,
|
|
/// isExtended) → name. Null selects
|
|
/// <see cref="PlatformKeyNameProvider.ForCurrentProcess"/> (Win32
|
|
/// <c>GetKeyNameTextW</c> on Windows, no OS lookup elsewhere). Tests
|
|
/// inject deterministic fakes here.</param>
|
|
public RetailKeyNames(
|
|
Func<uint, uint, string?> resolveString,
|
|
Func<byte, bool, string?>? osKeyName = null)
|
|
{
|
|
_resolveString = resolveString
|
|
?? throw new ArgumentNullException(nameof(resolveString));
|
|
_osKeyName = osKeyName ?? PlatformKeyNameProvider.ForCurrentProcess();
|
|
_delimiter = resolveString(
|
|
DelimiterTableId, DatStringResolver.ComputeHash("ID_KeyDescDelimiter"))
|
|
?? "+";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display name for one bound chord — retail
|
|
/// <c>GetNameFromKey(QualifiedControl)</c>. 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).
|
|
/// </summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Silk key → DirectInput scan code + DIK name — the reverse of
|
|
/// <see cref="RetailScanCodeMap.ToSilkKey"/>'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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|