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>
150 lines
5.7 KiB
C#
150 lines
5.7 KiB
C#
using AcDream.App.UI.Layout;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// OP8 re-gate (2026-08-14): retail's key-binding display names —
|
|
/// <c>CInputManager_WIN32::GetNameFromKey @ 0x00687F40</c> over
|
|
/// <c>GetNameFromKey_Internal @ 0x00687800</c>. DAT string-table override
|
|
/// first (key table enum 4 → DID 0x2300000A, meta enum 5 → 0x2300000B),
|
|
/// OS-localized key name second, DIK-suffix spelling last; modifier prefixes
|
|
/// join through the authored ID_KeyDescDelimiter (enum 3 → 0x23000007).
|
|
/// </summary>
|
|
public sealed class RetailKeyNamesTests
|
|
{
|
|
private static string? NoStrings(uint table, uint hash) => null;
|
|
|
|
private static Func<uint, uint, string?> Table(
|
|
params (uint Table, string Key, string Value)[] entries)
|
|
=> (table, hash) =>
|
|
{
|
|
foreach ((uint t, string key, string value) in entries)
|
|
if (t == table && DatStringResolver.ComputeHash(key) == hash)
|
|
return value;
|
|
return null;
|
|
};
|
|
|
|
[Fact]
|
|
public void DatTableOverride_WinsOverOsName()
|
|
{
|
|
// The shipped dat authors DIK_LCONTROL -> "Left Ctrl" in 0x2300000A
|
|
// (live-probed 2026-08-14); the OS name must not be consulted.
|
|
var names = new RetailKeyNames(
|
|
Table((RetailKeyNames.KeyNameTableId, "DIK_LCONTROL", "Left Ctrl")),
|
|
osKeyName: (_, _) => throw new InvalidOperationException("OS lookup must not run"));
|
|
|
|
Assert.Equal("Left Ctrl", names.Describe(new KeyChord(Key.ControlLeft, ModifierMask.None)));
|
|
}
|
|
|
|
[Fact]
|
|
public void OsLocalizedName_UsedWhenTheDatTableMisses()
|
|
{
|
|
// DIK_LSHIFT has no authored override in the shipped dat — retail
|
|
// shows the keyboard layout's own name ("SKIFT" on Swedish).
|
|
var names = new RetailKeyNames(
|
|
NoStrings,
|
|
osKeyName: (scan, extended) =>
|
|
scan == 0x2A && !extended ? "SKIFT" : null);
|
|
|
|
Assert.Equal("SKIFT", names.Describe(new KeyChord(Key.ShiftLeft, ModifierMask.None)));
|
|
}
|
|
|
|
[Fact]
|
|
public void SelfModifier_ShowsOnlyTheKeyName_NeverShiftPlusShiftLeft()
|
|
{
|
|
// acdream's wire-side chord for retail's bare DIK_LSHIFT walk-mode row
|
|
// carries the self-modifier bit; retail's QualifiedControl has
|
|
// meta-mode 0 and displays just the key.
|
|
var names = new RetailKeyNames(
|
|
NoStrings,
|
|
osKeyName: (scan, _) => scan == 0x2A ? "SKIFT" : null);
|
|
|
|
Assert.Equal("SKIFT", names.Describe(new KeyChord(Key.ShiftLeft, ModifierMask.Shift)));
|
|
// The fake OS lookup only answers LSHIFT's scan code; RSHIFT proves
|
|
// the same no-prefix rule through the DIK-suffix fallback instead.
|
|
Assert.Equal("RSHIFT", names.Describe(new KeyChord(Key.ShiftRight, ModifierMask.Shift)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ModifierPrefixes_JoinWithTheAuthoredDelimiter_InMetaBitOrder()
|
|
{
|
|
// Meta-mode bits ascending (Shift=1, Ctrl=2, Alt=4 — the shipped
|
|
// keymap's Metakeys header), each named through the meta table + OS
|
|
// fallback, joined by ID_KeyDescDelimiter.
|
|
var names = new RetailKeyNames(
|
|
Table((RetailKeyNames.DelimiterTableId, "ID_KeyDescDelimiter", "+")),
|
|
osKeyName: (scan, _) => scan switch
|
|
{
|
|
0x2A => "SKIFT",
|
|
0x1D => "CTRL",
|
|
0x38 => "ALT",
|
|
0x32 => "M",
|
|
_ => null,
|
|
});
|
|
|
|
Assert.Equal(
|
|
"SKIFT+CTRL+ALT+M",
|
|
names.Describe(new KeyChord(
|
|
Key.M, ModifierMask.Shift | ModifierMask.Ctrl | ModifierMask.Alt)));
|
|
}
|
|
|
|
[Fact]
|
|
public void MetaTableOverride_WinsForTheModifierPrefix()
|
|
{
|
|
var names = new RetailKeyNames(
|
|
Table(
|
|
(RetailKeyNames.DelimiterTableId, "ID_KeyDescDelimiter", "+"),
|
|
(RetailKeyNames.MetaKeyNameTableId, "DIK_LSHIFT", "Shift")),
|
|
osKeyName: (scan, _) => scan == 0x32 ? "M" : null);
|
|
|
|
Assert.Equal("Shift+M", names.Describe(new KeyChord(Key.M, ModifierMask.Shift)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtendedKeys_PassTheExtendedFlagToTheOsLookup()
|
|
{
|
|
// DIK_UP = 0xC8: scan 0x48 + the extended bit — the same split
|
|
// GetKeyNameText expects in lParam bit 24.
|
|
(byte Scan, bool Extended)? seen = null;
|
|
var names = new RetailKeyNames(
|
|
NoStrings,
|
|
osKeyName: (scan, extended) =>
|
|
{
|
|
seen = (scan, extended);
|
|
return "UP ARROW";
|
|
});
|
|
|
|
Assert.Equal("UP ARROW", names.Describe(new KeyChord(Key.Up, ModifierMask.None)));
|
|
Assert.Equal(((byte)0x48, true), seen);
|
|
}
|
|
|
|
[Fact]
|
|
public void DikSuffixSpelling_WhenBothDatAndOsMiss()
|
|
{
|
|
var names = new RetailKeyNames(NoStrings, osKeyName: (_, _) => null);
|
|
|
|
Assert.Equal("W", names.Describe(new KeyChord(Key.W, ModifierMask.None)));
|
|
Assert.Equal("NUMPADENTER", names.Describe(new KeyChord(Key.KeypadEnter, ModifierMask.None)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ControlsOutsideTheDikTable_KeepTheEnumSpelling()
|
|
{
|
|
var names = new RetailKeyNames(NoStrings, osKeyName: (_, _) => null);
|
|
|
|
// Key.F13 never appears in the DAT's 84 observed scan codes.
|
|
Assert.Equal("F13", names.Describe(new KeyChord(Key.F13, ModifierMask.None)));
|
|
Assert.Equal(
|
|
"Shift+F13",
|
|
names.Describe(new KeyChord(Key.F13, ModifierMask.Shift)));
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultChord_DescribesAsEmpty()
|
|
{
|
|
var names = new RetailKeyNames(NoStrings, osKeyName: (_, _) => null);
|
|
Assert.Equal(string.Empty, names.Describe(default));
|
|
}
|
|
}
|