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
|
|
@ -210,13 +210,27 @@ public sealed class KeyboardConfigController
|
|||
// BEFORE reassigning a chord already bound to another row on this screen.
|
||||
// message is pre-composed (real row labels, no invented retail text);
|
||||
// the callback receives the user's Yes(true)/No(false) choice.
|
||||
Action<string, Action<bool>> ConfirmOverwrite);
|
||||
Action<string, Action<bool>> ConfirmOverwrite,
|
||||
// OP8 re-gate (2026-08-14): retail's capture-instruction dialog —
|
||||
// UIOption_ActionKeyMap::InitiateBinding @ 0x004899D0 opens the
|
||||
// MapWarn wait dialog (ID_ActionKeyMap_MapInstructions with the row's
|
||||
// action label interpolated) BEFORE registering the key handler, and
|
||||
// refuses to arm capture at all if the dialog could not open. Open
|
||||
// receives the row label and returns the dialog context (0 = could
|
||||
// not open); Close closes that context when the capture ends (key hit
|
||||
// or ESC). Null keeps the pre-dialog capture behavior for hosts with
|
||||
// no dialog factory (unit fixtures).
|
||||
Func<string, uint>? OpenCaptureInstructions = null,
|
||||
Action<uint>? CloseCaptureInstructions = null);
|
||||
|
||||
public OptionPage Page { get; } = new();
|
||||
public IReadOnlyList<RowView> Rows => _rows;
|
||||
|
||||
private readonly List<RowView> _rows = new();
|
||||
private readonly Dictionary<(uint LayoutId, uint ElementId), UiDatFont?> _templateFontCache = new();
|
||||
private Bindings? _bindings;
|
||||
private Func<KeyChord, string> _describe = DescribeChord;
|
||||
private Func<uint, uint, UiDatFont?>? _resolveTemplateFont;
|
||||
|
||||
private KeyboardConfigController() { }
|
||||
|
||||
|
|
@ -233,7 +247,8 @@ public sealed class KeyboardConfigController
|
|||
RetailActionMapSnapshot snapshot,
|
||||
Func<uint, uint, UiElement?> templateResolver,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings)
|
||||
Bindings bindings,
|
||||
Func<uint, uint, UiDatFont?>? resolveTemplateFont = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
|
@ -249,7 +264,15 @@ public sealed class KeyboardConfigController
|
|||
return null;
|
||||
}
|
||||
|
||||
var controller = new KeyboardConfigController { _bindings = bindings };
|
||||
var controller = new KeyboardConfigController
|
||||
{
|
||||
_bindings = bindings,
|
||||
_resolveTemplateFont = resolveTemplateFont,
|
||||
// OP8 re-gate (2026-08-14): key-button captions through retail's
|
||||
// GetNameFromKey pipeline (DAT table override → OS-localized name)
|
||||
// instead of raw enum spellings ("Shift+ShiftLeft").
|
||||
_describe = new RetailKeyNames(resolveString).Describe,
|
||||
};
|
||||
|
||||
var byClass = snapshot.Rows
|
||||
.Where(r => r.ActionClass != RetailActionClass.None)
|
||||
|
|
@ -391,6 +414,12 @@ public sealed class KeyboardConfigController
|
|||
Padding = 2f,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top,
|
||||
DefaultColor = mapped ? Vector4.One : UiRenderContext.StoreOnlyCaptionColor,
|
||||
// OP8 re-gate (2026-08-14): retail draws the row's action label
|
||||
// with the row template's OWN authored font (UIOption_ActionKeyMap
|
||||
// element 0x1000002F authors FontDid 0x4000000A, the 18px serif —
|
||||
// live-DAT probed). The synthesized caption previously set no
|
||||
// DatFont at all and fell back to the debug bitmap font.
|
||||
DatFont = RowCaptionFont(listBox),
|
||||
};
|
||||
if (label is not null)
|
||||
captionText.LinesProvider = () => new[] { new UiText.Line(label, captionText.DefaultColor) };
|
||||
|
|
@ -451,6 +480,24 @@ public sealed class KeyboardConfigController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>The authored font of this ListBox's action-row template,
|
||||
/// resolved once per (layout, element) pair. Null (template import or
|
||||
/// font-load failure, or no resolver wired) keeps the debug-font fallback.</summary>
|
||||
private UiDatFont? RowCaptionFont(UiTemplateListBox listBox)
|
||||
{
|
||||
if (_resolveTemplateFont is null
|
||||
|| RowTemplateIndex >= listBox.Templates.Count)
|
||||
return null;
|
||||
(uint layoutId, uint elementId) = (
|
||||
listBox.Templates[RowTemplateIndex].TemplateLayoutId,
|
||||
listBox.Templates[RowTemplateIndex].TemplateElementId);
|
||||
if (_templateFontCache.TryGetValue((layoutId, elementId), out UiDatFont? cached))
|
||||
return cached;
|
||||
UiDatFont? font = _resolveTemplateFont(layoutId, elementId);
|
||||
_templateFontCache[(layoutId, elementId)] = font;
|
||||
return font;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<KeyChord> DatDefaultsToChords(IReadOnlyList<RetailKeyChord> raw)
|
||||
{
|
||||
var result = new List<KeyChord>(raw.Count);
|
||||
|
|
@ -463,16 +510,19 @@ public sealed class KeyboardConfigController
|
|||
return result;
|
||||
}
|
||||
|
||||
private static void RefreshRowButtons(RowView view)
|
||||
private void RefreshRowButtons(RowView view)
|
||||
{
|
||||
IReadOnlyList<KeyChord> current = view.Model.Current;
|
||||
for (int i = 0; i < view.KeyButtons.Count; i++)
|
||||
{
|
||||
bool bound = i < current.Count && current[i] != default;
|
||||
view.KeyButtons[i].Label = bound ? DescribeChord(current[i]) : null;
|
||||
view.KeyButtons[i].Label = bound ? _describe(current[i]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raw enum spelling — construction-time default until Bind swaps
|
||||
/// in <see cref="RetailKeyNames.Describe"/>, and that class's own fallback
|
||||
/// for controls outside the DIK table.</summary>
|
||||
private static string DescribeChord(KeyChord chord)
|
||||
{
|
||||
string mods = chord.Modifiers == ModifierMask.None ? "" : chord.Modifiers.ToString() + "+";
|
||||
|
|
@ -481,8 +531,28 @@ public sealed class KeyboardConfigController
|
|||
|
||||
private void BeginSlotCapture(RowView view, int slot, Bindings bindings)
|
||||
{
|
||||
// Retail InitiateBinding @ 0x004899D0: open the capture-instruction
|
||||
// dialog (ID_ActionKeyMap_MapInstructions with this row's label) and
|
||||
// register the input handler ONLY if the dialog opened. A host with no
|
||||
// dialog seam wired (null) keeps the dialog-less capture.
|
||||
uint instructionsContext = 0u;
|
||||
if (bindings.OpenCaptureInstructions is { } openInstructions)
|
||||
{
|
||||
instructionsContext = openInstructions(view.Label ?? string.Empty);
|
||||
if (instructionsContext == 0u)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"[D.2b] KeyboardConfigController: capture-instruction dialog "
|
||||
+ "could not open — capture not armed (retail refuses too).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bindings.BeginCapture(captured =>
|
||||
{
|
||||
if (instructionsContext != 0u)
|
||||
bindings.CloseCaptureInstructions?.Invoke(instructionsContext);
|
||||
|
||||
if (captured is not { } chord) return; // Escape — retail cancels silently.
|
||||
|
||||
(ConflictOutcome outcome, List<RowView> conflictRows) = FindConflicts(chord, exclude: view);
|
||||
|
|
@ -504,7 +574,7 @@ public sealed class KeyboardConfigController
|
|||
// the first). Only on accept do the losing rows lose the slot.
|
||||
string names = string.Join(", ", conflictRows.Select(r => r.Label ?? "?"));
|
||||
string message =
|
||||
$"'{DescribeChord(chord)}' is already bound to {names}. "
|
||||
$"'{_describe(chord)}' is already bound to {names}. "
|
||||
+ $"Reassign it to '{view.Label}'?";
|
||||
bindings.ConfirmOverwrite(message, accepted =>
|
||||
{
|
||||
|
|
@ -525,7 +595,7 @@ public sealed class KeyboardConfigController
|
|||
});
|
||||
}
|
||||
|
||||
private static void ApplySlot(RowView view, int slot, KeyChord chord)
|
||||
private void ApplySlot(RowView view, int slot, KeyChord chord)
|
||||
{
|
||||
List<KeyChord> updated = new(view.Model.Current);
|
||||
while (updated.Count <= slot) updated.Add(default);
|
||||
|
|
@ -621,7 +691,7 @@ public sealed class KeyboardConfigController
|
|||
row.Model.SetDefaultValue(row.Model.DefaultValue);
|
||||
controller.Page.Defaults();
|
||||
foreach (RowView row in controller._rows)
|
||||
RefreshRowButtons(row);
|
||||
controller.RefreshRowButtons(row);
|
||||
};
|
||||
|
||||
if (layout.FindElement(RevertButtonId) is UiButton revertButton)
|
||||
|
|
@ -629,7 +699,7 @@ public sealed class KeyboardConfigController
|
|||
{
|
||||
controller.Page.Reset();
|
||||
foreach (RowView row in controller._rows)
|
||||
RefreshRowButtons(row);
|
||||
controller.RefreshRowButtons(row);
|
||||
};
|
||||
|
||||
// OK — right-click release in retail (idMessage 0x19); ported as a plain
|
||||
|
|
@ -650,7 +720,7 @@ public sealed class KeyboardConfigController
|
|||
{
|
||||
controller.Page.Reset();
|
||||
foreach (RowView row in controller._rows)
|
||||
RefreshRowButtons(row);
|
||||
controller.RefreshRowButtons(row);
|
||||
bindings.Toggle();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue