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>
This commit is contained in:
parent
1528e5693b
commit
30fa6ee507
16 changed files with 1105 additions and 24 deletions
|
|
@ -68,6 +68,10 @@ public sealed class KeyboardConfigControllerTests
|
|||
public int ToggleCalls { get; private set; }
|
||||
public Action<KeyChord?>? PendingCapture { get; private set; }
|
||||
public (string Message, Action<bool> OnResult)? PendingConfirm { get; private set; }
|
||||
public List<string> InstructionOpens { get; } = new();
|
||||
public List<uint> InstructionCloses { get; } = new();
|
||||
public uint NextInstructionContext { get; set; } = 7u;
|
||||
public bool WireInstructions { get; set; }
|
||||
|
||||
public void Capture(KeyChord? chord)
|
||||
{
|
||||
|
|
@ -101,7 +105,15 @@ public sealed class KeyboardConfigControllerTests
|
|||
Toggle: () => ToggleCalls++,
|
||||
DisplaySystemMessage: msg => Messages.Add(msg),
|
||||
NonBindableRefusalText: "cannot overwrite",
|
||||
ConfirmOverwrite: (message, onResult) => PendingConfirm = (message, onResult));
|
||||
ConfirmOverwrite: (message, onResult) => PendingConfirm = (message, onResult),
|
||||
OpenCaptureInstructions: WireInstructions
|
||||
? label =>
|
||||
{
|
||||
InstructionOpens.Add(label);
|
||||
return NextInstructionContext;
|
||||
}
|
||||
: null,
|
||||
CloseCaptureInstructions: context => InstructionCloses.Add(context));
|
||||
}
|
||||
|
||||
private static readonly KeyChord ChordW = new(Silk.NET.Input.Key.W, ModifierMask.None);
|
||||
|
|
@ -270,6 +282,94 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.Empty(fake.MappedSets);
|
||||
}
|
||||
|
||||
// OP8 re-gate (2026-08-14): retail InitiateBinding @ 0x004899D0 opens the
|
||||
// capture-instruction wait dialog before arming the key handler, closes it
|
||||
// when the capture ends (key or ESC), and refuses to arm at all when the
|
||||
// dialog could not open.
|
||||
[Fact]
|
||||
public void KeyButtonClick_OpensInstructionDialog_AndClosesOnCapturedKey()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[] { Row(0x4, 0x29, RetailActionClass.Movement) });
|
||||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 42u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
||||
Assert.Single(fake.InstructionOpens);
|
||||
Assert.NotNull(fake.PendingCapture);
|
||||
Assert.Empty(fake.InstructionCloses);
|
||||
|
||||
fake.Capture(ChordW);
|
||||
Assert.Equal(new[] { 42u }, fake.InstructionCloses);
|
||||
Assert.Contains(ChordW, row.Model.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyButtonClick_EscapeCapture_StillClosesInstructionDialog()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[] { Row(0x4, 0x29, RetailActionClass.Movement) });
|
||||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 9u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(null); // Escape sentinel
|
||||
|
||||
Assert.Equal(new[] { 9u }, fake.InstructionCloses);
|
||||
Assert.Empty(fake.MappedSets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyButtonClick_InstructionDialogUnavailable_DoesNotArmCapture()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[] { Row(0x4, 0x29, RetailActionClass.Movement) });
|
||||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 0u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
||||
// Retail refuses to register the input handler when OpenMapWarnDialog
|
||||
// fails; the capture must not be armed either.
|
||||
Assert.Null(fake.PendingCapture);
|
||||
Assert.Empty(fake.InstructionCloses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_ResolvesTheRowTemplatesAuthoredCaptionFont_OncePerTemplate()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
Row(0x4, 0x2A, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var requests = new List<(uint LayoutId, uint ElementId)>();
|
||||
KeyboardConfigController? controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings(),
|
||||
resolveTemplateFont: (layoutId, elementId) =>
|
||||
{
|
||||
requests.Add((layoutId, elementId));
|
||||
return null; // UiDatFont needs GPU atlases — the call contract is the assertion.
|
||||
});
|
||||
|
||||
Assert.NotNull(controller);
|
||||
// The authored action-row template (0x21000009 element 0x1000002F,
|
||||
// FontDid 0x4000000A — live-DAT probed 2026-08-14) is resolved exactly
|
||||
// once, not once per row: the per-template cache absorbs row N > 1.
|
||||
(uint LayoutId, uint ElementId) single = Assert.Single(requests);
|
||||
Assert.Equal(KeyboardConfigController.LayoutId, single.LayoutId);
|
||||
Assert.Equal(0x1000002Fu, single.ElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyButtonRightClick_ErasesThatSlot()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue